From e0d25a2c61ce8381f03327be8968791420570e28 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp Date: Mon, 11 Sep 2023 13:40:27 -0400 Subject: [PATCH 1/7] added basic allowance grant msg --- .../src/utils/TransactionMessageData.ts | 78 ++++++++++++++++++- deploy-web/src/utils/customRegistry.ts | 4 +- deploy-web/src/utils/proto/grant.ts | 2 + 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/deploy-web/src/utils/TransactionMessageData.ts b/deploy-web/src/utils/TransactionMessageData.ts index 2d89f5175..3363de057 100644 --- a/deploy-web/src/utils/TransactionMessageData.ts +++ b/deploy-web/src/utils/TransactionMessageData.ts @@ -1,5 +1,7 @@ import { networkVersion } from "./constants"; import { BidDto } from "@src/types/deployment"; +import { BasicAllowance, MsgGrantAllowance } from "./proto/grant"; +import { longify } from "@cosmjs/stargate/build/queryclient"; export function setMessageTypes() { TransactionMessageData.Types.MSG_CLOSE_DEPLOYMENT = `/akash.deployment.${networkVersion}.MsgCloseDeployment`; @@ -32,7 +34,9 @@ export class TransactionMessageData { // Cosmos MSG_SEND_TOKENS: "/cosmos.bank.v1beta1.MsgSend", MSG_GRANT: "/cosmos.authz.v1beta1.MsgGrant", - MSG_REVOKE: "/cosmos.authz.v1beta1.MsgRevoke" + MSG_REVOKE: "/cosmos.authz.v1beta1.MsgRevoke", + MSG_GRANT_ALLOWANCE: "/cosmos.feegrant.v1beta1.MsgGrantAllowance", + MSG_REVOKE_ALLOWANCE: "/cosmos.feegrant.v1beta1.MsgRevokeAllowance" }; static getRevokeCertificateMsg(address: string, serial: string) { @@ -200,6 +204,78 @@ export class TransactionMessageData { return message; } + static getGrantBasicAllowanceMsg(granter: string, grantee: string, spendLimit: number, denom: string, expiration?: Date) { + const allowance = { + typeUrl: "/cosmos.feegrant.v1beta1.BasicAllowance", + value: Uint8Array.from( + BasicAllowance.encode({ + spendLimit: [ + { + denom: "ucosm", + amount: "1234567" + } + ], + expiration: expiration + ? { + seconds: longify(Math.floor(expiration.getTime() / 1_000)), + nanos: Math.floor((expiration.getTime() % 1_000) * 1_000_000) + } + : undefined + }).finish() + ) + }; + + const message = { + typeUrl: TransactionMessageData.Types.MSG_GRANT_ALLOWANCE, + value: MsgGrantAllowance.fromPartial({ + granter: granter, + grantee: grantee, + allowance: allowance + }) + }; + + return message; + } + + // static getGrantPeriodicAllowanceMsg(granter: string, grantee: string, spendLimit: number, denom: string, expiration?: Date) { + // const message = { + // typeUrl: TransactionMessageData.Types.MSG_GRANT_ALLOWANCE, + // value: { + // granter: granter, + // grantee: grantee, + // allowance: { + // typeUrl: "/cosmos.feegrant.v1beta1.PeriodicAllowance", + // value: { + // spend_limit: [{ denom: denom, amount: spendLimit.toString() }], + // // Can be undefined, the grant will be valid forever + // expiration: undefined + // } + // } + // } + // }; + + // if (expiration) { + // message.value.allowance.value.expiration = { + // seconds: Math.floor(expiration.getTime() / 1_000), // Convert milliseconds to seconds + // nanos: Math.floor((expiration.getTime() % 1_000) * 1_000_000) // Convert reminder into nanoseconds + // }; + // } + + // return message; + // } + + static getRevokeAllowanceMsg(granter: string, grantee: string) { + const message = { + typeUrl: TransactionMessageData.Types.MSG_REVOKE_ALLOWANCE, + value: { + granter: granter, + grantee: grantee + } + }; + + return message; + } + static getUpdateProviderMsg(owner: string, hostUri: string, attributes: { key: string; value: string }[], info?: { email: string; website: string }) { const message = { typeUrl: TransactionMessageData.Types.MSG_UPDATE_PROVIDER, diff --git a/deploy-web/src/utils/customRegistry.ts b/deploy-web/src/utils/customRegistry.ts index cf4374876..af160053f 100644 --- a/deploy-web/src/utils/customRegistry.ts +++ b/deploy-web/src/utils/customRegistry.ts @@ -3,7 +3,7 @@ import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx"; import { protoTypes } from "./proto"; import { TransactionMessageData } from "./TransactionMessageData"; -import { MsgGrant } from "./proto/grant"; +import { MsgGrant, MsgGrantAllowance, MsgRevokeAllowance } from "./proto/grant"; export let customRegistry: Registry; @@ -21,6 +21,8 @@ export function registerTypes() { registry.register(TransactionMessageData.Types.MSG_GRANT, MsgGrant); registry.register(TransactionMessageData.Types.MSG_REVOKE, MsgRevoke); registry.register(TransactionMessageData.Types.MSG_SEND_TOKENS, MsgSend); + registry.register(TransactionMessageData.Types.MSG_GRANT_ALLOWANCE, MsgGrantAllowance); + registry.register(TransactionMessageData.Types.MSG_REVOKE_ALLOWANCE, MsgRevokeAllowance); customRegistry = registry; } diff --git a/deploy-web/src/utils/proto/grant.ts b/deploy-web/src/utils/proto/grant.ts index 1cdf3ff38..02b024769 100644 --- a/deploy-web/src/utils/proto/grant.ts +++ b/deploy-web/src/utils/proto/grant.ts @@ -1,4 +1,6 @@ import { Field, Type } from "protobufjs"; +export { MsgGrantAllowance, MsgRevokeAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx"; +export { BasicAllowance, PeriodicAllowance, AllowedMsgAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant"; // TODO: Find a solution to the MsgGrant proto type not working from cosmjs-types From f6337a466f2616672d126584e94c5e9782e786b7 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp Date: Tue, 12 Sep 2023 12:38:04 -0400 Subject: [PATCH 2/7] added grant allowances issued and received --- .../DeploymentDepositModal.tsx | 4 +- .../settings/AllowanceIssuedRow.tsx | 44 ++++ .../src/components/wallet/AllowanceModal.tsx | 227 ++++++++++++++++++ .../src/components/wallet/GrantModal.tsx | 8 +- .../src/pages/settings/authorizations.tsx | 168 ++++++++++++- deploy-web/src/queries/queryKeys.ts | 2 + deploy-web/src/queries/useGrantsQuery.ts | 38 +++ deploy-web/src/types/grant.ts | 13 + .../src/utils/TransactionMessageData.ts | 10 +- deploy-web/src/utils/apiUtils.ts | 6 + deploy-web/src/utils/priceUtils.ts | 2 +- 11 files changed, 505 insertions(+), 17 deletions(-) create mode 100644 deploy-web/src/components/settings/AllowanceIssuedRow.tsx create mode 100644 deploy-web/src/components/wallet/AllowanceModal.tsx diff --git a/deploy-web/src/components/deploymentDetail/DeploymentDepositModal.tsx b/deploy-web/src/components/deploymentDetail/DeploymentDepositModal.tsx index ac6aed4c6..c34ad0384 100644 --- a/deploy-web/src/components/deploymentDetail/DeploymentDepositModal.tsx +++ b/deploy-web/src/components/deploymentDetail/DeploymentDepositModal.tsx @@ -97,7 +97,7 @@ export const DeploymentDepositModal: React.FunctionComponent = ({ handleC let spendLimitUDenom = coinToUDenom(grant.authorization.spend_limit); if (depositAmount > spendLimitUDenom) { - setError(`Spend limit remaining: ${udenomToDenom(spendLimitUDenom)} ${depositData.label}`); + setError(`Spend limit remaining: ${udenomToDenom(spendLimitUDenom)} ${depositData?.label}`); return false; } @@ -117,7 +117,7 @@ export const DeploymentDepositModal: React.FunctionComponent = ({ handleC const onBalanceClick = () => { clearErrors(); - setValue("amount", depositData.inputMax); + setValue("amount", depositData?.inputMax); }; const onDepositClick = event => { diff --git a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx new file mode 100644 index 000000000..dd50d6730 --- /dev/null +++ b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx @@ -0,0 +1,44 @@ +import React, { ReactNode } from "react"; +import { IconButton, TableCell } from "@mui/material"; +import { CustomTableRow } from "../shared/CustomTable"; +import { Address } from "../shared/Address"; +import { FormattedTime } from "react-intl"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { AllowanceType } from "@src/types/grant"; +import { AKTAmount } from "../shared/AKTAmount"; +import { coinToUDenom } from "@src/utils/priceUtils"; + +type Props = { + allowance: AllowanceType; + children?: ReactNode; + onEditAllowance: (allowance: AllowanceType) => void; + setDeletingAllowance: (grantallowance: AllowanceType) => void; +}; + +export const AllowanceIssuedRow: React.FunctionComponent = ({ allowance, onEditAllowance, setDeletingAllowance }) => { + // const denomData = useDenomData(grant.authorization.spend_limit.denom); + + return ( + + {allowance.allowance["@type"]} + +
+ + + AKT + + + + + + onEditAllowance(allowance)}> + + + setDeletingAllowance(allowance)}> + + + + + ); +}; diff --git a/deploy-web/src/components/wallet/AllowanceModal.tsx b/deploy-web/src/components/wallet/AllowanceModal.tsx new file mode 100644 index 000000000..b92a0972d --- /dev/null +++ b/deploy-web/src/components/wallet/AllowanceModal.tsx @@ -0,0 +1,227 @@ +import { useRef, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { FormControl, TextField, Typography, Box, Alert, Select, MenuItem, InputLabel, InputAdornment } from "@mui/material"; +import { addYears, format } from "date-fns"; +import { makeStyles } from "tss-react/mui"; +import { useKeplr } from "@src/context/KeplrWalletProvider"; +import { aktToUakt, coinToDenom } from "@src/utils/priceUtils"; +import { TransactionMessageData } from "@src/utils/TransactionMessageData"; +import { LinkTo } from "../shared/LinkTo"; +import { event } from "nextjs-google-analytics"; +import { AnalyticsEvents } from "@src/utils/analytics"; +import { AllowanceType, GrantType } from "@src/types/grant"; +import { Popup } from "../shared/Popup"; +import { getUsdcDenom, useUsdcDenom } from "@src/hooks/useDenom"; +import { denomToUdenom } from "@src/utils/mathHelpers"; +import { useDenomData } from "@src/hooks/useWalletBalance"; +import { uAktDenom } from "@src/utils/constants"; +import { FormattedDate } from "react-intl"; + +const useStyles = makeStyles()(theme => ({ + formControl: { + marginBottom: "1rem" + } +})); + +type Props = { + address: string; + editingAllowance?: AllowanceType; + onClose: () => void; +}; + +export const AllowanceModal: React.FunctionComponent = ({ editingAllowance, address, onClose }) => { + const formRef = useRef(null); + const [error, setError] = useState(""); + const { classes } = useStyles(); + const { signAndBroadcastTx } = useKeplr(); + const usdcDenom = useUsdcDenom(); + const { + handleSubmit, + control, + formState: { errors }, + watch, + clearErrors, + setValue + } = useForm({ + defaultValues: { + amount: editingAllowance ? coinToDenom(editingAllowance.allowance.spend_limit[0]) : 0, + expiration: format(addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm"), + useDepositor: false, + granteeAddress: editingAllowance?.grantee ?? "" + } + }); + const { amount, granteeAddress, expiration } = watch(); + const denomData = useDenomData(uAktDenom); + + console.log(coinToDenom(editingAllowance.allowance.spend_limit[0])); + + const onDepositClick = event => { + event.preventDefault(); + formRef.current.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); + }; + + const onSubmit = async ({ amount }) => { + setError(""); + clearErrors(); + + const messages = []; + const spendLimit = aktToUakt(amount); + const expirationDate = new Date(expiration); + + if (editingAllowance) { + messages.push(TransactionMessageData.getRevokeAllowanceMsg(address, granteeAddress)); + } + messages.push(TransactionMessageData.getGrantBasicAllowanceMsg(address, granteeAddress, spendLimit, uAktDenom, expirationDate)); + const response = await signAndBroadcastTx(messages); + + if (response) { + event(AnalyticsEvents.AUTHORIZE_SPEND, { + category: "deployments", + label: "Authorize wallet to spend on deployment deposits" + }); + + onClose(); + } + }; + + function handleDocClick(ev, url: string) { + ev.preventDefault(); + + window.open(url, "_blank"); + } + + const onBalanceClick = () => { + clearErrors(); + setValue("amount", denomData?.inputMax); + }; + + return ( + +
+ + + handleDocClick(ev, "https://docs.cosmos.network/v0.46/modules/feegrant/")}>Authorized Fee Spend allows users to + authorize spend of a set number of tokens on fees from a source wallet to a destination, funded wallet. + + + + + onBalanceClick()}> + Balance: {denomData?.balance} {denomData?.label} + + + + + { + const helperText = fieldState.error?.type === "validate" ? "Invalid amount." : "Amount is required."; + + return ( + AKT + }} + /> + ); + }} + /> + + + + { + return ( + + ); + }} + /> + + + + { + return ( + + ); + }} + /> + + + {!!amount && granteeAddress && ( + + + This address will be able to spend up to {amount} AKT on fees on your behalf ending on{" "} + . + + + )} + + {error && {error}} +
+
+ ); +}; diff --git a/deploy-web/src/components/wallet/GrantModal.tsx b/deploy-web/src/components/wallet/GrantModal.tsx index eabbf5f3e..8b22edb05 100644 --- a/deploy-web/src/components/wallet/GrantModal.tsx +++ b/deploy-web/src/components/wallet/GrantModal.tsx @@ -59,7 +59,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre const { amount, granteeAddress, expiration, token } = watch(); const selectedToken = supportedTokens.find(x => x.id === token); const denom = token === "akt" ? uAktDenom : usdcDenom; - const depositData = useDenomData(denom); + const denomData = useDenomData(denom); const onDepositClick = event => { event.preventDefault(); @@ -95,7 +95,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre const onBalanceClick = () => { clearErrors(); - setValue("amount", depositData.inputMax); + setValue("amount", denomData?.inputMax); }; return ( @@ -145,7 +145,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre onBalanceClick()}> - Balance: {depositData?.balance} {depositData?.label} + Balance: {denomData?.balance} {denomData?.label} @@ -189,7 +189,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre autoFocus error={!!fieldState.error} helperText={fieldState.error && helperText} - inputProps={{ min: 0, step: 0.000001, max: depositData?.inputMax }} + inputProps={{ min: 0, step: 0.000001, max: denomData?.inputMax }} sx={{ flexGrow: 1, marginLeft: "1rem" }} /> ); diff --git a/deploy-web/src/pages/settings/authorizations.tsx b/deploy-web/src/pages/settings/authorizations.tsx index dd9ae35fb..6901500f0 100644 --- a/deploy-web/src/pages/settings/authorizations.tsx +++ b/deploy-web/src/pages/settings/authorizations.tsx @@ -9,23 +9,40 @@ import { useKeplr } from "@src/context/KeplrWalletProvider"; import { CustomTableHeader } from "@src/components/shared/CustomTable"; import { Address } from "@src/components/shared/Address"; import { GrantModal } from "@src/components/wallet/GrantModal"; -import { GrantType } from "@src/types/grant"; +import { AllowanceType, GrantType } from "@src/types/grant"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; import AccountBalanceIcon from "@mui/icons-material/AccountBalance"; -import { useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; +import { useAllowancesGranted, useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; import { Popup } from "@src/components/shared/Popup"; import { GranterRow } from "@src/components/settings/GranterRow"; import { GranteeRow } from "@src/components/settings/GranteeRow"; +import { AllowanceModal } from "@src/components/wallet/AllowanceModal"; +import { AllowanceIssuedRow } from "@src/components/settings/AllowanceIssuedRow"; +import { makeStyles } from "tss-react/mui"; type Props = {}; +const useStyles = makeStyles()(theme => ({ + subTitle: { + fontSize: "1rem", + color: theme.palette.text.secondary, + marginBottom: "1rem" + } +})); + const SettingsSecurityPage: React.FunctionComponent = ({}) => { + const { classes } = useStyles(); const { address } = useKeplr(); const [editingGrant, setEditingGrant] = useState(null); + const [editingAllowance, setEditingAllowance] = useState(null); const [showGrantModal, setShowGrantModal] = useState(false); + const [showAllowanceModal, setShowAllowanceModal] = useState(false); const [deletingGrant, setDeletingGrant] = useState(null); + const [deletingAllowance, setDeletingAllowance] = useState(null); const { data: granterGrants, isLoading: isLoadingGranterGrants, refetch: refetchGranterGrants } = useGranterGrants(address); const { data: granteeGrants, isLoading: isLoadingGranteeGrants, refetch: refetchGranteeGrants } = useGranteeGrants(address); + const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued, refetch: refetchAllowancesIssued } = useAllowancesIssued(address); + const { data: allowancesGranted, isLoading: isLoadingAllowancesGranted, refetch: refetchAllowancesGranted } = useAllowancesGranted(address); const { signAndBroadcastTx } = useKeplr(); @@ -40,6 +57,17 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { } } + async function onDeleteAllowanceConfirmed() { + const message = TransactionMessageData.getRevokeAllowanceMsg(address, deletingAllowance.grantee); + + const response = await signAndBroadcastTx([message]); + + if (response) { + refetchAllowancesIssued(); + setDeletingAllowance(null); + } + } + function onCreateNewGrant() { setEditingGrant(null); setShowGrantModal(true); @@ -56,12 +84,28 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { setShowGrantModal(false); } + function onCreateNewAllowance() { + setEditingAllowance(null); + setShowAllowanceModal(true); + refetchAllowancesIssued(); + } + + function onAllowanceClose() { + refetchAllowancesIssued(); + setShowAllowanceModal(false); + } + + function onEditAllowance(allowance: AllowanceType) { + setEditingAllowance(allowance); + setShowAllowanceModal(true); + } + return ( - + @@ -72,7 +116,10 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { } > - + + + These authorizations allow you authorize other addresses to spend on deployments using your funds. You can revoke these authorizations at any time. +
{isLoadingGranterGrants || !granterGrants ? ( @@ -138,6 +185,99 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { )}
+ + + Fee Authorizations + + + + + + These authorizations allow you authorize other addresses to spend on fees using your funds. You can revoke these authorizations at any time. + + +
+ {isLoadingAllowancesIssued || !allowancesIssued ? ( + + + + ) : ( + <> + {allowancesIssued.length > 0 ? ( + + + + + Type + Grantee + Spending Limit + Expiration + + + + + + {allowancesIssued.map(allowance => ( + + ))} + +
+
+ ) : ( + No allowances issued. + )} + + )} +
+ +
+ {isLoadingGranteeGrants || !granteeGrants ? ( + + + + ) : ( + <> + {granteeGrants.length > 0 ? ( + + + + + Granter + Spending Limit + Expiration + + + + + {granteeGrants.map(grant => ( + + ))} + +
+
+ ) : ( + No authorizations received. + )} + + )} +
+ {!!deletingGrant && ( = ({}) => { will revoke their ability to spend your funds on deployments. )} + {!!deletingAllowance && ( + setDeletingAllowance(null)} + onCancel={() => setDeletingAllowance(null)} + onValidate={onDeleteAllowanceConfirmed} + enableCloseOnBackdropClick + > + Deleting allowance to{" "} + +
+ {" "} + will revoke their ability to fees on your behalf. + + )} {showGrantModal && } + {showAllowanceModal && } diff --git a/deploy-web/src/queries/queryKeys.ts b/deploy-web/src/queries/queryKeys.ts index b3b70a2f4..73f8abbb5 100644 --- a/deploy-web/src/queries/queryKeys.ts +++ b/deploy-web/src/queries/queryKeys.ts @@ -22,6 +22,8 @@ export class QueryKeys { static getUserFavoriteTemplatesKey = (userId: string) => ["USER_FAVORITES_TEMPLATES", userId]; static getGranterGrants = (address: string) => ["GRANTER_GRANTS", address]; static getGranteeGrants = (address: string) => ["GRANTEE_GRANTS", address]; + static getAllowancesIssued = (address: string) => ["ALLOWANCES_ISSUED", address]; + static getAllowancesGranted = (address: string) => ["ALLOWANCES_GRANTED", address]; // Deploy static getDeploymentListKey = (address: string) => ["DEPLOYMENT_LIST", address]; diff --git a/deploy-web/src/queries/useGrantsQuery.ts b/deploy-web/src/queries/useGrantsQuery.ts index 8a3ad7d22..f8ba606ad 100644 --- a/deploy-web/src/queries/useGrantsQuery.ts +++ b/deploy-web/src/queries/useGrantsQuery.ts @@ -41,3 +41,41 @@ export function useGranteeGrants(address: string, options = {}) { return useQuery(QueryKeys.getGranteeGrants(address), () => getGranteeGrants(settings.apiEndpoint, address), options); } + +async function getAllowancesIssued(apiEndpoint: string, address: string) { + if (!address) return null; + + const response = await axios.get(ApiUrlService.allowancesIssued(apiEndpoint, address)); + // const filteredGrants = response.data.grants.filter( + // x => + // x.authorization["@type"] === "/akash.deployment.v1beta2.DepositDeploymentAuthorization" || + // x.authorization["@type"] === "/akash.deployment.v1beta3.DepositDeploymentAuthorization" + // ); + + return response.data.allowances; +} + +export function useAllowancesIssued(address: string, options = {}) { + const { settings } = useSettings(); + + return useQuery(QueryKeys.getAllowancesIssued(address), () => getAllowancesIssued(settings.apiEndpoint, address), options); +} + +async function getAllowancesGranted(apiEndpoint: string, address: string) { + if (!address) return null; + + const response = await axios.get(ApiUrlService.allowancesGranted(apiEndpoint, address)); + // const filteredGrants = response.data.grants.filter( + // x => + // x.authorization["@type"] === "/akash.deployment.v1beta2.DepositDeploymentAuthorization" || + // x.authorization["@type"] === "/akash.deployment.v1beta3.DepositDeploymentAuthorization" + // ); + + return response.data.allowances; +} + +export function useAllowancesGranted(address: string, options = {}) { + const { settings } = useSettings(); + + return useQuery(QueryKeys.getAllowancesGranted(address), () => getAllowancesGranted(settings.apiEndpoint, address), options); +} \ No newline at end of file diff --git a/deploy-web/src/types/grant.ts b/deploy-web/src/types/grant.ts index db2de3f28..cd9c8c227 100644 --- a/deploy-web/src/types/grant.ts +++ b/deploy-web/src/types/grant.ts @@ -10,3 +10,16 @@ export type GrantType = { }; }; }; + +export type AllowanceType = { + granter: string; + grantee: string; + allowance: { + "@type": string; + expiration: string; + spend_limit: { + denom: string; + amount: string; + }[]; + }; +}; diff --git a/deploy-web/src/utils/TransactionMessageData.ts b/deploy-web/src/utils/TransactionMessageData.ts index 3363de057..dfd1889cd 100644 --- a/deploy-web/src/utils/TransactionMessageData.ts +++ b/deploy-web/src/utils/TransactionMessageData.ts @@ -1,6 +1,6 @@ import { networkVersion } from "./constants"; import { BidDto } from "@src/types/deployment"; -import { BasicAllowance, MsgGrantAllowance } from "./proto/grant"; +import { BasicAllowance, MsgGrantAllowance, MsgRevokeAllowance } from "./proto/grant"; import { longify } from "@cosmjs/stargate/build/queryclient"; export function setMessageTypes() { @@ -211,8 +211,8 @@ export class TransactionMessageData { BasicAllowance.encode({ spendLimit: [ { - denom: "ucosm", - amount: "1234567" + denom: denom, + amount: spendLimit.toString() } ], expiration: expiration @@ -267,10 +267,10 @@ export class TransactionMessageData { static getRevokeAllowanceMsg(granter: string, grantee: string) { const message = { typeUrl: TransactionMessageData.Types.MSG_REVOKE_ALLOWANCE, - value: { + value: MsgRevokeAllowance.fromPartial({ granter: granter, grantee: grantee - } + }) }; return message; diff --git a/deploy-web/src/utils/apiUtils.ts b/deploy-web/src/utils/apiUtils.ts index 805cde45a..23298ed3c 100644 --- a/deploy-web/src/utils/apiUtils.ts +++ b/deploy-web/src/utils/apiUtils.ts @@ -45,6 +45,12 @@ export class ApiUrlService { static granterGrants(apiEndpoint: string, address: string) { return `${apiEndpoint}/cosmos/authz/v1beta1/grants/granter/${address}`; } + static allowancesIssued(apiEndpoint: string, address: string) { + return `${apiEndpoint}/cosmos/feegrant/v1beta1/issued/${address}`; + } + static allowancesGranted(apiEndpoint: string, address: string) { + return `${apiEndpoint}/cosmos/feegrant/v1beta1/allowances/${address}`; + } static dashboardData() { return `${BASE_API_URL}/dashboardData`; } diff --git a/deploy-web/src/utils/priceUtils.ts b/deploy-web/src/utils/priceUtils.ts index 281b6380c..ae09a81e1 100644 --- a/deploy-web/src/utils/priceUtils.ts +++ b/deploy-web/src/utils/priceUtils.ts @@ -38,7 +38,7 @@ export function coinToDenom(coin: Coin) { if (coin.denom === "akt") { value = parseFloat(coin.amount); } else if (coin.denom === uAktDenom || coin.denom === usdcDenom) { - value = uaktToAKT(parseFloat(coin.amount)); + value = uaktToAKT(parseFloat(coin.amount), 6); } else { throw Error("Unrecognized denom: " + coin.denom); } From 70d9a42ab92ba10cd32544927239ae16dc607279 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp Date: Tue, 12 Sep 2023 12:41:48 -0400 Subject: [PATCH 3/7] allowance type in table --- .../src/components/settings/AllowanceIssuedRow.tsx | 3 ++- deploy-web/src/utils/grants.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 deploy-web/src/utils/grants.ts diff --git a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx index dd50d6730..9d15f8617 100644 --- a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx +++ b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx @@ -8,6 +8,7 @@ import DeleteIcon from "@mui/icons-material/Delete"; import { AllowanceType } from "@src/types/grant"; import { AKTAmount } from "../shared/AKTAmount"; import { coinToUDenom } from "@src/utils/priceUtils"; +import { getAllowanceTitleByType } from "@src/utils/grants"; type Props = { allowance: AllowanceType; @@ -21,7 +22,7 @@ export const AllowanceIssuedRow: React.FunctionComponent = ({ allowance, return ( - {allowance.allowance["@type"]} + {getAllowanceTitleByType(allowance)}
diff --git a/deploy-web/src/utils/grants.ts b/deploy-web/src/utils/grants.ts new file mode 100644 index 000000000..68c894965 --- /dev/null +++ b/deploy-web/src/utils/grants.ts @@ -0,0 +1,13 @@ +import { AllowanceType } from "@src/types/grant"; + +export const getAllowanceTitleByType = (allowance: AllowanceType) => { + switch (allowance.allowance["@type"]) { + case "/cosmos.feegrant.v1beta1.BasicAllowance": + return "Basic"; + case "/cosmos.feegrant.v1beta1.PeriodicAllowance": + return "Periodic"; + + default: + return "Unknown"; + } +}; From 29c83c4de3644d7ef5e5f2bd9b864159b176c339 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp Date: Tue, 12 Sep 2023 13:35:24 -0400 Subject: [PATCH 4/7] refactored MsgGrant type to cosmjs --- deploy-web/src/queries/useGrantsQuery.ts | 6 +++-- .../src/utils/TransactionMessageData.ts | 27 ++++++++++--------- deploy-web/src/utils/customRegistry.ts | 3 +-- deploy-web/src/utils/proto/grant.ts | 26 +----------------- 4 files changed, 21 insertions(+), 41 deletions(-) diff --git a/deploy-web/src/queries/useGrantsQuery.ts b/deploy-web/src/queries/useGrantsQuery.ts index f8ba606ad..6f0d079d6 100644 --- a/deploy-web/src/queries/useGrantsQuery.ts +++ b/deploy-web/src/queries/useGrantsQuery.ts @@ -29,7 +29,9 @@ async function getGranteeGrants(apiEndpoint: string, address: string) { const response = await axios.get(ApiUrlService.granteeGrants(apiEndpoint, address)); const filteredGrants = response.data.grants.filter( x => - x.authorization["@type"] === "/akash.deployment.v1beta2.DepositDeploymentAuthorization" || + // TODO: this is not working + // Only the v1beta3 authorization are working + // x.authorization["@type"] === "/akash.deployment.v1beta2.DepositDeploymentAuthorization" || x.authorization["@type"] === "/akash.deployment.v1beta3.DepositDeploymentAuthorization" ); @@ -78,4 +80,4 @@ export function useAllowancesGranted(address: string, options = {}) { const { settings } = useSettings(); return useQuery(QueryKeys.getAllowancesGranted(address), () => getAllowancesGranted(settings.apiEndpoint, address), options); -} \ No newline at end of file +} diff --git a/deploy-web/src/utils/TransactionMessageData.ts b/deploy-web/src/utils/TransactionMessageData.ts index dfd1889cd..b5f2467f5 100644 --- a/deploy-web/src/utils/TransactionMessageData.ts +++ b/deploy-web/src/utils/TransactionMessageData.ts @@ -1,7 +1,8 @@ import { networkVersion } from "./constants"; import { BidDto } from "@src/types/deployment"; -import { BasicAllowance, MsgGrantAllowance, MsgRevokeAllowance } from "./proto/grant"; +import { BasicAllowance, MsgGrant, MsgGrantAllowance, MsgRevoke, MsgRevokeAllowance } from "./proto/grant"; import { longify } from "@cosmjs/stargate/build/queryclient"; +import { protoTypes } from "./proto"; export function setMessageTypes() { TransactionMessageData.Types.MSG_CLOSE_DEPLOYMENT = `/akash.deployment.${networkVersion}.MsgCloseDeployment`; @@ -162,20 +163,22 @@ export class TransactionMessageData { } static getGrantMsg(granter: string, grantee: string, spendLimit: number, expiration: Date, denom: string) { - const message = { + const grantMsg = { typeUrl: TransactionMessageData.Types.MSG_GRANT, value: { granter: granter, grantee: grantee, grant: { authorization: { - type_url: TransactionMessageData.Types.MSG_DEPOSIT_DEPLOYMENT_AUTHZ, - value: { - spend_limit: { - denom: denom, - amount: spendLimit.toString() - } - } + typeUrl: TransactionMessageData.Types.MSG_DEPOSIT_DEPLOYMENT_AUTHZ, + value: protoTypes.DepositDeploymentAuthorization.encode( + protoTypes.DepositDeploymentAuthorization.fromPartial({ + spendLimit: { + denom: denom, + amount: spendLimit.toString() + } + }) + ).finish() }, expiration: { seconds: Math.floor(expiration.getTime() / 1_000), // Convert milliseconds to seconds @@ -185,7 +188,7 @@ export class TransactionMessageData { } }; - return message; + return grantMsg; } static getRevokeMsg(granter: string, grantee: string, grantType: string) { @@ -194,11 +197,11 @@ export class TransactionMessageData { const message = { typeUrl: TransactionMessageData.Types.MSG_REVOKE, - value: { + value: MsgRevoke.fromPartial({ granter: granter, grantee: grantee, msgTypeUrl: msgTypeUrl - } + }) }; return message; diff --git a/deploy-web/src/utils/customRegistry.ts b/deploy-web/src/utils/customRegistry.ts index af160053f..d01d23548 100644 --- a/deploy-web/src/utils/customRegistry.ts +++ b/deploy-web/src/utils/customRegistry.ts @@ -1,9 +1,8 @@ import { Registry } from "@cosmjs/proto-signing"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; -import { MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx"; import { protoTypes } from "./proto"; import { TransactionMessageData } from "./TransactionMessageData"; -import { MsgGrant, MsgGrantAllowance, MsgRevokeAllowance } from "./proto/grant"; +import { MsgGrant, MsgRevoke, MsgGrantAllowance, MsgRevokeAllowance } from "./proto/grant"; export let customRegistry: Registry; diff --git a/deploy-web/src/utils/proto/grant.ts b/deploy-web/src/utils/proto/grant.ts index 02b024769..f5424a577 100644 --- a/deploy-web/src/utils/proto/grant.ts +++ b/deploy-web/src/utils/proto/grant.ts @@ -1,27 +1,3 @@ -import { Field, Type } from "protobufjs"; export { MsgGrantAllowance, MsgRevokeAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx"; export { BasicAllowance, PeriodicAllowance, AllowedMsgAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant"; - -// TODO: Find a solution to the MsgGrant proto type not working from cosmjs-types - -const Coin = new Type("Coin").add(new Field("denom", 1, "string")).add(new Field("amount", 2, "string")); - -const Authorization = new Type("Authorization").add(Coin).add(new Field("spend_limit", 1, "Coin")); - -const AnyGrantDeposit = new Type("AnyGrantDeposit") - .add(new Field("type_url", 1, "string")) - .add(new Field("value", 2, "Authorization")) - .add(Authorization); - -const Timestamp = new Type("Timestamp").add(new Field("seconds", 1, "uint64")).add(new Field("nanos", 2, "uint32")); -const Grant = new Type("Grant") - .add(new Field("authorization", 1, "AnyGrantDeposit")) - .add(AnyGrantDeposit) - .add(new Field("expiration", 2, "Timestamp")) - .add(Timestamp); - -export const MsgGrant = new Type("MsgGrant") - .add(new Field("granter", 1, "string")) - .add(new Field("grantee", 2, "string")) - .add(new Field("grant", 3, "Grant")) - .add(Grant); +export { MsgGrant, MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx"; From ef57baf56569833c89f8c803d122ab714d268dad Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp Date: Tue, 12 Sep 2023 18:52:01 -0400 Subject: [PATCH 5/7] added refreshing when deleting/adding grants --- .../settings/AllowanceGrantedRow.tsx | 33 ++++++++ .../settings/AllowanceIssuedRow.tsx | 2 +- .../src/components/settings/GranterRow.tsx | 2 +- .../src/components/wallet/AllowanceModal.tsx | 11 +-- .../src/pages/settings/authorizations.tsx | 82 +++++++++++++------ deploy-web/src/utils/deploymentUtils.ts | 9 +- deploy-web/src/utils/timer.ts | 8 ++ 7 files changed, 103 insertions(+), 44 deletions(-) create mode 100644 deploy-web/src/components/settings/AllowanceGrantedRow.tsx diff --git a/deploy-web/src/components/settings/AllowanceGrantedRow.tsx b/deploy-web/src/components/settings/AllowanceGrantedRow.tsx new file mode 100644 index 000000000..6e0444e3f --- /dev/null +++ b/deploy-web/src/components/settings/AllowanceGrantedRow.tsx @@ -0,0 +1,33 @@ +import React, { ReactNode } from "react"; +import { TableCell } from "@mui/material"; +import { CustomTableRow } from "../shared/CustomTable"; +import { Address } from "../shared/Address"; +import { FormattedTime } from "react-intl"; +import { AllowanceType } from "@src/types/grant"; +import { AKTAmount } from "../shared/AKTAmount"; +import { coinToUDenom } from "@src/utils/priceUtils"; +import { getAllowanceTitleByType } from "@src/utils/grants"; + +type Props = { + allowance: AllowanceType; + children?: ReactNode; +}; + +export const AllowanceGrantedRow: React.FunctionComponent = ({ allowance }) => { + // const denomData = useDenomData(grant.authorization.spend_limit.denom); + + return ( + + {getAllowanceTitleByType(allowance)} + +
+ + + AKT + + + + + + ); +}; diff --git a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx index 9d15f8617..d9aced5d4 100644 --- a/deploy-web/src/components/settings/AllowanceIssuedRow.tsx +++ b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx @@ -32,7 +32,7 @@ export const AllowanceIssuedRow: React.FunctionComponent = ({ allowance, - + onEditAllowance(allowance)}> diff --git a/deploy-web/src/components/settings/GranterRow.tsx b/deploy-web/src/components/settings/GranterRow.tsx index 812fbd783..703c4a0c3 100644 --- a/deploy-web/src/components/settings/GranterRow.tsx +++ b/deploy-web/src/components/settings/GranterRow.tsx @@ -31,7 +31,7 @@ export const GranterRow: React.FunctionComponent = ({ children, grant, on - + onEditGrant(grant)}> diff --git a/deploy-web/src/components/wallet/AllowanceModal.tsx b/deploy-web/src/components/wallet/AllowanceModal.tsx index b92a0972d..79ffd2fb6 100644 --- a/deploy-web/src/components/wallet/AllowanceModal.tsx +++ b/deploy-web/src/components/wallet/AllowanceModal.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from "react"; import { useForm, Controller } from "react-hook-form"; -import { FormControl, TextField, Typography, Box, Alert, Select, MenuItem, InputLabel, InputAdornment } from "@mui/material"; +import { FormControl, TextField, Typography, Box, Alert, InputAdornment } from "@mui/material"; import { addYears, format } from "date-fns"; import { makeStyles } from "tss-react/mui"; import { useKeplr } from "@src/context/KeplrWalletProvider"; @@ -9,10 +9,8 @@ import { TransactionMessageData } from "@src/utils/TransactionMessageData"; import { LinkTo } from "../shared/LinkTo"; import { event } from "nextjs-google-analytics"; import { AnalyticsEvents } from "@src/utils/analytics"; -import { AllowanceType, GrantType } from "@src/types/grant"; +import { AllowanceType } from "@src/types/grant"; import { Popup } from "../shared/Popup"; -import { getUsdcDenom, useUsdcDenom } from "@src/hooks/useDenom"; -import { denomToUdenom } from "@src/utils/mathHelpers"; import { useDenomData } from "@src/hooks/useWalletBalance"; import { uAktDenom } from "@src/utils/constants"; import { FormattedDate } from "react-intl"; @@ -34,7 +32,6 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc const [error, setError] = useState(""); const { classes } = useStyles(); const { signAndBroadcastTx } = useKeplr(); - const usdcDenom = useUsdcDenom(); const { handleSubmit, control, @@ -53,8 +50,6 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc const { amount, granteeAddress, expiration } = watch(); const denomData = useDenomData(uAktDenom); - console.log(coinToDenom(editingAllowance.allowance.spend_limit[0])); - const onDepositClick = event => { event.preventDefault(); formRef.current.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); @@ -214,7 +209,7 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc {!!amount && granteeAddress && ( - This address will be able to spend up to {amount} AKT on fees on your behalf ending on{" "} + This address will be able to spend up to {amount} AKT on transaction fees on your behalf ending on{" "} . diff --git a/deploy-web/src/pages/settings/authorizations.tsx b/deploy-web/src/pages/settings/authorizations.tsx index 6901500f0..75233cada 100644 --- a/deploy-web/src/pages/settings/authorizations.tsx +++ b/deploy-web/src/pages/settings/authorizations.tsx @@ -4,7 +4,7 @@ import PageContainer from "@src/components/shared/PageContainer"; import SettingsLayout, { SettingsTabs } from "@src/components/settings/SettingsLayout"; import { Fieldset } from "@src/components/shared/Fieldset"; import { Box, Button, CircularProgress, Table, TableBody, TableCell, TableContainer, TableRow, Typography } from "@mui/material"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useKeplr } from "@src/context/KeplrWalletProvider"; import { CustomTableHeader } from "@src/components/shared/CustomTable"; import { Address } from "@src/components/shared/Address"; @@ -19,6 +19,8 @@ import { GranteeRow } from "@src/components/settings/GranteeRow"; import { AllowanceModal } from "@src/components/wallet/AllowanceModal"; import { AllowanceIssuedRow } from "@src/components/settings/AllowanceIssuedRow"; import { makeStyles } from "tss-react/mui"; +import { averageBlockTime } from "@src/utils/priceUtils"; +import { AllowanceGrantedRow } from "@src/components/settings/AllowanceGrantedRow"; type Props = {}; @@ -30,6 +32,10 @@ const useStyles = makeStyles()(theme => ({ } })); +type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | "allowancesGranted" | null; +const defaultRefetchInterval = 30 * 1000; +const refreshingInterval = 1000; + const SettingsSecurityPage: React.FunctionComponent = ({}) => { const { classes } = useStyles(); const { address } = useKeplr(); @@ -39,20 +45,43 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { const [showAllowanceModal, setShowAllowanceModal] = useState(false); const [deletingGrant, setDeletingGrant] = useState(null); const [deletingAllowance, setDeletingAllowance] = useState(null); - const { data: granterGrants, isLoading: isLoadingGranterGrants, refetch: refetchGranterGrants } = useGranterGrants(address); - const { data: granteeGrants, isLoading: isLoadingGranteeGrants, refetch: refetchGranteeGrants } = useGranteeGrants(address); - const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued, refetch: refetchAllowancesIssued } = useAllowancesIssued(address); - const { data: allowancesGranted, isLoading: isLoadingAllowancesGranted, refetch: refetchAllowancesGranted } = useAllowancesGranted(address); - + const [isRefreshing, setIsRefreshing] = useState(null); + const { data: granterGrants, isLoading: isLoadingGranterGrants } = useGranterGrants(address, { + refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval + }); + const { data: granteeGrants, isLoading: isLoadingGranteeGrants } = useGranteeGrants(address, { + refetchInterval: isRefreshing === "granteeGrants" ? refreshingInterval : defaultRefetchInterval + }); + 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 { signAndBroadcastTx } = useKeplr(); + useEffect(() => { + let timeout: NodeJS.Timeout; + if (isRefreshing) { + timeout = setTimeout(() => { + setIsRefreshing(null); + }, averageBlockTime + 1000); + } + + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, [isRefreshing]); + async function onDeleteGrantConfirmed() { const message = TransactionMessageData.getRevokeMsg(address, deletingGrant.grantee, deletingGrant.authorization["@type"]); const response = await signAndBroadcastTx([message]); if (response) { - refetchGranterGrants(); + setIsRefreshing("granterGrants"); setDeletingGrant(null); } } @@ -63,7 +92,7 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { const response = await signAndBroadcastTx([message]); if (response) { - refetchAllowancesIssued(); + setIsRefreshing("allowancesIssued"); setDeletingAllowance(null); } } @@ -71,7 +100,6 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { function onCreateNewGrant() { setEditingGrant(null); setShowGrantModal(true); - refetchGranterGrants(); } function onEditGrant(grant: GrantType) { @@ -80,18 +108,17 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { } function onGrantClose() { - refetchGranteeGrants(); + setIsRefreshing("granterGrants"); setShowGrantModal(false); } function onCreateNewAllowance() { setEditingAllowance(null); setShowAllowanceModal(true); - refetchAllowancesIssued(); } function onAllowanceClose() { - refetchAllowancesIssued(); + setIsRefreshing("allowancesIssued"); setShowAllowanceModal(false); } @@ -101,7 +128,7 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { } return ( - + = ({}) => { } > - These authorizations allow you authorize other addresses to spend on deployments using your funds. You can revoke these authorizations at any time. + These authorizations allow you authorize other addresses to spend on deployments or deployment deposits using your funds. You can revoke these + authorizations at any time.
{isLoadingGranterGrants || !granterGrants ? ( @@ -135,7 +163,7 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { Grantee Spending Limit Expiration - + @@ -195,7 +223,7 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { }} > - Fee Authorizations + Tx Fee Authorizations