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/graph/Graph.tsx b/deploy-web/src/components/graph/Graph.tsx index 2d149efd8..83dcea035 100644 --- a/deploy-web/src/components/graph/Graph.tsx +++ b/deploy-web/src/components/graph/Graph.tsx @@ -20,7 +20,7 @@ const useStyles = makeStyles()(theme => ({ fontWeight: "bold", letterSpacing: "1px", fontSize: "1rem", - color: "rgba(255,255,255,.2)" + color: theme.palette.mode === "dark" ? theme.palette.grey[800] : theme.palette.grey[400] } }, graphTooltip: { 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 new file mode 100644 index 000000000..d9aced5d4 --- /dev/null +++ b/deploy-web/src/components/settings/AllowanceIssuedRow.tsx @@ -0,0 +1,45 @@ +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"; +import { getAllowanceTitleByType } from "@src/utils/grants"; + +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 ( + + {getAllowanceTitleByType(allowance)} + +
+ + + AKT + + + + + + onEditAllowance(allowance)}> + + + setDeletingAllowance(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 new file mode 100644 index 000000000..79ffd2fb6 --- /dev/null +++ b/deploy-web/src/components/wallet/AllowanceModal.tsx @@ -0,0 +1,222 @@ +import { useRef, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +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"; +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 } from "@src/types/grant"; +import { Popup } from "../shared/Popup"; +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 { + 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); + + 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 transaction 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..75233cada 100644 --- a/deploy-web/src/pages/settings/authorizations.tsx +++ b/deploy-web/src/pages/settings/authorizations.tsx @@ -4,46 +4,102 @@ 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"; 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"; +import { averageBlockTime } from "@src/utils/priceUtils"; +import { AllowanceGrantedRow } from "@src/components/settings/AllowanceGrantedRow"; type Props = {}; +const useStyles = makeStyles()(theme => ({ + subTitle: { + fontSize: "1rem", + color: theme.palette.text.secondary, + marginBottom: "1rem" + } +})); + +type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | "allowancesGranted" | null; +const defaultRefetchInterval = 30 * 1000; +const refreshingInterval = 1000; + 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 { data: granterGrants, isLoading: isLoadingGranterGrants, refetch: refetchGranterGrants } = useGranterGrants(address); - const { data: granteeGrants, isLoading: isLoadingGranteeGrants, refetch: refetchGranteeGrants } = useGranteeGrants(address); - + const [deletingAllowance, setDeletingAllowance] = useState(null); + 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); } } + async function onDeleteAllowanceConfirmed() { + const message = TransactionMessageData.getRevokeAllowanceMsg(address, deletingAllowance.grantee); + + const response = await signAndBroadcastTx([message]); + + if (response) { + setIsRefreshing("allowancesIssued"); + setDeletingAllowance(null); + } + } + function onCreateNewGrant() { setEditingGrant(null); setShowGrantModal(true); - refetchGranterGrants(); } function onEditGrant(grant: GrantType) { @@ -52,27 +108,46 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { } function onGrantClose() { - refetchGranteeGrants(); + setIsRefreshing("granterGrants"); setShowGrantModal(false); } + function onCreateNewAllowance() { + setEditingAllowance(null); + setShowAllowanceModal(true); + } + + function onAllowanceClose() { + setIsRefreshing("allowancesIssued"); + setShowAllowanceModal(false); + } + + function onEditAllowance(allowance: AllowanceType) { + setEditingAllowance(allowance); + setShowAllowanceModal(true); + } + return ( - + } > - + + + 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 ? ( @@ -88,7 +163,7 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { Grantee Spending Limit Expiration - + @@ -138,6 +213,101 @@ const SettingsSecurityPage: React.FunctionComponent = ({}) => { )}
+ + + Tx Fee Authorizations + + + + + + These authorizations allow you authorize other addresses to spend on transaction 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. + )} + + )} +
+ +
+ {isLoadingAllowancesGranted || !allowancesGranted ? ( + + + + ) : ( + <> + {allowancesGranted.length > 0 ? ( + + + + + Type + Grantee + Spending Limit + Expiration + + + + + {allowancesGranted.map(allowance => ( + + ))} + +
+
+ ) : ( + No allowances 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..951f262af 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" ); @@ -41,3 +43,31 @@ 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)); + + 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)); + + return response.data.allowances; +} + +export function useAllowancesGranted(address: string, options = {}) { + const { settings } = useSettings(); + + return useQuery(QueryKeys.getAllowancesGranted(address), () => getAllowancesGranted(settings.apiEndpoint, address), options); +} 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 2d89f5175..b5f2467f5 100644 --- a/deploy-web/src/utils/TransactionMessageData.ts +++ b/deploy-web/src/utils/TransactionMessageData.ts @@ -1,5 +1,8 @@ import { networkVersion } from "./constants"; import { BidDto } from "@src/types/deployment"; +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`; @@ -32,7 +35,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) { @@ -158,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 @@ -181,7 +188,7 @@ export class TransactionMessageData { } }; - return message; + return grantMsg; } static getRevokeMsg(granter: string, grantee: string, grantType: string) { @@ -190,11 +197,83 @@ export class TransactionMessageData { const message = { typeUrl: TransactionMessageData.Types.MSG_REVOKE, - value: { + value: MsgRevoke.fromPartial({ granter: granter, grantee: grantee, msgTypeUrl: msgTypeUrl - } + }) + }; + + 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: denom, + amount: spendLimit.toString() + } + ], + 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: 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/customRegistry.ts b/deploy-web/src/utils/customRegistry.ts index cf4374876..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 } from "./proto/grant"; +import { MsgGrant, MsgRevoke, MsgGrantAllowance, MsgRevokeAllowance } from "./proto/grant"; export let customRegistry: Registry; @@ -21,6 +20,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/deploymentUtils.ts b/deploy-web/src/utils/deploymentUtils.ts index e5c609b6f..0e6db1419 100644 --- a/deploy-web/src/utils/deploymentUtils.ts +++ b/deploy-web/src/utils/deploymentUtils.ts @@ -1,6 +1,7 @@ import axios from "axios"; import { PROVIDER_PROXY_URL } from "./constants"; import { LocalCert } from "@src/context/CertificateProvider/CertificateProviderContext"; +import { wait } from "./timer"; export const sendManifestToProvider = async (providerInfo, manifest, dseq: string, localCert: LocalCert) => { console.log("Sending manifest to " + providerInfo?.owner); @@ -43,11 +44,3 @@ export const sendManifestToProvider = async (providerInfo, manifest, dseq: strin return response; }; - -async function wait(time) { - return new Promise((res, rej) => { - setTimeout(() => { - res(true); - }, time); - }); -} 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"; + } +}; 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); } diff --git a/deploy-web/src/utils/proto/grant.ts b/deploy-web/src/utils/proto/grant.ts index 1cdf3ff38..f5424a577 100644 --- a/deploy-web/src/utils/proto/grant.ts +++ b/deploy-web/src/utils/proto/grant.ts @@ -1,25 +1,3 @@ -import { Field, Type } from "protobufjs"; - -// 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 { MsgGrantAllowance, MsgRevokeAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx"; +export { BasicAllowance, PeriodicAllowance, AllowedMsgAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant"; +export { MsgGrant, MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx"; diff --git a/deploy-web/src/utils/timer.ts b/deploy-web/src/utils/timer.ts index b6825ded8..353824223 100644 --- a/deploy-web/src/utils/timer.ts +++ b/deploy-web/src/utils/timer.ts @@ -22,3 +22,11 @@ export const Timer = (ms: number) => { abort }; }; + +export async function wait(time: number) { + return new Promise((res, rej) => { + setTimeout(() => { + res(true); + }, time); + }); +}