From 85308a399fe8b0d68617dce61161e2a018d396ff Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Fri, 26 Apr 2024 10:19:52 +0200 Subject: [PATCH 01/10] :construction: :sparkles: create contracts context --- components/Sidebar.tsx | 22 +++--- components/create/createLoader.tsx | 29 +++++--- components/import/importLoader.tsx | 26 ++++--- components/topUpForm.tsx | 17 +++-- context/contracts.tsx | 97 +++++++++++++++++++++++++++ context/dapps.tsx | 21 +++++- pages/[walletAddress]/fund-wallet.tsx | 12 ++-- pages/[walletAddress]/history.tsx | 22 +++--- pages/[walletAddress]/proposals.tsx | 30 ++++++--- pages/[walletAddress]/settings.tsx | 17 +++-- pages/_app.tsx | 5 +- utils/localStorage.ts | 3 + 12 files changed, 232 insertions(+), 69 deletions(-) create mode 100644 context/contracts.tsx diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 6756d5d1..4ba07891 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -18,6 +18,7 @@ import React, { useState, } from "react"; import { PREFERED_NETWORK } from "../context/config"; +import { useContracts } from "../context/contracts"; import { useAppDispatch, useAppState } from "../context/state"; import { TezosToolkitContext } from "../context/tezos-toolkit"; import fetchVersion from "../context/version"; @@ -144,6 +145,7 @@ const Sidebar = ({ const { userAddress } = useWallet(); const { tezos } = useContext(TezosToolkitContext); + const { addOrUpdateContract } = useContracts(); const isOwner = useIsOwner(); @@ -173,15 +175,17 @@ const Sidebar = ({ const updatedContract = toStorage(version, storage, balance); - state.contracts[state.currentContract] - ? dispatch({ - type: "updateContract", - payload: { - address: state.currentContract, - contract: updatedContract, - }, - }) - : null; + if (state.contracts[state.currentContract]) + addOrUpdateContract(state.currentContract, updatedContract); + // state.contracts[state.currentContract] + // ? dispatch({ + // type: "updateContract", + // payload: { + // address: state.currentContract, + // contract: updatedContract, + // }, + // }) + // : null; })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.currentContract]); diff --git a/components/create/createLoader.tsx b/components/create/createLoader.tsx index c71dc0b7..ef0154d5 100644 --- a/components/create/createLoader.tsx +++ b/components/create/createLoader.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import React, { useContext, useEffect, useState } from "react"; +import { useContracts } from "../../context/contracts"; import FormContext from "../../context/formContext"; import { ContractStorage, @@ -18,6 +19,7 @@ function Success() { const [address, setAddress] = useState({ status: 0, address: "" }); const [loading, setLoading] = useState(true); const { tezos } = useTezosToolkit(); + const { addOrUpdateContract } = useContracts(); useEffect(() => { (async () => { @@ -49,17 +51,22 @@ function Success() { const balance = await tezos.tz.getBalance(tzsafe!.address!); setAddress({ address: tzsafe?.address!, status: 1 }); setLoading(false); - dispatch!({ - type: "addContract", - payload: { - aliases: Object.fromEntries([ - ...formState!.validators!.map(x => [x.address, x.name]), - [tzsafe?.address!, formState?.walletName || ""], - ]), - contract: toStorage(formState.version, c, balance!), - address: tzsafe!.address!, - }, - }); + // dispatch!({ + // type: "addContract", + // payload: { + // aliases: Object.fromEntries([ + // ...formState!.validators!.map(x => [x.address, x.name]), + // [tzsafe?.address!, formState?.walletName || ""], + // ]), + // contract: toStorage(formState.version, c, balance!), + // address: tzsafe!.address!, + // }, + // }); + addOrUpdateContract( + tzsafe!.address, + toStorage(formState.version, c, balance!) + ); + // TODO ADD ALIAS } catch (err) { console.log(err); setAddress({ status: -1, address: "" }); diff --git a/components/import/importLoader.tsx b/components/import/importLoader.tsx index 3bef948c..65c6f8fa 100644 --- a/components/import/importLoader.tsx +++ b/components/import/importLoader.tsx @@ -1,6 +1,7 @@ import { tzip16 } from "@taquito/tzip16"; import Link from "next/link"; import React, { useContext, useEffect, useState } from "react"; +import { useContracts } from "../../context/contracts"; import FormContext from "../../context/formContext"; import { useAppDispatch, useAppState } from "../../context/state"; import { useTezosToolkit } from "../../context/tezos-toolkit"; @@ -18,6 +19,7 @@ function Success() { }); let [loading, setLoading] = useState(true); const { tezos } = useTezosToolkit(); + const { addOrUpdateContract } = useContracts(); useEffect(() => { (async () => { @@ -31,17 +33,19 @@ function Success() { setAddress({ address: address.address, status: 1 }); setLoading(false); - dispatch!({ - type: "addContract", - payload: { - aliases: Object.fromEntries([ - ...formState!.validators!.map(x => [x.address, x.name]), - [address.address!, formState?.walletName || ""], - ]), - contract: v, - address: address.address, - }, - }); + // dispatch!({ + // type: "addContract", + // payload: { + // aliases: Object.fromEntries([ + // ...formState!.validators!.map(x => [x.address, x.name]), + // [address.address!, formState?.walletName || ""], + // ]), + // contract: v, + // address: address.address, + // }, + // }); + // TODO ADD ALIAS + addOrUpdateContract(address.address, v); } catch (err) { console.log(err); setAddress({ status: -1, address: "" }); diff --git a/components/topUpForm.tsx b/components/topUpForm.tsx index 6549cd17..7f25d843 100644 --- a/components/topUpForm.tsx +++ b/components/topUpForm.tsx @@ -12,6 +12,7 @@ import React, { useState, } from "react"; import { TZKT_API_URL, MODAL_TIMEOUT, THUMBNAIL_URL } from "../context/config"; +import { useContracts } from "../context/contracts"; import { useAppDispatch, useAppState } from "../context/state"; import { TezosToolkitContext } from "../context/tezos-toolkit"; import fetchVersion from "../context/version"; @@ -96,6 +97,7 @@ function TopUp(props: { const [options, setOptions] = useState([]); const fetchOffsetRef = useRef(0); const { userBalance } = useWallet(); + const { addOrUpdateContract } = useContracts(); const fetchTokens = useCallback( (value: string, offset: number) => @@ -286,13 +288,14 @@ function TopUp(props: { const storage = toStorage(version, cs, balance); if (!!state.contracts[props.address]) { - dispatch({ - type: "updateContract", - payload: { - address: props.address, - contract: toStorage(version, cs, balance), - }, - }); + // dispatch({ + // type: "updateContract", + // payload: { + // address: props.address, + // contract: toStorage(version, cs, balance), + // }, + // }); + addOrUpdateContract(props.address, toStorage(version, cs, balance)); } else { storage.address = props.address; diff --git a/context/contracts.tsx b/context/contracts.tsx new file mode 100644 index 00000000..d5d82e33 --- /dev/null +++ b/context/contracts.tsx @@ -0,0 +1,97 @@ +import { createContext, useContext, useEffect, useReducer } from "react"; +import { ContractStorage, Contracts } from "../types/app"; +import { + loadContracts, + loadDapps, + saveContractsToStorage, +} from "../utils/localStorage"; +import { useWallet } from "./wallet"; + +type ContractsContextType = { + contracts: Contracts; + addOrUpdateContract(addr: string, contract: ContractStorage): void; + removeContract(addr: string): void; +}; + +type ContractsActions = + | { + type: "ADD_OR_UPDATE_CONTRACT"; + payload: { + contract: ContractStorage; + contractAddress: string; + }; + } + | { + type: "REMOVE_CONTRACT"; + payload: { contractAddress: string }; + } + | { type: "LOAD_CONTRACTS"; payload: { contracts: Contracts } }; + +const ContractsContext = createContext({ + contracts: {}, + addOrUpdateContract: () => {}, + removeContract: () => {}, +}); + +const reducer = (state: Contracts, action: ContractsActions) => { + switch (action.type) { + case "ADD_OR_UPDATE_CONTRACT": { + const p = action.payload; + return { ...state, [p.contractAddress]: p.contract }; + } + case "REMOVE_CONTRACT": { + const p = action.payload; + const { [p.contractAddress]: contract, ...otherContracts } = state; + return { ...state, ...otherContracts }; + } + case "LOAD_CONTRACTS": + return action.payload.contracts; + default: + return state; + } +}; + +export const ContractsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [state, dispatch] = useReducer(reducer, {}); + const { userAddress } = useWallet(); + + useEffect(() => { + const contracts = userAddress ? loadContracts(userAddress) : {}; + dispatch({ + type: "LOAD_CONTRACTS", + payload: { contracts: contracts ?? {} }, + }); + }, [userAddress]); + + // Save state to storage when state is updated + useEffect(() => { + if (Object.keys(state).length !== 0) + saveContractsToStorage(userAddress || "", state); + }, [state, userAddress]); + + return ( + + dispatch({ + type: "ADD_OR_UPDATE_CONTRACT", + payload: { contract, contractAddress: addr }, + }), + removeContract: addr => + dispatch({ + type: "REMOVE_CONTRACT", + payload: { contractAddress: addr }, + }), + }} + > + {children} + + ); +}; + +export const useContracts = () => useContext(ContractsContext); diff --git a/context/dapps.tsx b/context/dapps.tsx index 3fab2b65..a050fb3c 100644 --- a/context/dapps.tsx +++ b/context/dapps.tsx @@ -14,6 +14,7 @@ type P2PContextType = { getDappsByContract( contractAddress: string ): { [appUrl: string]: P2pData } | undefined; + removeContractDapps(contractAddress: string): void; }; type DappsActions = @@ -28,7 +29,11 @@ type DappsActions = type: "REMOVE_DAPP"; payload: { dappUrl: string; contractAddress: string }; } - | { type: "LOAD_DAPPS"; payload: { dApps: ConnectedDapps } }; + | { type: "LOAD_DAPPS"; payload: { dApps: ConnectedDapps } } + | { + type: "REMOVE_CONTRACT_DAPPS"; + payload: { contractAddress: string }; + }; const P2PContext = createContext({ client: new P2PClient({ @@ -39,6 +44,7 @@ const P2PContext = createContext({ addDapp: () => {}, removeDapp: () => {}, getDappsByContract: () => ({}), + removeContractDapps: () => {}, }); const createP2PClientConnection = async () => { @@ -97,6 +103,10 @@ const reducer = (state: ConnectedDapps, action: DappsActions) => { ...connectedDapps } = state; return { ...state, ...{ ...connectedDapps, othersApps } }; + case "REMOVE_CONTRACT_DAPPS": + const { [action.payload.contractAddress]: deletedContract, ...others } = + state; + return { ...state, ...others }; case "LOAD_DAPPS": return action.payload.dApps; default: @@ -147,6 +157,15 @@ export const P2PProvider = ({ children }: { children: React.ReactNode }) => { }); }, getDappsByContract: contractAddress => state[contractAddress], + removeContractDapps: contractAddress => { + Object.values(state[contractAddress]).forEach( + async dapp => await removeDappConnection(dapp, client) + ); + dispatch({ + type: "REMOVE_CONTRACT_DAPPS", + payload: { contractAddress }, + }); + }, }} > {children} diff --git a/pages/[walletAddress]/fund-wallet.tsx b/pages/[walletAddress]/fund-wallet.tsx index da973587..cd1bb1d3 100644 --- a/pages/[walletAddress]/fund-wallet.tsx +++ b/pages/[walletAddress]/fund-wallet.tsx @@ -8,6 +8,7 @@ import { renderWarning } from "../../components/formUtils"; import Meta from "../../components/meta"; import TopUp from "../../components/topUpForm"; import { TZKT_API_URL } from "../../context/config"; +import { useContracts } from "../../context/contracts"; import { useAppDispatch, useAppState } from "../../context/state"; import { TezosToolkitContext } from "../../context/tezos-toolkit"; import { useWallet } from "../../context/wallet"; @@ -24,6 +25,7 @@ const TopUpPage = () => { const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(false); const [isSuccess, setIsSuccess] = useState(false); + const { addOrUpdateContract } = useContracts(); const onSuccess = async (txId: string) => { if (!state.currentContract) return; @@ -59,10 +61,12 @@ const TopUpPage = () => { newContract.balance = new BigNumber(newContract.balance) .plus(transaction[0].amount as number) .toString(); - disptach({ - type: "updateContract", - payload: { contract: newContract, address: state.currentContract }, - }); + // disptach({ + // type: "updateContract", + // payload: { contract: newContract, address: state.currentContract }, + // }); + addOrUpdateContract(state.currentContract, newContract); + setIsLoading(false); setIsSuccess(true); } catch (e) { diff --git a/pages/[walletAddress]/history.tsx b/pages/[walletAddress]/history.tsx index 0eb3fa7b..bcd379f6 100644 --- a/pages/[walletAddress]/history.tsx +++ b/pages/[walletAddress]/history.tsx @@ -8,6 +8,7 @@ import HistoryTransfer from "../../components/history/HistoryTransfer"; import Meta from "../../components/meta"; import Modal from "../../components/modal"; import ProposalSignForm from "../../components/proposalSignForm"; +import { useContracts } from "../../context/contracts"; import { getTokenTransfers, getTransfers } from "../../context/proposals"; import { useAppDispatch, useAppState } from "../../context/state"; import { TezosToolkitContext } from "../../context/tezos-toolkit"; @@ -151,6 +152,7 @@ const History = () => { const { tezos } = useContext(TezosToolkitContext); const walletTokens = useWalletTokens(); + const { addOrUpdateContract } = useContracts(); const [state, dispatch] = useReducer(reducer, { isLoading: true, @@ -213,15 +215,17 @@ const History = () => { : fetchVersion(c)); const updatedContract = toStorage(version, storage, balance); - globalState.contracts[globalState.currentContract] - ? globalDispatch({ - type: "updateContract", - payload: { - address: globalState.currentContract, - contract: updatedContract, - }, - }) - : null; + if (globalState.contracts[globalState.currentContract]) + addOrUpdateContract(globalState.currentContract, updatedContract); + // globalState.contracts[globalState.currentContract] + // ? globalDispatch({ + // type: "updateContract", + // payload: { + // address: globalState.currentContract, + // contract: updatedContract, + // }, + // }) + // : null; storage.version = version; diff --git a/pages/[walletAddress]/proposals.tsx b/pages/[walletAddress]/proposals.tsx index 67ff8631..8e0d2939 100644 --- a/pages/[walletAddress]/proposals.tsx +++ b/pages/[walletAddress]/proposals.tsx @@ -7,6 +7,7 @@ import Spinner from "../../components/Spinner"; import Meta from "../../components/meta"; import Modal from "../../components/modal"; import ProposalSignForm from "../../components/proposalSignForm"; +import { useContracts } from "../../context/contracts"; import { tezosState, action as globalAction, @@ -143,7 +144,8 @@ async function getProposals( globalDispatch: Dispatch, dispatch: Dispatch, state: state, - tezos: TezosToolkit + tezos: TezosToolkit, + addOrUpdateContract: any ) { if (!globalState.currentContract) return; @@ -178,13 +180,18 @@ async function getProposals( if (globalState.contracts[globalState.currentContract ?? ""]) { const balance = await tezos.tz.getBalance(globalState.currentContract); - globalDispatch({ - type: "updateContract", - payload: { - address: globalState.currentContract, - contract: toStorage(version, storage, balance), - }, - }); + // globalDispatch({ + // type: "updateContract", + // payload: { + // address: globalState.currentContract, + // contract: toStorage(version, storage, balance), + // }, + // }); + + addOrUpdateContract( + globalState.currentContract, + toStorage(version, storage, balance) + ); } return proposals as proposals; @@ -195,6 +202,7 @@ const Proposals = () => { const globalDispatch = useAppDispatch(); const isOwner = useIsOwner(); const walletTokens = useWalletTokens(); + const { addOrUpdateContract } = useContracts(); const { userAddress } = useWallet(); @@ -252,7 +260,8 @@ const Proposals = () => { globalDispatch, dispatch, state, - tezos + tezos, + addOrUpdateContract ); if (!proposals) return; @@ -288,7 +297,8 @@ const Proposals = () => { globalDispatch, dispatch, state, - tezos + tezos, + addOrUpdateContract ); if (!proposals) return; diff --git a/pages/[walletAddress]/settings.tsx b/pages/[walletAddress]/settings.tsx index 018156bc..9237f450 100644 --- a/pages/[walletAddress]/settings.tsx +++ b/pages/[walletAddress]/settings.tsx @@ -2,17 +2,20 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import Meta from "../../components/meta"; import SignersForm from "../../components/signersForm"; -import { useAppDispatch, useAppState } from "../../context/state"; +import { useContracts } from "../../context/contracts"; +import { useDapps } from "../../context/dapps"; +import { useAppState } from "../../context/state"; import { ParsedUrlQueryContract } from "../../types/app"; import useIsOwner from "../../utils/useIsOwner"; const Settings = () => { const state = useAppState(); - const dispatch = useAppDispatch(); const router = useRouter(); const isOwner = useIsOwner(); const { walletAddress: currentContract } = router.query as ParsedUrlQueryContract; + const { removeContract } = useContracts(); + const { removeContractDapps } = useDapps(); const [canDelete, setCanDelete] = useState( !!state.contracts[currentContract] @@ -48,10 +51,12 @@ const Settings = () => { onClick={() => { setIsDeleting(true); setCanDelete(false); - dispatch!({ - type: "removeContract", - address: currentContract, - }); + // dispatch!({ + // type: "removeContract", + // address: currentContract, + // }); + removeContract(currentContract); + removeContractDapps(currentContract); const addresses = Object.keys(state.contracts); if (addresses.length === 0) { diff --git a/pages/_app.tsx b/pages/_app.tsx index d020508b..132fe9cb 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,7 @@ import type { AppProps } from "next/app"; import Layout from "../components/Layout"; import { AliasesProvider } from "../context/aliases"; +import { ContractsProvider } from "../context/contracts"; import { P2PProvider } from "../context/dapps"; import { AppStateProvider } from "../context/state"; import { TezosToolkitProvider } from "../context/tezos-toolkit"; @@ -14,7 +15,9 @@ export default function App({ Component, pageProps }: AppProps) { - + + + diff --git a/utils/localStorage.ts b/utils/localStorage.ts index 1d2d7149..a9eaafbe 100644 --- a/utils/localStorage.ts +++ b/utils/localStorage.ts @@ -53,3 +53,6 @@ export const loadStorage = (userAddress: string): AppStorage => { export const loadDapps = (userAddress: string) => loadStorage(userAddress).connectedDapps; + +export const loadContracts = (userAddress: string) => + loadStorage(userAddress).contracts; From b7eef4878d81b8afc56dd714476f850bc366c4b7 Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Fri, 26 Apr 2024 11:47:16 +0200 Subject: [PATCH 02/10] :recycle: store aliases into context and update files to use it --- components/Sidebar.tsx | 9 -- components/create/createLoader.tsx | 20 ++-- components/import/importLoader.tsx | 20 ++-- components/signersForm.tsx | 7 +- components/topUpForm.tsx | 7 -- context/aliases.tsx | 70 ++++++++++++- context/state.tsx | 138 -------------------------- pages/[walletAddress]/fund-wallet.tsx | 5 +- pages/[walletAddress]/history.tsx | 13 +-- pages/[walletAddress]/proposals.tsx | 8 -- pages/address-book.tsx | 16 +-- utils/localStorage.ts | 3 + 12 files changed, 97 insertions(+), 219 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 4ba07891..4e55942b 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -177,15 +177,6 @@ const Sidebar = ({ if (state.contracts[state.currentContract]) addOrUpdateContract(state.currentContract, updatedContract); - // state.contracts[state.currentContract] - // ? dispatch({ - // type: "updateContract", - // payload: { - // address: state.currentContract, - // contract: updatedContract, - // }, - // }) - // : null; })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.currentContract]); diff --git a/components/create/createLoader.tsx b/components/create/createLoader.tsx index ef0154d5..231f82b7 100644 --- a/components/create/createLoader.tsx +++ b/components/create/createLoader.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import React, { useContext, useEffect, useState } from "react"; +import { useAliases } from "../../context/aliases"; import { useContracts } from "../../context/contracts"; import FormContext from "../../context/formContext"; import { @@ -20,6 +21,7 @@ function Success() { const [loading, setLoading] = useState(true); const { tezos } = useTezosToolkit(); const { addOrUpdateContract } = useContracts(); + const { updateAliases } = useAliases(); useEffect(() => { (async () => { @@ -51,22 +53,16 @@ function Success() { const balance = await tezos.tz.getBalance(tzsafe!.address!); setAddress({ address: tzsafe?.address!, status: 1 }); setLoading(false); - // dispatch!({ - // type: "addContract", - // payload: { - // aliases: Object.fromEntries([ - // ...formState!.validators!.map(x => [x.address, x.name]), - // [tzsafe?.address!, formState?.walletName || ""], - // ]), - // contract: toStorage(formState.version, c, balance!), - // address: tzsafe!.address!, - // }, - // }); addOrUpdateContract( tzsafe!.address, toStorage(formState.version, c, balance!) ); - // TODO ADD ALIAS + updateAliases( + Object.fromEntries([ + ...formState.validators.map(x => [x.address, x.name]), + [tzsafe?.address!, formState?.walletName || ""], + ]) + ); } catch (err) { console.log(err); setAddress({ status: -1, address: "" }); diff --git a/components/import/importLoader.tsx b/components/import/importLoader.tsx index 65c6f8fa..90918092 100644 --- a/components/import/importLoader.tsx +++ b/components/import/importLoader.tsx @@ -1,6 +1,7 @@ import { tzip16 } from "@taquito/tzip16"; import Link from "next/link"; import React, { useContext, useEffect, useState } from "react"; +import { useAliases } from "../../context/aliases"; import { useContracts } from "../../context/contracts"; import FormContext from "../../context/formContext"; import { useAppDispatch, useAppState } from "../../context/state"; @@ -20,6 +21,7 @@ function Success() { let [loading, setLoading] = useState(true); const { tezos } = useTezosToolkit(); const { addOrUpdateContract } = useContracts(); + const { updateAliases } = useAliases(); useEffect(() => { (async () => { @@ -33,19 +35,13 @@ function Success() { setAddress({ address: address.address, status: 1 }); setLoading(false); - // dispatch!({ - // type: "addContract", - // payload: { - // aliases: Object.fromEntries([ - // ...formState!.validators!.map(x => [x.address, x.name]), - // [address.address!, formState?.walletName || ""], - // ]), - // contract: v, - // address: address.address, - // }, - // }); - // TODO ADD ALIAS addOrUpdateContract(address.address, v); + updateAliases( + Object.fromEntries([ + ...formState!.validators.map(x => [x.address, x.name]), + [address.address!, formState?.walletName || ""], + ]) + ); } catch (err) { console.log(err); setAddress({ status: -1, address: "" }); diff --git a/components/signersForm.tsx b/components/signersForm.tsx index d48d0e96..ff19220b 100644 --- a/components/signersForm.tsx +++ b/components/signersForm.tsx @@ -11,6 +11,7 @@ import { } from "formik"; import { useRouter } from "next/router"; import { FC, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useAliases } from "../context/aliases"; import { MODAL_TIMEOUT, PREFERED_NETWORK, @@ -90,6 +91,7 @@ const SignersForm: FC<{ const state = useAppState(); const dispatch = useAppDispatch(); const { userAddress } = useWallet(); + const { updateAliases } = useAliases(); const { tezos } = useContext(TezosToolkitContext); const router = useRouter(); const bakerAddressRef = useRef(null); @@ -474,10 +476,7 @@ const SignersForm: FC<{ try { await updateSettings(getOpsHelper(values)); setResult(true); - dispatch!({ - type: "updateAliases", - payload: { aliases: values.validators, keepOld: true }, - }); + updateAliases(values.validators); } catch (e) { console.log(e); setResult(false); diff --git a/components/topUpForm.tsx b/components/topUpForm.tsx index 7f25d843..91d8e824 100644 --- a/components/topUpForm.tsx +++ b/components/topUpForm.tsx @@ -288,13 +288,6 @@ function TopUp(props: { const storage = toStorage(version, cs, balance); if (!!state.contracts[props.address]) { - // dispatch({ - // type: "updateContract", - // payload: { - // address: props.address, - // contract: toStorage(version, cs, balance), - // }, - // }); addOrUpdateContract(props.address, toStorage(version, cs, balance)); } else { storage.address = props.address; diff --git a/context/aliases.tsx b/context/aliases.tsx index 8907edd4..7f1339c7 100644 --- a/context/aliases.tsx +++ b/context/aliases.tsx @@ -1,23 +1,85 @@ -import React, { useRef } from "react"; +import React, { useEffect, useReducer, useRef } from "react"; import { createContext } from "react"; +import { Aliases } from "../types/app"; +import { loadAliases, saveAliasesToStorage } from "../utils/localStorage"; import { TZKT_API_URL } from "./config"; import { useAppState } from "./state"; +import { useWallet } from "./wallet"; type AliasesContextType = { + addAlias(address: string, alias: string): void; + removeAlias(address: string): void; + updateAliases(aliases: Array<{ address: string; name: string }>): void; + addressBook: Aliases; // Aliases defined in Tzsafe getAlias(address: string, defaultAlias: string): Promise; }; // Map address with his alias. Alias can be from TZKT or Tzsafe or address by default +type AliasesActions = + | { + type: "LOAD_ALIASES"; + aliases: Aliases; + } + | { + type: "REMOVE_ALIAS"; + payload: { address: string }; + } + | { + type: "ADD_ALIAS"; + payload: { address: string; alias: string }; + } + | { + type: "UPDATE_ALIASES"; + aliases: Array<{ address: string; name: string }>; + }; + export const AliasesContext = createContext({ + addressBook: {}, getAlias: (address: string, defaultAlias: string) => Promise.resolve(address), + removeAlias: () => {}, + addAlias: () => {}, + updateAliases: () => {}, }); +const reducer = (state: Aliases, action: AliasesActions) => { + switch (action.type) { + case "ADD_ALIAS": + return { ...state, [action.payload.address]: action.payload.alias }; + case "REMOVE_ALIAS": + const { [action.payload.address]: deleted, ...others } = state; + return { ...state, ...others }; + case "LOAD_ALIASES": + return action.aliases; + case "UPDATE_ALIASES": + const newAliases = Object.fromEntries( + action.aliases.map(({ name, address }) => [address, name]) + ); + return { ...state, ...newAliases }; + default: + return state; + } +}; + export const AliasesProvider = ({ children, }: { children: React.ReactNode; }) => { + const { userAddress } = useWallet(); const aliases = useRef>>({}); + const [state, dispatch] = useReducer(reducer, {}); + + useEffect(() => { + const loadedAliases = userAddress ? loadAliases(userAddress) : {}; + dispatch({ type: "LOAD_ALIASES", aliases: loadedAliases ?? {} }); + }, [userAddress]); + + // Save state to storage when state is updated + useEffect(() => { + if (Object.keys(state).length !== 0) + saveAliasesToStorage(userAddress || "", state); + }, [state, userAddress]); + const aliasesFromState = useAppState().aliases; const getTzktAlias = async (address: string) => { @@ -58,6 +120,12 @@ export const AliasesProvider = ({ + dispatch({ type: "ADD_ALIAS", payload: { address, alias } }), + removeAlias: address => + dispatch({ type: "REMOVE_ALIAS", payload: { address } }), + updateAliases: aliases => dispatch({ type: "UPDATE_ALIASES", aliases }), }} > {children} diff --git a/context/state.tsx b/context/state.tsx index ac578d03..c2dfcd48 100644 --- a/context/state.tsx +++ b/context/state.tsx @@ -40,18 +40,6 @@ let emptyState = (): tezosState => { type action = | { type: "init"; payload: tezosState } - | { - type: "addContract"; - payload: { - aliases: { [address: string]: string }; - address: string; - contract: ContractStorage; - }; - } - | { - type: "updateContract"; - payload: { address: string; contract: ContractStorage }; - } | { type: "setCurrentStorage"; payload: ContractStorage & { address: string }; @@ -60,17 +48,9 @@ type action = type: "setCurrentContract"; payload: string; } - | { type: "removeContract"; address: string } | { type: "loadStorage"; payload: storage } | { type: "writeStorage"; payload: storage } | { type: "setDelegatorAddresses"; payload: string[] } - | { - type: "updateAliases"; - payload: { - aliases: { address: string; name: string }[]; - keepOld: boolean; - }; - } | { type: "setBanner"; payload: boolean; @@ -109,90 +89,6 @@ function reducer( { userAddress }: { userAddress: string } ): tezosState { switch (action.type) { - // case "p2pConnect": { - // return { ...state, p2pClient: action.payload }; - // } - // case "addDapp": { - // state.connectedDapps[action.payload.address] ??= {}; - - // state.connectedDapps[action.payload.address][action.payload.data.appUrl] = - // action.payload.data; - - // saveState(state, userAddress); - - // return state; - // } - // case "removeDapp": { - // if ( - // !state.currentContract || - // !state.connectedDapps[state.currentContract][action.payload] - // ) - // return state; - - // const newState = { ...state }; - - // delete newState.connectedDapps[state.currentContract][action.payload]; - - // saveState(newState, userAddress); - - // return newState; - // } - case "addContract": { - let al = action.payload.aliases; - let aliases = { ...state.aliases, ...al }; - - let contracts = { - ...state.contracts, - [action.payload.address]: action.payload.contract, - }; - - const newState: tezosState = { - ...state, - contracts: contracts, - aliases: aliases, - currentContract: state.currentContract, - aliasTrie: Trie.fromAliases(Object.entries(aliases)), - }; - - saveState(newState, userAddress); - - return newState; - } - case "updateAliases": { - const newAliases = Object.fromEntries( - action.payload.aliases.map(({ name, address }) => [address, name]) - ); - - const aliases = { - ...(action.payload.keepOld ? state.aliases : {}), - ...newAliases, - }; - - const newState = { - ...state, - aliases: aliases, - aliasTrie: Trie.fromAliases(Object.entries(aliases)), - }; - - saveState(newState, userAddress); - - return newState; - } - case "updateContract": { - let contracts = { - ...state.contracts, - [action.payload.address]: action.payload.contract, - }; - const newState = { - ...state, - contracts, - }; - - if (state.contracts[action.payload.address]) - saveState(newState, userAddress); - - return newState; - } case "setCurrentContract": const newState = { ...state, @@ -230,40 +126,6 @@ function reducer( }; } - case "removeContract": { - const { [action.address]: _, ...contracts } = state.contracts; - const { [action.address]: __, ...aliases } = state.aliases; - // TODO WHEN SPLIT CONTRACTS INTO CONTEXT - // const { [action.address]: contractDapps, ...connectedDapps } = - // state.connectedDapps; - - // Object.values(contractDapps ?? {}).forEach(async dapp => { - // const senderId = await getSenderId(dapp.publicKey); - // state.p2pClient?.removePeer( - // { - // ...dapp, - // type: "p2p-pairing-response", - // senderId, - // }, - // true - // ); - // }); - - const addresses = Object.keys(contracts); - const currentContract = addresses.length > 0 ? addresses[0] : null; - - const newState = { - ...state, - contracts, - currentContract, - aliases, - // connectedDapps, - }; - - saveState(newState, userAddress); - - return newState; - } case "setDelegatorAddresses": return { ...state, delegatorAddresses: action.payload }; case "refreshProposals": diff --git a/pages/[walletAddress]/fund-wallet.tsx b/pages/[walletAddress]/fund-wallet.tsx index cd1bb1d3..81c1f874 100644 --- a/pages/[walletAddress]/fund-wallet.tsx +++ b/pages/[walletAddress]/fund-wallet.tsx @@ -61,10 +61,7 @@ const TopUpPage = () => { newContract.balance = new BigNumber(newContract.balance) .plus(transaction[0].amount as number) .toString(); - // disptach({ - // type: "updateContract", - // payload: { contract: newContract, address: state.currentContract }, - // }); + addOrUpdateContract(state.currentContract, newContract); setIsLoading(false); diff --git a/pages/[walletAddress]/history.tsx b/pages/[walletAddress]/history.tsx index bcd379f6..08a48c1f 100644 --- a/pages/[walletAddress]/history.tsx +++ b/pages/[walletAddress]/history.tsx @@ -10,7 +10,7 @@ import Modal from "../../components/modal"; import ProposalSignForm from "../../components/proposalSignForm"; import { useContracts } from "../../context/contracts"; import { getTokenTransfers, getTransfers } from "../../context/proposals"; -import { useAppDispatch, useAppState } from "../../context/state"; +import { useAppState } from "../../context/state"; import { TezosToolkitContext } from "../../context/tezos-toolkit"; import fetchVersion from "../../context/version"; import { ContractStorage } from "../../types/app"; @@ -21,7 +21,6 @@ import { tokenTransfer, version, } from "../../types/display"; -import { mutezToTez } from "../../utils/tez"; import useWalletTokens from "../../utils/useWalletTokens"; import { getProposalsBigmapId, @@ -148,7 +147,6 @@ const getLatestTimestamp = (og: { const History = () => { const globalState = useAppState(); - const globalDispatch = useAppDispatch(); const { tezos } = useContext(TezosToolkitContext); const walletTokens = useWalletTokens(); @@ -217,15 +215,6 @@ const History = () => { if (globalState.contracts[globalState.currentContract]) addOrUpdateContract(globalState.currentContract, updatedContract); - // globalState.contracts[globalState.currentContract] - // ? globalDispatch({ - // type: "updateContract", - // payload: { - // address: globalState.currentContract, - // contract: updatedContract, - // }, - // }) - // : null; storage.version = version; diff --git a/pages/[walletAddress]/proposals.tsx b/pages/[walletAddress]/proposals.tsx index 8e0d2939..778dc154 100644 --- a/pages/[walletAddress]/proposals.tsx +++ b/pages/[walletAddress]/proposals.tsx @@ -180,14 +180,6 @@ async function getProposals( if (globalState.contracts[globalState.currentContract ?? ""]) { const balance = await tezos.tz.getBalance(globalState.currentContract); - // globalDispatch({ - // type: "updateContract", - // payload: { - // address: globalState.currentContract, - // contract: toStorage(version, storage, balance), - // }, - // }); - addOrUpdateContract( globalState.currentContract, toStorage(version, storage, balance) diff --git a/pages/address-book.tsx b/pages/address-book.tsx index bf2c053f..25371e7b 100644 --- a/pages/address-book.tsx +++ b/pages/address-book.tsx @@ -7,15 +7,10 @@ import { Field, FormikErrors, } from "formik"; -import { useContext } from "react"; import renderError from "../components/formUtils"; import Meta from "../components/meta"; -import { - AppDispatchContext, - AppStateContext, - useAppDispatch, - useAppState, -} from "../context/state"; +import { useAliases } from "../context/aliases"; +import { useAppState } from "../context/state"; function get( s: string | FormikErrors<{ name: string; address: string }> @@ -32,7 +27,7 @@ function get( } function Home() { const state = useAppState(); - const dispatch = useAppDispatch(); + const { updateAliases } = useAliases(); const byName = Object.fromEntries( Object.entries(state.aliases).map(([k, v]) => [v, k]) @@ -109,10 +104,7 @@ function Home() { return errors; }} onSubmit={values => { - dispatch!({ - type: "updateAliases", - payload: { aliases: values.validators, keepOld: false }, - }); + updateAliases(values.validators); }} > {({ values, errors, setTouched }) => { diff --git a/utils/localStorage.ts b/utils/localStorage.ts index a9eaafbe..f16d141a 100644 --- a/utils/localStorage.ts +++ b/utils/localStorage.ts @@ -56,3 +56,6 @@ export const loadDapps = (userAddress: string) => export const loadContracts = (userAddress: string) => loadStorage(userAddress).contracts; + +export const loadAliases = (userAddress: string) => + loadStorage(userAddress).aliases; From fb65244bf30ba08d43a3a9f0107603d688b539ca Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Fri, 26 Apr 2024 11:53:59 +0200 Subject: [PATCH 03/10] =?UTF-8?q?:fire:=20remove=20banner=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/Banner.tsx | 21 --------------------- components/Layout.tsx | 16 ++-------------- components/Sidebar.tsx | 4 +--- components/modal.tsx | 4 +--- components/navbar.tsx | 6 +++--- components/transferForm.tsx | 4 +--- context/state.tsx | 4 ---- 7 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 components/Banner.tsx diff --git a/components/Banner.tsx b/components/Banner.tsx deleted file mode 100644 index c1761edd..00000000 --- a/components/Banner.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Cross1Icon } from "@radix-ui/react-icons"; -import { useState } from "react"; - -type props = { - children: React.ReactNode; -}; - -const Banner = ({ children }: props) => { - const [hasBanner, setHasBanner] = useState(true); - - return hasBanner ? ( -
-
{children}
- -
- ) : null; -}; - -export default Banner; diff --git a/components/Layout.tsx b/components/Layout.tsx index c28ae6da..6abd8b06 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -10,7 +10,6 @@ import { useAppDispatch, useAppState } from "../context/state"; import { useTezosToolkit } from "../context/tezos-toolkit"; 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"; @@ -163,14 +162,7 @@ export default function Layout({ /> )} - - Make sure the URL is - {PREFERED_NETWORK === NetworkType.MAINNET - ? "tzsafe.marigold.dev" - : PREFERED_NETWORK === NetworkType.GHOSTNET - ? "ghostnet.tzsafe.marigold.dev" - : "a valid URL"} - + {isSidebarHidden ? null : ( @@ -181,11 +173,7 @@ export default function Layout({ /> )} -
+
-
- )} - -
- ); -}; - -export default Autocomplete; diff --git a/components/ExecuteContractForm.tsx b/components/ExecuteContractForm.tsx deleted file mode 100644 index e0b660b1..00000000 --- a/components/ExecuteContractForm.tsx +++ /dev/null @@ -1,129 +0,0 @@ -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 { 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/proposals.tsx b/components/proposals.tsx deleted file mode 100644 index 174d616d..00000000 --- a/components/proposals.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { FC, useState } from "react"; -import { tezosState, useAppState } from "../context/state"; -import { useWallet } from "../context/wallet"; -import { ContractStorage } from "../types/app"; -import { - mutezTransfer, - proposal, - proposalContent, - status, -} from "../types/display"; -import { adaptiveTime, countdown } from "../utils/adaptiveTime"; -import { mutezToTez } from "../utils/tez"; -import { signers } from "../versioned/apis"; -import ContractLoader from "./contractLoader"; - -function getClass(x: number, active: number): string { - return x == active - ? "inline-block p-4 w-full md:w-full text-left md:text-center break-normal rounded-t-lg border-b-2 text-md md:text-2xl uppercase border-primary text-white" - : "inline-block p-4 w-full md:w-full text-left md:text-center text-md md:text-2xl uppercase rounded-t-lg border-b-2 border-gray-100 hover:text-zinc-600 hover:border-primary text-white "; -} -const Proposals: FC<{ - proposals: [number, { og: any; ui: proposal }][]; - address: string; - contract: ContractStorage; - transfers: mutezTransfer[]; - setCloseModal: (_: number, arg: boolean | undefined) => void; -}> = ({ proposals, address, contract, setCloseModal, transfers }) => { - const { userAddress } = useWallet(); - let [currentTab, setCurrentTab] = useState(0); - let state = useAppState(); - - return ( -
-

Proposals

-
-
    -
  • - -
  • -
  • - -
  • -
-
-
-
    - {proposals && - proposals.length > 0 && - [ - ...proposals.filter( - ([_, proposal]) => "Proposing" === proposal.ui.status - ), - ] - .sort((a, b) => b[0] - a[0]) - .map(x => { - return ( - - setCloseModal(x[0], arg) - } - key={JSON.stringify(x[1])} - prop={x[1]} - address={address} - signable={ - !!userAddress && - !!!x[1].ui.signatures.find( - x => x.signer == userAddress - ) && - true - } - /> - ); - })} -
-
    - {proposals && - proposals.length > 0 && - [ - ...proposals.filter( - ([_, proposal]) => !("Proposing" === proposal.ui.status) - ), - ] - .concat( - transfers.map( - x => [-1, { ui: { timestamp: x.timestamp }, ...x }] as any - ) - ) - .sort( - (a, b) => - Number(Date.parse(b[1].ui.timestamp).toString(10)) - - Number(Date.parse(a[1].ui.timestamp).toString(10)) - ) - .map(x => { - return x[0] == -1 ? ( - - ) : ( - - ); - })} -
-
-
- ); -}; -const Transfer: FC<{ - prop: mutezTransfer; - address: string; -}> = ({ prop, address }) => { - let state = useAppState(); - - return ( -
  • -
    -

    - Transaction: received Tez{" "} -

    -
    -
    -

    Sender:

    -

    - {state.aliases[prop.sender.address] || prop.sender.address} -

    -
    - {prop.initiator && ( -
    -

    Initiator:

    -

    - {state.aliases[prop.initiator.address] || prop.initiator.address} -

    -
    - )} -
    -

    Target:

    -

    - {state.aliases[address] || address} -

    -
    -
    -

    Amount:

    -

    - {mutezToTez(prop.amount)} -

    -
    -
    -

    Timestamp:

    -

    - {prop.timestamp} -

    -
    -
  • - ); -}; -function getState(t: proposal): status { - return t.status; -} -const Card: FC<{ - prop: { og: any; ui: proposal }; - address: string; - id: number; - signable: boolean; - contract: ContractStorage; - setCloseModal?: (arg: boolean | undefined) => void; -}> = ({ contract, prop, address, id, signable, setCloseModal = () => {} }) => { - let state = useAppState(); - let [loading, setLoading] = useState(false); - const { userAddress } = useWallet(); - function resolvable( - signatures: { signer: string; result: boolean }[] - ): boolean { - let pro = - signatures.filter(x => x.result).length >= contract.threshold.toNumber(); - let against = - signatures.filter(x => !x.result).length > contract.threshold.toNumber(); - return pro || against; - } - return ( -
  • -
    -

    Status:

    -

    - {getState(prop.ui)} -

    -
    - {"effective_period" in contract && ( -
    -

    Expires in:

    -

    - {countdown(contract.effective_period, prop.ui.timestamp)} -

    -
    - )} -
    -

    Proposed by:

    -

    - {state.aliases[prop.ui.author] || prop.ui.author} -

    -
    - - {("Executed" === prop.ui.status || "Rejected" === prop.ui.status) && ( -
    -

    Signed By:

    -

    - [ {[...prop.ui.signatures.keys()].join(", ")} ] -

    -
    - )} - {"Proposing" === prop.ui.status && ( -
    -

    - Waiting for signatures from:{" "} -

    -

    - [ - {signers(contract) - .filter(x => !!!prop.ui.signatures.find(p => x === p.signer)) - .map(x => state.aliases[x] || x) - .join(", ")}{" "} - ] -

    -
    - )} -
    -

    Transactions:

    -

    - [ - {prop.ui.content - .map(x => `${renderContent(x, state, address, contract)}`) - .join(", ")}{" "} - ] -

    -
    -
    - - {userAddress && - signers(contract).includes(userAddress) && - signable && ( - - )} - {userAddress && - signers(contract).includes(userAddress) && - resolvable(prop.ui.signatures) && - "Executed" !== prop.ui.status && ( - - )} - {userAddress && - signers(contract).includes(userAddress) && - signable && ( - - )} - {userAddress && - signers(contract).includes(userAddress) && - !resolvable(prop.ui.signatures) && - !signable && - "Proposing" === prop.ui.status && ( -

    - Waiting for signatures of other owners -

    - )} -
    -
    -
  • - ); -}; - -function renderContent( - x: proposalContent, - state: tezosState, - address: string, - contract: ContractStorage -): string { - if ("transfer" in x) { - return `${mutezToTez(x.transfer.amount)} Tez to ${ - state.aliases[x.transfer.destination] || x.transfer.destination - }`; - } - if ("executeLambda" in x) { - return `Execute Lambda(${x.executeLambda.metadata})`; - } - if ("execute" in x) { - return `Execute (${x.execute})`; - } - if ("adjustEffectivePeriod" in x) { - return `Adjust effective period: (${adaptiveTime( - x.adjustEffectivePeriod.toString() - )})`; - } - if ("addOwners" in x) { - return `Add [${x.addOwners.join(", ")}] to validators`; - } - if ("removeOwners" in x) { - return `Remove [${x.removeOwners.join(", ")}] from validators`; - } - if ("changeThreshold" in x) { - return `Change threshold from ${contract.threshold} to ${x.changeThreshold}`; - } - if ("add_or_update_metadata" in x) { - return `Updata metadata`; - } - return "Not supported"; -} -export default Proposals; diff --git a/package-lock.json b/package-lock.json index bf7468de..7e98dd11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.3", - "@stablelib/blake2b": "^1.0.1", "@taquito/beacon-wallet": "^19.2.0", "@taquito/rpc": "^19.2.0", "@taquito/taquito": "^19.2.0", @@ -27,7 +26,6 @@ "@types/uuid": "^9.0.1", "@wert-io/widget-initializer": "^4.0.1", "assert-never": "^1.2.1", - "axios": "^1.6.5", "bignumber.js": "^9.1.1", "bs58check": "^3", "chart.js": "^4.4.2", diff --git a/package.json b/package.json index 9e5068e0..6812d5bd 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.3", - "@stablelib/blake2b": "^1.0.1", "@taquito/beacon-wallet": "^19.2.0", "@taquito/rpc": "^19.2.0", "@taquito/taquito": "^19.2.0", @@ -39,7 +38,6 @@ "@types/uuid": "^9.0.1", "@wert-io/widget-initializer": "^4.0.1", "assert-never": "^1.2.1", - "axios": "^1.6.5", "bignumber.js": "^9.1.1", "bs58check": "^3", "chart.js": "^4.4.2", diff --git a/utils/adaptiveTime.ts b/utils/adaptiveTime.ts index 83924cde..c846671f 100644 --- a/utils/adaptiveTime.ts +++ b/utils/adaptiveTime.ts @@ -70,15 +70,6 @@ export function adaptiveTime(x: string): string { return duration.normalize().toHuman(); } -export function countdown(x: string, createdOn: string): string { - let created = DateTime.fromISO(new Date(createdOn).toISOString()).toObject(); - let duration = Duration.fromMillis(Number(x) * 1000) - .plus(Duration.fromObject(created)) - .minus(Duration.fromObject(DateTime.now().toObject())) - .rescale(); - return conv(duration).toHuman(); -} - export const parseIntOr = ( value: string | undefined, defaultValue: T From 385cfc83625e58b0ab61850d7f327f6be82c46a5 Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Tue, 30 Apr 2024 11:14:18 +0200 Subject: [PATCH 06/10] :bug: fix error with update aliases --- components/create/createLoader.tsx | 10 ++++------ components/import/importLoader.tsx | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/components/create/createLoader.tsx b/components/create/createLoader.tsx index 231f82b7..557392d4 100644 --- a/components/create/createLoader.tsx +++ b/components/create/createLoader.tsx @@ -57,12 +57,10 @@ function Success() { tzsafe!.address, toStorage(formState.version, c, balance!) ); - updateAliases( - Object.fromEntries([ - ...formState.validators.map(x => [x.address, x.name]), - [tzsafe?.address!, formState?.walletName || ""], - ]) - ); + updateAliases([ + ...formState!.validators, + { address: address.address, name: formState?.walletName || "" }, + ]); } catch (err) { console.log(err); setAddress({ status: -1, address: "" }); diff --git a/components/import/importLoader.tsx b/components/import/importLoader.tsx index 90918092..03e4a81a 100644 --- a/components/import/importLoader.tsx +++ b/components/import/importLoader.tsx @@ -36,12 +36,10 @@ function Success() { setAddress({ address: address.address, status: 1 }); setLoading(false); addOrUpdateContract(address.address, v); - updateAliases( - Object.fromEntries([ - ...formState!.validators.map(x => [x.address, x.name]), - [address.address!, formState?.walletName || ""], - ]) - ); + updateAliases([ + ...formState!.validators, + { address: address.address, name: formState?.walletName || "" }, + ]); } catch (err) { console.log(err); setAddress({ status: -1, address: "" }); From 177ec7045a7ede815861af4a856dee97a273b14b Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Tue, 30 Apr 2024 16:17:05 +0200 Subject: [PATCH 07/10] :recycle: use contracts context instead of state --- components/ContractExecution.tsx | 6 +- components/FA1_2.tsx | 7 +- components/FA2Transfer.tsx | 8 +- components/Layout.tsx | 105 ++++----------------- components/LoginModal.tsx | 16 ++-- components/PoeModal.tsx | 101 +++++++++++--------- components/ProposalCard.tsx | 20 ++-- components/Sidebar.tsx | 126 +++++++++++-------------- components/import/basic.tsx | 4 +- components/navbar.tsx | 9 +- components/proposalSignForm.tsx | 19 ++-- components/signersForm.tsx | 10 +- components/topUpForm.tsx | 39 +++----- components/transferForm.tsx | 13 ++- hooks/useCurrentContract.ts | 18 ++++ pages/[walletAddress]/beacon.tsx | 15 ++- pages/[walletAddress]/dashboard.tsx | 5 +- pages/[walletAddress]/fund-wallet.tsx | 38 ++++---- pages/[walletAddress]/new-proposal.tsx | 15 ++- pages/[walletAddress]/settings.tsx | 27 ++---- utils/useIsOwner.ts | 10 +- utils/useWalletTokens.ts | 13 ++- 22 files changed, 275 insertions(+), 349 deletions(-) create mode 100644 hooks/useCurrentContract.ts diff --git a/components/ContractExecution.tsx b/components/ContractExecution.tsx index ff66f090..fbbfcc21 100644 --- a/components/ContractExecution.tsx +++ b/components/ContractExecution.tsx @@ -8,8 +8,10 @@ import { useFormikContext, } from "formik"; import React, { useEffect } from "react"; +import { useContracts } from "../context/contracts"; import { useAppState } from "../context/state"; import { useTezosToolkit } from "../context/tezos-toolkit"; +import useCurrentContract from "../hooks/useCurrentContract"; import { parseContract, genLambda, @@ -489,6 +491,8 @@ function ExecuteForm( const state = useAppState(); const { tezos } = useTezosToolkit(); + const { contracts } = useContracts(); + const currentContract = useCurrentContract(); const address = props.address; const setLoading = props.setLoading; @@ -528,7 +532,7 @@ function ExecuteForm( props.onShapeChange(values); try { genLambda( - state.contracts[state.currentContract ?? ""]?.version ?? + contracts[currentContract]?.version ?? state.currentStorage?.version, props, values diff --git a/components/FA1_2.tsx b/components/FA1_2.tsx index db73da2d..cb3ce71e 100644 --- a/components/FA1_2.tsx +++ b/components/FA1_2.tsx @@ -2,6 +2,7 @@ import { Field, useFormikContext } from "formik"; import { useCallback, useEffect, useRef, useState } from "react"; import { TZKT_API_URL, THUMBNAIL_URL } from "../context/config"; import { useAppState } from "../context/state"; +import useCurrentContract from "../hooks/useCurrentContract"; import { debounce, promiseWithTimeout } from "../utils/timeout"; import { proposals } from "../versioned/interface"; import ErrorMessage from "./ErrorMessage"; @@ -77,7 +78,7 @@ const tokenToOption = (fa1_2Token: fa1_2Token) => { const FA1_2 = ({ index, remove, children }: props) => { const state = useAppState(); const { setFieldValue, getFieldProps } = useFormikContext(); - + const currentContract = useCurrentContract(); const [isFetching, setIsFetching] = useState(true); const [canSeeMore, setCanSeeMore] = useState(true); const [selectError, setSelectError] = useState(); @@ -103,7 +104,7 @@ const FA1_2 = ({ index, remove, children }: props) => { (value: string, offset: number) => promiseWithTimeout( fetch( - `${TZKT_API_URL}/v1/tokens/balances?account=${state.currentContract}&offset=${offset}&limit=${FETCH_COUNT}&token.metadata.name.as=*${value}*&balance.ne=0&sort.desc=lastTime&token.standard.eq=fa1.2` + `${TZKT_API_URL}/v1/tokens/balances?account=${currentContract}&offset=${offset}&limit=${FETCH_COUNT}&token.metadata.name.as=*${value}*&balance.ne=0&sort.desc=lastTime&token.standard.eq=fa1.2` ) .catch(e => { console.log(e); @@ -126,7 +127,7 @@ const FA1_2 = ({ index, remove, children }: props) => { return Promise.resolve(v); }), - [state.currentContract] + [currentContract] ); useEffect(() => { diff --git a/components/FA2Transfer.tsx b/components/FA2Transfer.tsx index 4fcc1bd7..95b4a32c 100644 --- a/components/FA2Transfer.tsx +++ b/components/FA2Transfer.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { v4 as uuidV4 } from "uuid"; import { TZKT_API_URL, THUMBNAIL_URL } from "../context/config"; import { useAppState } from "../context/state"; +import useCurrentContract from "../hooks/useCurrentContract"; import { debounce } from "../utils/timeout"; import { proposals } from "../versioned/interface"; import ErrorMessage from "./ErrorMessage"; @@ -94,6 +95,7 @@ const FA2Transfer = ({ const [currentToken, setCurrentToken] = useState(); const [options, setOptions] = useState([]); const fetchOffsetRef = useRef(0); + const currentContract = useCurrentContract(); const makeName = (key: string) => `transfers.${proposalIndex}.values.${localIndex}.${key}`; @@ -126,9 +128,7 @@ const FA2Transfer = ({ const fetchTokens = useCallback( (value: string, offset: number) => fetch( - `${TZKT_API_URL}/v1/tokens/balances?account=${ - state.currentContract - }&offset=${offset}&limit=${FETCH_COUNT}&token.metadata.name.as=*${value}*&balance.ne=0&sort.desc=lastTime&token.standard.eq=fa2${ + `${TZKT_API_URL}/v1/tokens/balances?account=${currentContract}&offset=${offset}&limit=${FETCH_COUNT}&token.metadata.name.as=*${value}*&balance.ne=0&sort.desc=lastTime&token.standard.eq=fa2${ !!fa2ContractAddress ? "&token.contract=" + fa2ContractAddress : "" }` ) @@ -148,7 +148,7 @@ const FA2Transfer = ({ v.filter(token => !toExclude.includes(token.id)) ); }), - [state.currentContract, toExclude] + [currentContract, toExclude] ); useEffect(() => { diff --git a/components/Layout.tsx b/components/Layout.tsx index 6abd8b06..d0efa657 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -1,15 +1,13 @@ -import { 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 { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { PREFERED_NETWORK } from "../context/config"; -import { useAppDispatch, useAppState } from "../context/state"; +import { useContracts } from "../context/contracts"; +import { useAppState } from "../context/state"; import { useTezosToolkit } from "../context/tezos-toolkit"; -import { contractStorage } from "../types/Proposal0_3_1"; -import { fetchContract } from "../utils/fetchContract"; +import useCurrentContract from "../hooks/useCurrentContract"; import LoginModal from "./LoginModal"; import PoeModal from "./PoeModal"; import Sidebar from "./Sidebar"; @@ -22,16 +20,17 @@ export default function Layout({ pageProps, }: Pick) { const state = useAppState(); - const dispatch = useAppDispatch(); const { tezos } = useTezosToolkit(); const [data, setData] = useState(); const [hasSidebar, setHasSidebar] = useState(false); - const [isFetching, setIsFetching] = useState(true); + const [isFetching, setIsFetching] = useState(false); const router = useRouter(); const path = usePathname(); + const { contracts, fetchContract } = useContracts(); + const currentContract = useCurrentContract(); const isSidebarHidden = - Object.values(state.contracts).length === 0 && + Object.values(contracts).length === 0 && (path === "/" || path === "/new-wallet" || path === "/import-wallet" || @@ -48,10 +47,10 @@ export default function Layout({ setData(queryParams.get("data")!); } - const contracts = Object.keys(state.contracts); + const contractsAddress = Object.keys(contracts); - if ((path === "/" || path === "") && contracts.length > 0) { - const contract = contracts[0]; + if ((path === "/" || path === "") && contractsAddress.length > 0) { + const contract = contractsAddress[0]; router.replace(`/${contract}/dashboard`); return; @@ -61,90 +60,24 @@ export default function Layout({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.currentContract, path, state.contracts]); + }, [currentContract, path, 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) - ) { + if (validateAddress(currentContract) !== ValidationResult.VALID) { setIsFetching(false); + router.replace(`/invalid-contract?address=${currentContract}`); return; } - - if (!!state.contracts[router.query.walletAddress]) { - dispatch({ - type: "setCurrentContract", - payload: router.query.walletAddress, - }); - setIsFetching(false); - } - - 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(true); + fetchContract(currentContract) + .then(() => setIsFetching(false)) + .catch(() => { 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, + router.replace(`/invalid-contract?address=${currentContract}`); }); - - 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, - ]); + }, [currentContract, state.currentStorage, tezos]); useEffect(() => { setHasSidebar(false); diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx index 4e375b19..4dbb1d84 100644 --- a/components/LoginModal.tsx +++ b/components/LoginModal.tsx @@ -1,8 +1,8 @@ import { useEffect, useMemo, useState } from "react"; import { Event } from "../context/P2PClient"; import { useAliases } from "../context/aliases"; +import { useContracts } from "../context/contracts"; import { useDapps, useP2PClient } from "../context/dapps"; -import { useAppState } from "../context/state"; import { useWallet } from "../context/wallet"; import { decodeData } from "../pages/[walletAddress]/beacon"; import { P2pData } from "../types/app"; @@ -21,7 +21,6 @@ enum State { } const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { - const state = useAppState(); const p2pClient = useP2PClient(); const { addDapp } = useDapps(); @@ -30,14 +29,15 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { const { userAddress, wallet, connectWallet } = useWallet(); const { addressBook } = useAliases(); + const { contracts } = useContracts(); const options = useMemo(() => { if (!userAddress) return []; - return Object.keys(state.contracts).flatMap(address => { - if (!hasTzip27Support(state.contracts[address].version)) return []; + return Object.keys(contracts).flatMap(address => { + if (!hasTzip27Support(contracts[address].version)) return []; - if (!signers(state.contracts[address]).includes(userAddress!)) return []; + if (!signers(contracts[address]).includes(userAddress!)) return []; return [ { @@ -47,7 +47,7 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { }, ]; }); - }, [state.contracts, userAddress]); + }, [contracts, userAddress, addressBook]); const [selectedWallet, setSelectedWallet] = useState< { id: string; value: string; label: string } | undefined @@ -80,13 +80,13 @@ const LoginModal = ({ data, onEnd }: { data: string; onEnd: () => void }) => { setError((e as Error).message); setCurrentState(State.ERROR); } - }, [data, p2pClient]); + }, [data, p2pClient, userAddress]); useEffect(() => { if (currentState === State.LOGIN && !!userAddress) { setCurrentState(State.INITIAL); } - }, [userAddress]); + }, [userAddress, currentState]); return (
    diff --git a/components/PoeModal.tsx b/components/PoeModal.tsx index 48493eb0..6b79957f 100644 --- a/components/PoeModal.tsx +++ b/components/PoeModal.tsx @@ -21,6 +21,7 @@ import { ChangeEvent, useEffect, useMemo, useState } from "react"; import { Event } from "../context/P2PClient"; import { useAliases } from "../context/aliases"; import { PREFERED_NETWORK } from "../context/config"; +import { useContracts } from "../context/contracts"; import { useP2PClient } from "../context/dapps"; import { generateDelegateMichelson, @@ -31,6 +32,7 @@ import { useTezosToolkit } from "../context/tezos-toolkit"; import fetchVersion from "../context/version"; import { useWallet } from "../context/wallet"; import { CustomView, customViewMatchers } from "../dapps"; +import useCurrentContract from "../hooks/useCurrentContract"; import { State } from "../pages/[walletAddress]/beacon"; import { proposalContent } from "../types/display"; import useWalletTokens from "../utils/useWalletTokens"; @@ -82,6 +84,7 @@ const PoeModal = () => { const { tezos } = useTezosToolkit(); const p2pClient = useP2PClient(); const { addressBook } = useAliases(); + const { contracts } = useContracts(); const path = usePathname(); @@ -104,9 +107,10 @@ const PoeModal = () => { const [signImmediatelyFlag, setSignImmediatelyFlag] = useState(true); const [resolveImmediatelyFlag, setResolveImmediatelyFlag] = useState(false); + const currentContract = useCurrentContract(); const version = - state.contracts[address ?? ""]?.version ?? state.currentStorage?.version; + contracts[address ?? ""]?.version ?? state.currentStorage?.version; const { rows, dapp } = useMemo(() => { const rows = (transfers ?? []).map(t => @@ -133,7 +137,7 @@ const PoeModal = () => { }; const transactionCb = async (message: OperationRequestOutput) => { - if (!state.contracts[message.sourceAddress]) { + if (!contracts[message.sourceAddress]) { p2pClient.abortRequest( message.id, "The contract is not an imported TzSafe one" @@ -158,7 +162,7 @@ const PoeModal = () => { return; } - const version = state.contracts[message.sourceAddress].version; + const version = contracts[message.sourceAddress].version; const transfers = (await Promise.all( message.operationDetails.map(async detail => { @@ -354,7 +358,7 @@ const PoeModal = () => { const simulatedProofOfEventCb = async ( message: SimulatedProofOfEventChallengeRequest ) => { - const contract = state.contracts[message.contractAddress]; + const contract = contracts[message.contractAddress]; if (!contract) { p2pClient.sendError( @@ -415,44 +419,49 @@ const PoeModal = () => { setAddress(undefined); }; - const Checkboxes = () => ( - <> -
    - - ) => { - if (!e.target.checked) { - setResolveImmediatelyFlag(e.target.checked); - } - setSignImmediatelyFlag(e.target.checked); - }} - className="h-4 w-4 rounded-md p-2" - /> -
    - - {state.currentContract && - state.contracts[state.currentContract]?.threshold.toNumber() <= 1 && ( -
    - - ) => { + const Checkboxes = () => { + const currentContract = useCurrentContract(); + + return ( + <> +
    + + ) => { + if (!e.target.checked) { setResolveImmediatelyFlag(e.target.checked); - }} - name="resolveImmediatelyFlag" - type="checkbox" - className="h-4 w-4 rounded-md p-2" - /> -
    - )} - - ); + } + setSignImmediatelyFlag(e.target.checked); + }} + className="h-4 w-4 rounded-md p-2" + /> +
    + + {/* TODO: get currentContract from outside state and Router */} + {currentContract && + contracts[currentContract]?.threshold.toNumber() <= 1 && ( +
    + + ) => { + setResolveImmediatelyFlag(e.target.checked); + }} + name="resolveImmediatelyFlag" + type="checkbox" + className="h-4 w-4 rounded-md p-2" + /> +
    + )} + + ); + }; return (
    @@ -555,10 +564,10 @@ const PoeModal = () => {

    will create the proposal

    - {state.currentContract !== address && ( + {currentContract !== address && (

    The signing wallet is different from{" "} - +

    )} {!!dapp?.logo && ( @@ -702,7 +711,7 @@ const PoeModal = () => { try { const cc = await tezos.wallet.at(address); const versioned = VersionedApi( - state.contracts[address].version, + contracts[address].version, address ); const submitTimeoutAndHash = @@ -727,7 +736,7 @@ const PoeModal = () => { if ( path?.endsWith("proposals") && - address === state.currentContract + address === currentContract ) { dispatch({ type: "refreshProposals" }); } @@ -813,7 +822,7 @@ const PoeModal = () => { const cc = await tezos.wallet.at(address); const versioned = VersionedApi( - state.contracts[address].version, + contracts[address].version, address ); diff --git a/components/ProposalCard.tsx b/components/ProposalCard.tsx index de515933..bc8be9c8 100644 --- a/components/ProposalCard.tsx +++ b/components/ProposalCard.tsx @@ -1,9 +1,11 @@ import { InfoCircledIcon, TriangleDownIcon } from "@radix-ui/react-icons"; import * as Switch from "@radix-ui/react-switch"; import { useState, useMemo } from "react"; +import { useContracts } from "../context/contracts"; import { useAppState } from "../context/state"; import { useTezosToolkit } from "../context/tezos-toolkit"; import { CustomView, customViewMatchers } from "../dapps"; +import useCurrentContract from "../hooks/useCurrentContract"; import { proposalContent } from "../types/display"; import { walletToken } from "../utils/useWalletTokens"; import { signers } from "../versioned/apis"; @@ -48,7 +50,8 @@ const ProposalCard = ({ }: ProposalCardProps) => { const state = useAppState(); const { tezos } = useTezosToolkit(); - const currentContract = state.currentContract ?? ""; + const { contracts } = useContracts(); + const currentContract = useCurrentContract(); const [isOpen, setIsOpen] = useState(false); const [hasDefaultView, setHasDefaultView] = useState(false); @@ -57,14 +60,13 @@ const ProposalCard = ({ const resolveDate = new Date(resolver?.timestamp ?? 0); const allSigners = signers( - state.contracts[currentContract] ?? state.currentStorage + contracts[currentContract] ?? state.currentStorage ); const { rows, dapp } = useMemo(() => { const rows = content.map(v => contentToData( - state.contracts[state.currentContract ?? ""]?.version ?? - state.currentStorage?.version, + contracts[currentContract]?.version ?? state.currentStorage?.version, v, walletTokens ) @@ -85,7 +87,7 @@ const ProposalCard = ({ } return { rows, dapp }; - }, [content, state.currentContract, state.currentStorage, state.contracts]); + }, [content, currentContract, state.currentStorage, contracts]); return (
    { setIsClient(true); }, []); useEffect(() => { - if (!state.currentContract) return; - (async () => { - if (!state.currentContract) return; - - 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] - ? Promise.resolve( - state.contracts[state.currentContract].version - ) - : fetchVersion(c)); - - const updatedContract = toStorage(version, storage, balance); - - if (state.contracts[state.currentContract]) - addOrUpdateContract(state.currentContract, updatedContract); + if (validateAddress(currentContract) !== ValidationResult.VALID) + fetchContract(currentContract) + .then(updatedContract => { + addOrUpdateContract(currentContract, updatedContract); + }) + .catch(console.error); })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.currentContract]); + }, [currentContract]); if (!isClient) return null; - const currentContract = state.currentContract ?? ""; - return (
    - {state && state.contracts && ( + {contracts && (
    {!userAddress ? (
    diff --git a/components/proposalSignForm.tsx b/components/proposalSignForm.tsx index d1ce887b..71e436d0 100644 --- a/components/proposalSignForm.tsx +++ b/components/proposalSignForm.tsx @@ -7,10 +7,12 @@ import { useRouter } from "next/router"; import React, { useContext, useState, useMemo } from "react"; import { MODAL_TIMEOUT, PREFERED_NETWORK } from "../context/config"; import { PROPOSAL_DURATION_WARNING } from "../context/config"; -import { AppStateContext, useAppState } from "../context/state"; +import { useContracts } from "../context/contracts"; +import { useAppState } from "../context/state"; import { TezosToolkitContext } from "../context/tezos-toolkit"; import { useWallet } from "../context/wallet"; import { CustomView, customViewMatchers } from "../dapps"; +import useCurrentContract from "../hooks/useCurrentContract"; import { version, proposal } from "../types/display"; import { canExecute, canReject } from "../utils/proposals"; import { walletToken } from "../utils/useWalletTokens"; @@ -46,7 +48,8 @@ function ProposalSignForm({ onSuccess?: () => void; }) { const state = useAppState(); - const currentContract = state.currentContract ?? ""; + const { contracts } = useContracts(); + const currentContract = useCurrentContract(); const { userAddress } = useWallet(); @@ -62,8 +65,7 @@ function ProposalSignForm({ const { rows, dapp } = useMemo(() => { const rows = proposal.ui.content.map(v => contentToData( - state.contracts[currentContract]?.version ?? - state.currentStorage?.version, + contracts[currentContract]?.version ?? state.currentStorage?.version, v, walletTokens ) @@ -80,12 +82,7 @@ function ProposalSignForm({ console.log("Failed to parse dapp:", e); } return { rows, dapp }; - }, [ - proposal.ui.content, - state.currentContract, - state.contracts, - state.currentStorage, - ]); + }, [proposal.ui.content, currentContract, contracts, state.currentStorage]); async function sign( proposal: number, @@ -195,7 +192,7 @@ function ProposalSignForm({ } const allSigners = signers( - state.contracts[currentContract] ?? state.currentStorage + contracts[currentContract] ?? state.currentStorage ); const signatures = proposal.ui.signatures.filter(({ signer }) => diff --git a/components/signersForm.tsx b/components/signersForm.tsx index 45039fbe..b5506eff 100644 --- a/components/signersForm.tsx +++ b/components/signersForm.tsx @@ -18,6 +18,7 @@ import { PROPOSAL_DURATION_WARNING, } from "../context/config"; import { TZKT_API_URL } from "../context/config"; +import { useContracts } from "../context/contracts"; import { generateDelegateMichelson, generateUndelegateMichelson, @@ -25,6 +26,7 @@ import { import { useAppDispatch, useAppState } from "../context/state"; import { TezosToolkitContext } from "../context/tezos-toolkit"; import { useWallet } from "../context/wallet"; +import useCurrentContract from "../hooks/useCurrentContract"; import { ContractStorage } from "../types/app"; import { durationOfDaysHoursMinutes, @@ -95,6 +97,8 @@ const SignersForm: FC<{ const { tezos } = useContext(TezosToolkitContext); const router = useRouter(); const bakerAddressRef = useRef(null); + const { contracts } = useContracts(); + const currentContract = useCurrentContract(); const [loading, setLoading] = useState(false); const [timeoutAndHash, setTimeoutAndHash] = useState([false, ""]); @@ -168,7 +172,7 @@ const SignersForm: FC<{ if (!props.contract) return []; const version = - state.contracts[state.currentContract ?? ""]?.version ?? + contracts[currentContract ?? ""]?.version ?? state.currentStorage?.version; const initialSigners = new Set( @@ -278,7 +282,7 @@ const SignersForm: FC<{
    - {state.currentContract && - state.contracts[ - state.currentContract + {currentContract && + contracts[ + currentContract ]?.threshold.toNumber() <= 1 && (
    - {!state.currentContract ? ( + {!currentContract ? (

    Please select a wallet in the sidebar

    @@ -179,10 +180,7 @@ const TopUpPage = () => {

    Send from

    - {}} - /> + {}} /> )}
    diff --git a/pages/[walletAddress]/new-proposal.tsx b/pages/[walletAddress]/new-proposal.tsx index 0f961e33..ee9a233b 100644 --- a/pages/[walletAddress]/new-proposal.tsx +++ b/pages/[walletAddress]/new-proposal.tsx @@ -2,8 +2,9 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; import Meta from "../../components/meta"; import TransferForm from "../../components/transferForm"; +import { useContracts } from "../../context/contracts"; import { useAppState } from "../../context/state"; -import { ParsedUrlQueryContract } from "../../types/app"; +import useCurrentContract from "../../hooks/useCurrentContract"; import useIsOwner from "../../utils/useIsOwner"; const CreateProposal = () => { @@ -12,12 +13,12 @@ const CreateProposal = () => { const router = useRouter(); const isOwner = useIsOwner(); - const { walletAddress: currentContract } = - router.query as ParsedUrlQueryContract; + const { contracts } = useContracts(); + const currentContract = useCurrentContract(); useEffect(() => { - if (!isOwner) router.replace(`/${router.query.walletAddress}/proposals`); - }, [isOwner, router]); + if (!isOwner) router.replace(`/${currentContract}/proposals`); + }, [isOwner, router, currentContract]); return (
    @@ -36,9 +37,7 @@ const CreateProposal = () => { ) : ( )} diff --git a/pages/[walletAddress]/settings.tsx b/pages/[walletAddress]/settings.tsx index 9237f450..ebde3d58 100644 --- a/pages/[walletAddress]/settings.tsx +++ b/pages/[walletAddress]/settings.tsx @@ -5,26 +5,24 @@ import SignersForm from "../../components/signersForm"; import { useContracts } from "../../context/contracts"; import { useDapps } from "../../context/dapps"; import { useAppState } from "../../context/state"; -import { ParsedUrlQueryContract } from "../../types/app"; +import useCurrentContract from "../../hooks/useCurrentContract"; import useIsOwner from "../../utils/useIsOwner"; const Settings = () => { const state = useAppState(); const router = useRouter(); const isOwner = useIsOwner(); - const { walletAddress: currentContract } = - router.query as ParsedUrlQueryContract; - const { removeContract } = useContracts(); + + const { removeContract, contracts } = useContracts(); const { removeContractDapps } = useDapps(); + const currentContract = useCurrentContract(); - const [canDelete, setCanDelete] = useState( - !!state.contracts[currentContract] - ); + const [canDelete, setCanDelete] = useState(!!contracts[currentContract]); const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { - setCanDelete(!!state.contracts[currentContract]); - }, [currentContract, state.contracts]); + setCanDelete(!!contracts[currentContract]); + }, [currentContract, contracts]); useEffect(() => { if (!isDeleting) return; @@ -51,14 +49,11 @@ const Settings = () => { onClick={() => { setIsDeleting(true); setCanDelete(false); - // dispatch!({ - // type: "removeContract", - // address: currentContract, - // }); + removeContract(currentContract); removeContractDapps(currentContract); - const addresses = Object.keys(state.contracts); + const addresses = Object.keys(contracts); if (addresses.length === 0) { router.replace(`/`); } else { @@ -80,9 +75,7 @@ const Settings = () => { )} diff --git a/utils/useIsOwner.ts b/utils/useIsOwner.ts index 333d801b..c9960196 100644 --- a/utils/useIsOwner.ts +++ b/utils/useIsOwner.ts @@ -1,21 +1,23 @@ import { useMemo } from "react"; +import { useContracts } from "../context/contracts"; import { useAppState } from "../context/state"; import { useWallet } from "../context/wallet"; +import useCurrentContract from "../hooks/useCurrentContract"; import { signers } from "../versioned/apis"; const useIsOwner = () => { let state = useAppState(); const { userAddress } = useWallet(); + const { contracts } = useContracts(); + const currentContract = useCurrentContract(); const isOwner = useMemo( () => !!userAddress && - (state.contracts[state.currentContract ?? ""]?.owners?.includes( - userAddress - ) ?? + (contracts[currentContract]?.owners?.includes(userAddress) ?? (!!state.currentStorage && signers(state.currentStorage).includes(userAddress!))), - [state.currentContract, userAddress, state.contracts, state.currentStorage] + [currentContract, userAddress, contracts, state.currentStorage] ); return isOwner; diff --git a/utils/useWalletTokens.ts b/utils/useWalletTokens.ts index 231b5ffe..d23b08c8 100644 --- a/utils/useWalletTokens.ts +++ b/utils/useWalletTokens.ts @@ -2,21 +2,20 @@ 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 { useAppState } from "../context/state"; +import useCurrentContract from "../hooks/useCurrentContract"; export type walletToken = fa1_2Token | fa2Token; const useWalletTokens = () => { - const state = useAppState(); + const currentContract = useCurrentContract(); const [tokens, setTokens] = useState(); useEffect(() => { - if (!state.currentContract) return; - - fetch(`${TZKT_API_URL}/v1/tokens/balances?account=${state.currentContract}`) + fetch(`${TZKT_API_URL}/v1/tokens/balances?account=${currentContract}`) .then(res => res.json()) - .then(setTokens); - }, [state.currentContract]); + .then(setTokens) + .catch(err => console.error(`Cannot fetch tokens balances : ${err}`)); + }, [currentContract]); return tokens; }; From 06c3b79758c2feddf23226a1fe6be9446c586fe0 Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Tue, 30 Apr 2024 16:21:44 +0200 Subject: [PATCH 08/10] :alembic: --- components/Alias.tsx | 2 +- types/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Alias.tsx b/components/Alias.tsx index 7842ab4d..1a2eb60d 100644 --- a/components/Alias.tsx +++ b/components/Alias.tsx @@ -22,7 +22,7 @@ const Alias = ({ useEffect(() => { aliasesCtx.getAlias(address, defaultAlias).then(setAlias); - }, [address, aliasesCtx]); + }, [address, aliasesCtx, defaultAlias]); return ( diff --git a/types/app.ts b/types/app.ts index 00515343..f0e28c04 100644 --- a/types/app.ts +++ b/types/app.ts @@ -28,5 +28,5 @@ export type Contracts = { [address: string]: ContractStorage }; export type Aliases = { [address: string]: string }; export interface ParsedUrlQueryContract extends ParsedUrlQuery { - walletAddress: string; + walletAddress: string | undefined; } From c2c9c350e6c935adbb7feaf4a052e3bbd632dc1f Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Tue, 30 Apr 2024 16:22:33 +0200 Subject: [PATCH 09/10] :goal_net: catch errors when fetching tzkt API --- context/aliases.tsx | 34 +++++------ utils/tzktHooks.ts | 145 ++++++++++++++++++++++++-------------------- 2 files changed, 96 insertions(+), 83 deletions(-) diff --git a/context/aliases.tsx b/context/aliases.tsx index 7f1339c7..a8d8bee4 100644 --- a/context/aliases.tsx +++ b/context/aliases.tsx @@ -3,7 +3,6 @@ import { createContext } from "react"; import { Aliases } from "../types/app"; import { loadAliases, saveAliasesToStorage } from "../utils/localStorage"; import { TZKT_API_URL } from "./config"; -import { useAppState } from "./state"; import { useWallet } from "./wallet"; type AliasesContextType = { @@ -80,29 +79,30 @@ export const AliasesProvider = ({ saveAliasesToStorage(userAddress || "", state); }, [state, userAddress]); - const aliasesFromState = useAppState().aliases; - const getTzktAlias = async (address: string) => { // address can be empty string... if (address === "") return undefined; const alias = aliases.current[address]; if (!alias) { - const aliasFromTzkt = fetch( - `${TZKT_API_URL}/v1/accounts/${address}` - ).then(async response => { - if (response.status >= 200 && response.status < 300) { - if (response.status === 204) { - // Meaning no content = no alias + const aliasFromTzkt = fetch(`${TZKT_API_URL}/v1/accounts/${address}`) + .then(async response => { + if (response.status >= 200 && response.status < 300) { + if (response.status === 204) { + // Meaning no content = no alias + return undefined; + } + const json = await response.json(); + if (json["alias"]) { + return json["alias"] as string; + } return undefined; - } - const json = await response.json(); - if (json["alias"]) { - return json["alias"] as string; - } + } else return undefined; + }) + .catch(err => { + console.error("Cannot fetch alias", err); return undefined; - } else return undefined; - }); + }); aliases.current[address] = aliasFromTzkt; return aliasFromTzkt; } @@ -112,7 +112,7 @@ export const AliasesProvider = ({ const getAlias = async (address: string, defaultAlias: string) => { const alias = await getTzktAlias(address); if (alias) return alias; - if (aliasesFromState[address]) return aliasesFromState[address]; + if (state[address]) return state[address]; return defaultAlias; }; diff --git a/utils/tzktHooks.ts b/utils/tzktHooks.ts index 5cc28fc6..d2931ea0 100644 --- a/utils/tzktHooks.ts +++ b/utils/tzktHooks.ts @@ -10,13 +10,17 @@ export const useTzktBalance = (address: string | null) => { useEffect(() => { if (!address) return; (async () => { - const response = await fetch( - `${TZKT_API_URL}/v1/accounts/${address}/balance` - ); + try { + const response = await fetch( + `${TZKT_API_URL}/v1/accounts/${address}/balance` + ); - if (response.status === 200) { - const json: number = await response.json(); - setBalance(json / 1_000_000); // Divided by the XTZ decimal + if (response.status === 200) { + const json: number = await response.json(); + setBalance(json / 1_000_000); // Divided by the XTZ decimal + } + } catch (err) { + console.error("Cannot fetch balance", err); } })(); }, [address]); @@ -64,43 +68,47 @@ export const useTzktDefiTokens = (address: string | null) => { useEffect(() => { if (address === null) return; (async () => { - const response = await fetch( - `${TZKT_API_URL}/v1/tokens/balances?account=${address}` - ); - // Check if the response is a success - if (response.status !== 200) { - return; + try { + const response = await fetch( + `${TZKT_API_URL}/v1/tokens/balances?account=${address}` + ); + // Check if the response is a success + if (response.status !== 200) { + return; + } + const tokens: Array = await response.json(); + // First removes what is not a DeFi token + const defi = tokens + .filter(token => { + return ( + token.token.metadata && + token.token.metadata.decimals && + token.token.metadata.symbol // If this field is defined then it's a defi token + ); + }) + .map(token => { + const contract = token.token.contract.address; + const decimals: number = + 10 ** Number.parseInt(token.token.metadata?.decimals || "0"); // Should be defined because of filter + const balance = Number.parseInt(token.balance) / decimals; // the balance is now a float + const symbol: string = token.token.metadata?.symbol as string; // Checked above to be non null + const thumbnailUri = token.token.metadata?.thumbnailUri; + let icon = ""; + if (thumbnailUri && thumbnailUri.startsWith("ipfs://")) { + icon = thumbnailUri.replace("ipfs://", "https://ipfs.io/ipfs/"); + } else if (thumbnailUri && thumbnailUri.startsWith("https://")) { + icon = thumbnailUri; + } else if (!thumbnailUri) { + icon = `https://services.tzkt.io/v1/avatars/${contract}`; // using Tzkt avatar API + } else { + } + return { contract, balance, symbol, icon }; + }) + .filter(token => token.balance !== 0); // We don't care about tokens with 0 balance, I prefer to filter here because the balance is converted to number + setDefi(defi); + } catch (err) { + console.error("Cannot fetch DeFi tokens", err); } - const tokens: Array = await response.json(); - // First removes what is not a DeFi token - const defi = tokens - .filter(token => { - return ( - token.token.metadata && - token.token.metadata.decimals && - token.token.metadata.symbol // If this field is defined then it's a defi token - ); - }) - .map(token => { - const contract = token.token.contract.address; - const decimals: number = - 10 ** Number.parseInt(token.token.metadata?.decimals || "0"); // Should be defined because of filter - const balance = Number.parseInt(token.balance) / decimals; // the balance is now a float - const symbol: string = token.token.metadata?.symbol as string; // Checked above to be non null - const thumbnailUri = token.token.metadata?.thumbnailUri; - let icon = ""; - if (thumbnailUri && thumbnailUri.startsWith("ipfs://")) { - icon = thumbnailUri.replace("ipfs://", "https://ipfs.io/ipfs/"); - } else if (thumbnailUri && thumbnailUri.startsWith("https://")) { - icon = thumbnailUri; - } else if (!thumbnailUri) { - icon = `https://services.tzkt.io/v1/avatars/${contract}`; // using Tzkt avatar API - } else { - } - return { contract, balance, symbol, icon }; - }) - .filter(token => token.balance !== 0); // We don't care about tokens with 0 balance, I prefer to filter here because the balance is converted to number - setDefi(defi); })(); }, [address]); return defi; @@ -120,32 +128,37 @@ export const useTzktPrice = (currency = "usd") => { const [price, setPrice] = useState(null); useEffect(() => { (async () => { - // Let's compute the price evolution - const pricesResponse = await fetch( - `https://back.tzkt.io/v1/home?quote=${currency}` - ); - if (pricesResponse.status !== 200) return; - const pricesJson = await pricesResponse.json(); - const prices: Array<{ date: string; value: number }> = - pricesJson.priceChart; - // let's find the oldest and the most recent date - prices.sort((price1, price2) => { - const date1 = new Date(price1.date); - const date2 = new Date(price2.date); - return date1.getTime() - date2.getTime(); - }); - const first = prices[0]; - const last = prices[prices.length - 1]; - const evolution = last.value / first.value; + try { + // Let's compute the price evolution + const pricesResponse = await fetch( + `https://back.tzkt.io/v1/home?quote=${currency}` + ); + if (pricesResponse.status !== 200) return; + const pricesJson = await pricesResponse.json(); + const prices: Array<{ date: string; value: number }> = + pricesJson.priceChart; + // let's find the oldest and the most recent date + prices.sort((price1, price2) => { + const date1 = new Date(price1.date); + const date2 = new Date(price2.date); + return date1.getTime() - date2.getTime(); + }); + const first = prices[0]; + const last = prices[prices.length - 1]; + const evolution = last.value / first.value; - // Let's get the current price - const headResponse = await fetch("https://back.tzkt.io/v1/head"); - if (headResponse.status !== 200) return; - const headJson = await headResponse.json(); - const value = currency === "usd" ? headJson.quoteUsd : headJson.quoteUsd; // by default we returned the usd price + // Let's get the current price + const headResponse = await fetch("https://back.tzkt.io/v1/head"); + if (headResponse.status !== 200) return; + const headJson = await headResponse.json(); + const value = + currency === "usd" ? headJson.quoteUsd : headJson.quoteUsd; // by default we returned the usd price - // update the hook state - setPrice({ value, evolution }); + // update the hook state + setPrice({ value, evolution }); + } catch (err) { + console.error("Cannot fetch price", err); + } })(); }, [currency]); return price; From 5ca99c42996dc4f632c1735ce8db02a8136d7779 Mon Sep 17 00:00:00 2001 From: Quentin Burg Date: Tue, 30 Apr 2024 16:23:10 +0200 Subject: [PATCH 10/10] :recycle: improving fetch contract --- context/contracts.tsx | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/context/contracts.tsx b/context/contracts.tsx index d5d82e33..6ab19fa4 100644 --- a/context/contracts.tsx +++ b/context/contracts.tsx @@ -1,16 +1,15 @@ import { createContext, useContext, useEffect, useReducer } from "react"; import { ContractStorage, Contracts } from "../types/app"; -import { - loadContracts, - loadDapps, - saveContractsToStorage, -} from "../utils/localStorage"; +import { fetchContract } from "../utils/fetchContract"; +import { loadContracts, saveContractsToStorage } from "../utils/localStorage"; +import { useTezosToolkit } from "./tezos-toolkit"; import { useWallet } from "./wallet"; type ContractsContextType = { contracts: Contracts; addOrUpdateContract(addr: string, contract: ContractStorage): void; removeContract(addr: string): void; + fetchContract(addr: string): Promise; }; type ContractsActions = @@ -31,6 +30,7 @@ const ContractsContext = createContext({ contracts: {}, addOrUpdateContract: () => {}, removeContract: () => {}, + fetchContract: () => Promise.reject(), }); const reducer = (state: Contracts, action: ContractsActions) => { @@ -58,6 +58,19 @@ export const ContractsProvider = ({ }) => { const [state, dispatch] = useReducer(reducer, {}); const { userAddress } = useWallet(); + const { tezos } = useTezosToolkit(); + + const fetch = async (address: string) => { + const contract = await fetchContract(tezos, address); + if (contract) { + dispatch({ + type: "ADD_OR_UPDATE_CONTRACT", + payload: { contractAddress: address, contract }, + }); + return contract; + } + return Promise.reject("Unknown contract"); + }; useEffect(() => { const contracts = userAddress ? loadContracts(userAddress) : {}; @@ -87,6 +100,7 @@ export const ContractsProvider = ({ type: "REMOVE_CONTRACT", payload: { contractAddress: addr }, }), + fetchContract: addr => fetch(addr), }} > {children} @@ -95,3 +109,7 @@ export const ContractsProvider = ({ }; export const useContracts = () => useContext(ContractsContext); +export const useContract = (address: string): ContractStorage | undefined => { + const { contracts } = useContext(ContractsContext); + return contracts[address]; +};