From 82d385c5654123435066d80844bba97c8410d488 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Wed, 12 Jun 2024 15:44:37 +0200 Subject: [PATCH 1/2] feat(wallet): add fee granter to transaction signer As a part of this add select modal prompt refs #219 --- .../src/components/shared/Popup.tsx | 72 ++++++++++++++++++- .../context/PopupProvider/PopupProvider.tsx | 43 ++++++++++- .../context/WalletProvider/WalletProvider.tsx | 42 ++++++++++- apps/deploy-web/src/pages/_app.tsx | 30 ++++---- apps/deploy-web/src/queries/useGrantsQuery.ts | 11 ++- .../src/utils/TransactionMessageData.ts | 3 +- 6 files changed, 173 insertions(+), 28 deletions(-) diff --git a/apps/deploy-web/src/components/shared/Popup.tsx b/apps/deploy-web/src/components/shared/Popup.tsx index 68e954653..d00737413 100644 --- a/apps/deploy-web/src/components/shared/Popup.tsx +++ b/apps/deploy-web/src/components/shared/Popup.tsx @@ -1,7 +1,9 @@ import * as React from "react"; +import { useMemo } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { DialogProps } from "@radix-ui/react-dialog"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui/select"; import { cn } from "@src/utils/styleUtils"; import { Button, ButtonProps } from "@akashnetwork/ui/components"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle as _DialogTitle } from "../ui/dialog"; @@ -32,6 +34,21 @@ type CustomPrompt = { actions: ActionButton[]; }; +export type SelectOption = { + text: string; + value: string; + selected?: boolean; + disabled?: boolean; +}; + +export type SelectProps = { + variant: "select"; + options: SelectOption[]; + placeholder?: string; + onValidate: (value: string | undefined) => void; + onCancel: () => void; +}; + export type TOnCloseHandler = { (event: any, reason: "backdropClick" | "escapeKeyDown" | "action"): void; }; @@ -58,7 +75,7 @@ export type ActionButton = ButtonProps & { isLoading?: boolean; }; -export type PopupProps = (MessageProps | ConfirmProps | PromptProps | CustomPrompt) & CommonProps; +export type PopupProps = (MessageProps | ConfirmProps | PromptProps | CustomPrompt | SelectProps) & CommonProps; export interface DialogTitleProps { children: React.ReactNode; @@ -78,6 +95,8 @@ export const DialogTitle = (props: DialogTitleProps) => { export function Popup(props: React.PropsWithChildren) { const [promptInput, setPromptInput] = React.useState(""); + const initialOption = useMemo(() => (props.variant === "select" ? props.options.find(option => option.selected)?.value : undefined), [props]); + const [selectOption, setSelectOption] = React.useState(initialOption); const component = [] as JSX.Element[]; const onClose: TOnCloseHandler = (event, reason) => { @@ -97,7 +116,30 @@ export function Popup(props: React.PropsWithChildren) { component.push({props.title}); } - if (props.message && props.variant !== "prompt") { + if (props.variant === "select") { + component.push( + + {props.message} + {props.variant === "select" ? ( + + ) : null} + + + ); + } else if (props.message && props.variant !== "prompt") { component.push( {props.message} @@ -215,6 +257,32 @@ export function Popup(props: React.PropsWithChildren) { ); break; } + case "select": { + component.push( + + + + + ); + break; + } } /** diff --git a/apps/deploy-web/src/context/PopupProvider/PopupProvider.tsx b/apps/deploy-web/src/context/PopupProvider/PopupProvider.tsx index 57d8f0843..a0480ab0d 100644 --- a/apps/deploy-web/src/context/PopupProvider/PopupProvider.tsx +++ b/apps/deploy-web/src/context/PopupProvider/PopupProvider.tsx @@ -1,13 +1,15 @@ import React, { useCallback, useMemo, useState } from "react"; import { firstValueFrom, Subject } from "rxjs"; -import { CommonProps, ConfirmProps, Popup, PopupProps } from "@src/components/shared/Popup"; +import { CommonProps, ConfirmProps, Popup, PopupProps, SelectOption, SelectProps } from "@src/components/shared/Popup"; import type { FCWithChildren } from "@src/types/component"; type ConfirmPopupProps = string | (Omit & Omit); +type SelectPopupProps = Omit & Omit; type PopupProviderContext = { confirm: (messageOrProps: ConfirmPopupProps) => Promise; + select: (props: SelectPopupProps) => Promise; }; const PopupContext = React.createContext(undefined); @@ -15,7 +17,7 @@ const PopupContext = React.createContext(undef export const PopupProvider: FCWithChildren = ({ children }) => { const [popupProps, setPopupProps] = useState(); - const confirm = useCallback( + const confirm: PopupProviderContext["confirm"] = useCallback( (messageOrProps: ConfirmPopupProps) => { let subject: Subject | undefined = new Subject(); @@ -45,7 +47,42 @@ export const PopupProvider: FCWithChildren = ({ children }) => { [setPopupProps] ); - const context = useMemo(() => ({ confirm }), [confirm]); + const select: PopupProviderContext["select"] = useCallback( + props => { + let subject: Subject | undefined = new Subject(); + + const reject = () => { + if (subject) { + subject.next(undefined); + subject.complete(); + setPopupProps(undefined); + subject = undefined; + } + }; + + setPopupProps({ + title: "Confirm", + ...props, + open: true, + variant: "select", + onValidate: value => { + if (subject) { + subject.next(value); + subject.complete(); + setPopupProps(undefined); + subject = undefined; + } + }, + onCancel: reject, + onClose: reject + }); + + return firstValueFrom(subject); + }, + [setPopupProps] + ); + + const context = useMemo(() => ({ confirm, select }), [confirm]); return ( diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index 70091110f..799ce9493 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -1,10 +1,12 @@ "use client"; -import React, { useRef } from "react"; +import React, { useMemo, useRef } from "react"; import { useEffect, useState } from "react"; import { EncodeObject } from "@cosmjs/proto-signing"; import { SigningStargateClient } from "@cosmjs/stargate"; import { useManager } from "@cosmos-kit/react"; import axios from "axios"; +import isAfter from "date-fns/isAfter"; +import parseISO from "date-fns/parseISO"; import { OpenNewWindow } from "iconoir-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -12,12 +14,17 @@ import { event } from "nextjs-google-analytics"; import { SnackbarKey, useSnackbar } from "notistack"; import { TransactionModal } from "@src/components/layout/TransactionModal"; +import { SelectOption } from "@src/components/shared/Popup"; import { Snackbar } from "@src/components/shared/Snackbar"; +import { usePopup } from "@src/context/PopupProvider/PopupProvider"; import { useUsdcDenom } from "@src/hooks/useDenom"; import { getSelectedNetwork, useSelectedNetwork } from "@src/hooks/useSelectedNetwork"; +import { useAllowancesGranted } from "@src/queries/useGrantsQuery"; import { AnalyticsEvents } from "@src/utils/analytics"; import { STATS_APP_URL, uAktDenom } from "@src/utils/constants"; import { customRegistry } from "@src/utils/customRegistry"; +import { udenomToDenom } from "@src/utils/mathHelpers"; +import { coinToUDenom } from "@src/utils/priceUtils"; import { UrlService } from "@src/utils/urlUtils"; import { LocalWalletDataType } from "@src/utils/walletUtils"; import { useSelectedChain } from "../CustomChainProvider"; @@ -55,6 +62,32 @@ export const WalletProvider = ({ children }) => { const usdcIbcDenom = useUsdcDenom(); const { disconnect, getOfflineSigner, isWalletConnected, address: walletAddress, connect, username, estimateFee, sign, broadcast } = useSelectedChain(); const { addEndpoints } = useManager(); + const { data: allowancesGranted } = useAllowancesGranted(walletAddress); + + const feeGranters = useMemo(() => { + if (!walletAddress || !allowancesGranted) { + return; + } + + const connectedWallet: SelectOption = { text: "Connected Wallet", value: walletAddress }; + const options: SelectOption[] = allowancesGranted.reduce( + (acc, grant, index) => { + if (isAfter(parseISO(grant.allowance.expiration), new Date())) { + acc.push({ + text: `${grant.granter} (${udenomToDenom(coinToUDenom(grant.allowance.spend_limit[0]), 6)} AKT)`, + value: grant.granter, + selected: index === 0 + }); + } + + return acc; + }, + [connectedWallet] + ); + + return options?.length > 1 ? options : undefined; + }, [allowancesGranted, walletAddress]); + const { select } = usePopup(); useEffect(() => { if (!settings.apiEndpoint || !settings.rpcEndpoint) return; @@ -160,8 +193,11 @@ export const WalletProvider = ({ children }) => { let pendingSnackbarKey: SnackbarKey | null = null; try { const estimatedFees = await estimateFee(msgs); - - const txRaw = await sign(msgs, estimatedFees); + const feeGranter = feeGranters && (await select({ title: "Select fee granter", options: feeGranters })); + const txRaw = await sign(msgs, { + ...estimatedFees, + granter: feeGranter + }); setIsWaitingForApproval(false); setIsBroadcastingTx(true); diff --git a/apps/deploy-web/src/pages/_app.tsx b/apps/deploy-web/src/pages/_app.tsx index be57dbd93..0b54da33a 100644 --- a/apps/deploy-web/src/pages/_app.tsx +++ b/apps/deploy-web/src/pages/_app.tsx @@ -64,22 +64,22 @@ const App: React.FunctionComponent = props => { - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + diff --git a/apps/deploy-web/src/queries/useGrantsQuery.ts b/apps/deploy-web/src/queries/useGrantsQuery.ts index 699f62ed5..e2bc1cfab 100644 --- a/apps/deploy-web/src/queries/useGrantsQuery.ts +++ b/apps/deploy-web/src/queries/useGrantsQuery.ts @@ -1,7 +1,9 @@ -import { useQuery } from "react-query"; +import { QueryObserverResult, useQuery } from "react-query"; import axios from "axios"; import { useSettings } from "@src/context/SettingsProvider"; +import { Coin } from "@src/types"; +import { AllowanceType } from "@src/types/grant"; import { ApiUrlService } from "@src/utils/apiUtils"; import { QueryKeys } from "./queryKeys"; @@ -67,8 +69,11 @@ async function getAllowancesGranted(apiEndpoint: string, address: string) { return response.data.allowances; } -export function useAllowancesGranted(address: string, options = {}) { +export function useAllowancesGranted(address?: string, options = {}): QueryObserverResult { const { settings } = useSettings(); - return useQuery(QueryKeys.getAllowancesGranted(address), () => getAllowancesGranted(settings.apiEndpoint, address), options); + return useQuery(address ? QueryKeys.getAllowancesGranted(address) : "", () => (address ? getAllowancesGranted(settings.apiEndpoint, address) : undefined), { + ...options, + enabled: !!address + }); } diff --git a/apps/deploy-web/src/utils/TransactionMessageData.ts b/apps/deploy-web/src/utils/TransactionMessageData.ts index 4aeff8d60..476eadf99 100644 --- a/apps/deploy-web/src/utils/TransactionMessageData.ts +++ b/apps/deploy-web/src/utils/TransactionMessageData.ts @@ -1,4 +1,3 @@ -import { longify } from "@cosmjs/stargate/build/queryclient"; import Long from "long"; import { BidDto } from "@src/types/deployment"; @@ -222,7 +221,7 @@ export class TransactionMessageData { ], expiration: expiration ? { - seconds: longify(Math.floor(expiration.getTime() / 1_000)) as unknown as Long, + seconds: Long.fromInt(Math.floor(expiration.getTime() / 1_000)) as unknown as Long, nanos: Math.floor((expiration.getTime() % 1_000) * 1_000_000) } : undefined From d8da6a30887d0168f6fc9a753e7f2ed5cd1f516e Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Thu, 13 Jun 2024 14:48:35 +0200 Subject: [PATCH 2/2] feat(wallet): implement fee granter as a global setting closes #219 --- .../authorizations/AllowanceGrantedRow.tsx | 18 ++-- .../authorizations/AllowanceWatcher.tsx | 8 ++ .../authorizations/Authorizations.tsx | 29 ++++-- .../context/WalletProvider/WalletProvider.tsx | 40 ++------- apps/deploy-web/src/hooks/useAllowance.tsx | 89 +++++++++++++++++++ apps/deploy-web/src/hooks/useWhen.ts | 10 +++ apps/deploy-web/src/pages/_app.tsx | 2 + apps/deploy-web/src/utils/grants.ts | 3 +- 8 files changed, 151 insertions(+), 48 deletions(-) create mode 100644 apps/deploy-web/src/components/authorizations/AllowanceWatcher.tsx create mode 100644 apps/deploy-web/src/hooks/useAllowance.tsx create mode 100644 apps/deploy-web/src/hooks/useWhen.ts diff --git a/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx b/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx index 85fae1fe7..a1ec1824a 100644 --- a/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx +++ b/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx @@ -4,6 +4,7 @@ import { FormattedTime } from "react-intl"; import { Address } from "@src/components/shared/Address"; import { AKTAmount } from "@src/components/shared/AKTAmount"; +import { Checkbox } from "@src/components/ui/checkbox"; import { TableCell, TableRow } from "@src/components/ui/table"; import { AllowanceType } from "@src/types/grant"; import { getAllowanceTitleByType } from "@src/utils/grants"; @@ -12,21 +13,24 @@ import { coinToUDenom } from "@src/utils/priceUtils"; type Props = { allowance: AllowanceType; children?: ReactNode; + onSelect?: () => void; + selected?: boolean; }; -export const AllowanceGrantedRow: React.FunctionComponent = ({ allowance }) => { +export const AllowanceGrantedRow: React.FunctionComponent = ({ allowance, selected, onSelect }) => { + const limit = allowance?.allowance.spend_limit[0]; return ( - {getAllowanceTitleByType(allowance)} -
+ checked && onSelect() : undefined} /> + {getAllowanceTitleByType(allowance)} + {allowance.granter &&
} - AKT - - - + {limit && } + {limit && "AKT"} + {} ); }; diff --git a/apps/deploy-web/src/components/authorizations/AllowanceWatcher.tsx b/apps/deploy-web/src/components/authorizations/AllowanceWatcher.tsx new file mode 100644 index 000000000..67e356c4d --- /dev/null +++ b/apps/deploy-web/src/components/authorizations/AllowanceWatcher.tsx @@ -0,0 +1,8 @@ +import { FC } from "react"; + +import { useAllowance } from "@src/hooks/useAllowance"; + +export const AllowanceWatcher: FC = () => { + useAllowance(); + return null; +}; diff --git a/apps/deploy-web/src/components/authorizations/Authorizations.tsx b/apps/deploy-web/src/components/authorizations/Authorizations.tsx index 1e0e6b2bc..35301511b 100644 --- a/apps/deploy-web/src/components/authorizations/Authorizations.tsx +++ b/apps/deploy-web/src/components/authorizations/Authorizations.tsx @@ -10,7 +10,8 @@ import Spinner from "@src/components/shared/Spinner"; import { Button } from "@akashnetwork/ui/components"; import { Table, TableBody, TableHead, TableHeader, TableRow } from "@src/components/ui/table"; import { useWallet } from "@src/context/WalletProvider"; -import { useAllowancesGranted, useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; +import { useAllowance } from "@src/hooks/useAllowance"; +import { useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; import { AllowanceType, GrantType } from "@src/types/grant"; import { averageBlockTime } from "@src/utils/priceUtils"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; @@ -46,9 +47,9 @@ export const Authorizations: React.FunctionComponent = () => { const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued } = useAllowancesIssued(address, { refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval }); - const { data: allowancesGranted, isLoading: isLoadingAllowancesGranted } = useAllowancesGranted(address, { - refetchInterval: isRefreshing === "allowancesGranted" ? refreshingInterval : defaultRefetchInterval - }); + const { + fee: { all: allowancesGranted, isLoading: isLoadingAllowancesGranted, setDefault, default: defaultAllowance } + } = useAllowance(); useEffect(() => { let timeout: NodeJS.Timeout; @@ -261,6 +262,7 @@ export const Authorizations: React.FunctionComponent = () => { + Default Type Grantee Spending Limit @@ -269,8 +271,25 @@ export const Authorizations: React.FunctionComponent = () => { + {!!allowancesGranted && ( + setDefault(undefined)} + selected={!defaultAllowance} + /> + )} {allowancesGranted.map(allowance => ( - + setDefault(allowance.granter)} + selected={defaultAllowance === allowance.granter} + /> ))}
diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index 799ce9493..546ac51d8 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -1,12 +1,10 @@ "use client"; -import React, { useMemo, useRef } from "react"; +import React, { useRef } from "react"; import { useEffect, useState } from "react"; import { EncodeObject } from "@cosmjs/proto-signing"; import { SigningStargateClient } from "@cosmjs/stargate"; import { useManager } from "@cosmos-kit/react"; import axios from "axios"; -import isAfter from "date-fns/isAfter"; -import parseISO from "date-fns/parseISO"; import { OpenNewWindow } from "iconoir-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -14,17 +12,13 @@ import { event } from "nextjs-google-analytics"; import { SnackbarKey, useSnackbar } from "notistack"; import { TransactionModal } from "@src/components/layout/TransactionModal"; -import { SelectOption } from "@src/components/shared/Popup"; import { Snackbar } from "@src/components/shared/Snackbar"; -import { usePopup } from "@src/context/PopupProvider/PopupProvider"; +import { useAllowance } from "@src/hooks/useAllowance"; import { useUsdcDenom } from "@src/hooks/useDenom"; import { getSelectedNetwork, useSelectedNetwork } from "@src/hooks/useSelectedNetwork"; -import { useAllowancesGranted } from "@src/queries/useGrantsQuery"; import { AnalyticsEvents } from "@src/utils/analytics"; import { STATS_APP_URL, uAktDenom } from "@src/utils/constants"; import { customRegistry } from "@src/utils/customRegistry"; -import { udenomToDenom } from "@src/utils/mathHelpers"; -import { coinToUDenom } from "@src/utils/priceUtils"; import { UrlService } from "@src/utils/urlUtils"; import { LocalWalletDataType } from "@src/utils/walletUtils"; import { useSelectedChain } from "../CustomChainProvider"; @@ -62,32 +56,9 @@ export const WalletProvider = ({ children }) => { const usdcIbcDenom = useUsdcDenom(); const { disconnect, getOfflineSigner, isWalletConnected, address: walletAddress, connect, username, estimateFee, sign, broadcast } = useSelectedChain(); const { addEndpoints } = useManager(); - const { data: allowancesGranted } = useAllowancesGranted(walletAddress); - - const feeGranters = useMemo(() => { - if (!walletAddress || !allowancesGranted) { - return; - } - - const connectedWallet: SelectOption = { text: "Connected Wallet", value: walletAddress }; - const options: SelectOption[] = allowancesGranted.reduce( - (acc, grant, index) => { - if (isAfter(parseISO(grant.allowance.expiration), new Date())) { - acc.push({ - text: `${grant.granter} (${udenomToDenom(coinToUDenom(grant.allowance.spend_limit[0]), 6)} AKT)`, - value: grant.granter, - selected: index === 0 - }); - } - - return acc; - }, - [connectedWallet] - ); - - return options?.length > 1 ? options : undefined; - }, [allowancesGranted, walletAddress]); - const { select } = usePopup(); + const { + fee: { default: feeGranter } + } = useAllowance(); useEffect(() => { if (!settings.apiEndpoint || !settings.rpcEndpoint) return; @@ -193,7 +164,6 @@ export const WalletProvider = ({ children }) => { let pendingSnackbarKey: SnackbarKey | null = null; try { const estimatedFees = await estimateFee(msgs); - const feeGranter = feeGranters && (await select({ title: "Select fee granter", options: feeGranters })); const txRaw = await sign(msgs, { ...estimatedFees, granter: feeGranter diff --git a/apps/deploy-web/src/hooks/useAllowance.tsx b/apps/deploy-web/src/hooks/useAllowance.tsx new file mode 100644 index 000000000..0c589e978 --- /dev/null +++ b/apps/deploy-web/src/hooks/useAllowance.tsx @@ -0,0 +1,89 @@ +import React, { FC, useMemo } from "react"; +import isAfter from "date-fns/isAfter"; +import parseISO from "date-fns/parseISO"; +import { OpenNewWindow } from "iconoir-react"; +import difference from "lodash/difference"; +import Link from "next/link"; +import { useSnackbar } from "notistack"; +import { useLocalStorage } from "usehooks-ts"; + +import { Snackbar } from "@src/components/shared/Snackbar"; +import { useWallet } from "@src/context/WalletProvider"; +import { useWhen } from "@src/hooks/useWhen"; +import { useAllowancesGranted } from "@src/queries/useGrantsQuery"; + +const persisted: Record = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("fee-granters") || "{}") : {}; + +const AllowanceNotificationMessage: FC = () => ( + <> + You can update default fee granter in + + Authorizations Settings + + + +); + +export const useAllowance = () => { + const { address } = useWallet(); + const [defaultFeeGranter, setDefaultFeeGranter] = useLocalStorage("default-fee-granter", undefined); + const { data: allFeeGranters, isLoading, isFetched } = useAllowancesGranted(address); + const { enqueueSnackbar } = useSnackbar(); + + const actualAddresses = useMemo(() => { + if (!address || !allFeeGranters) { + return []; + } + + return allFeeGranters.reduce((acc, grant) => { + if (isAfter(parseISO(grant.allowance.expiration), new Date())) { + acc.push(grant.granter); + } + + return acc; + }, [] as string[]); + }, [allFeeGranters, address]); + + useWhen( + isFetched && address, + () => { + const persistedAddresses = persisted[address] || []; + const added = difference(actualAddresses, persistedAddresses); + const removed = difference(persistedAddresses, actualAddresses); + + if (added.length || removed.length) { + persisted[address] = actualAddresses; + localStorage.setItem(`fee-granters`, JSON.stringify(persisted)); + } + + if (added.length) { + enqueueSnackbar(} />, { + variant: "info" + }); + } + + if (removed.length) { + enqueueSnackbar(} />, { + variant: "warning" + }); + } + + if (defaultFeeGranter && removed.includes(defaultFeeGranter)) { + setDefaultFeeGranter(undefined); + } + }, + [actualAddresses, persisted] + ); + + return useMemo( + () => ({ + fee: { + all: allFeeGranters, + default: defaultFeeGranter, + setDefault: setDefaultFeeGranter, + isLoading + } + }), + [defaultFeeGranter, setDefaultFeeGranter, allFeeGranters, isLoading] + ); +}; diff --git a/apps/deploy-web/src/hooks/useWhen.ts b/apps/deploy-web/src/hooks/useWhen.ts new file mode 100644 index 000000000..66d9a2278 --- /dev/null +++ b/apps/deploy-web/src/hooks/useWhen.ts @@ -0,0 +1,10 @@ +import { useEffect } from "react"; + +export function useWhen(condition: T, run: () => void, deps: unknown[] = []): void { + return useEffect(() => { + if (condition) { + run(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [condition, ...deps]); +} diff --git a/apps/deploy-web/src/pages/_app.tsx b/apps/deploy-web/src/pages/_app.tsx index 0b54da33a..29453a7c2 100644 --- a/apps/deploy-web/src/pages/_app.tsx +++ b/apps/deploy-web/src/pages/_app.tsx @@ -9,6 +9,7 @@ import Router from "next/router"; import { ThemeProvider } from "next-themes"; import NProgress from "nprogress"; //nprogress module +import { AllowanceWatcher } from "@src/components/authorizations/AllowanceWatcher"; import GoogleAnalytics from "@src/components/layout/CustomGoogleAnalytics"; import { CustomIntlProvider } from "@src/components/layout/CustomIntlProvider"; import { PageHead } from "@src/components/layout/PageHead"; @@ -71,6 +72,7 @@ const App: React.FunctionComponent = props => { + diff --git a/apps/deploy-web/src/utils/grants.ts b/apps/deploy-web/src/utils/grants.ts index 68c894965..cf3909911 100644 --- a/apps/deploy-web/src/utils/grants.ts +++ b/apps/deploy-web/src/utils/grants.ts @@ -6,7 +6,8 @@ export const getAllowanceTitleByType = (allowance: AllowanceType) => { return "Basic"; case "/cosmos.feegrant.v1beta1.PeriodicAllowance": return "Periodic"; - + case "$CONNECTED_WALLET": + return "Connected Wallet"; default: return "Unknown"; }