diff --git a/config/authz.ts b/config/authz.ts index 309512f..63e3b94 100644 --- a/config/authz.ts +++ b/config/authz.ts @@ -1,4 +1,5 @@ /* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable camelcase */ @@ -8,6 +9,14 @@ import { SendAuthorization } from 'cosmjs-types/cosmos/bank/v1beta1/authz' import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin' import type { AuthorizationType } from 'cosmjs-types/cosmos/staking/v1beta1/authz' import { StakeAuthorization, StakeAuthorization_Validators } from 'cosmjs-types/cosmos/staking/v1beta1/authz' +import { + AcceptedMessageKeysFilter, + AllowAllMessagesFilter, + CombinedLimit, + ContractExecutionAuthorization, + MaxCallsLimit, + MaxFundsLimit, +} from 'cosmjs-types/cosmwasm/wasm/v1/authz' import type { AuthorizationMode, GenericAuthorizationType, GrantAuthorizationType } from 'pages/authz/grant' export interface Msg { @@ -82,6 +91,82 @@ export function AuthzSendGrantMsg( } } +export function AuthzExecuteContractGrantMsg( + granter: string, + grantee: string, + contract: string, + expiration: number, + callsRemaining?: number, + amounts?: Coin[], + allowedMessages?: string[], +): Msg { + const sendAuthValue = ContractExecutionAuthorization.encode( + ContractExecutionAuthorization.fromPartial({ + grants: [ + { + contract, + filter: { + typeUrl: allowedMessages + ? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter' + : '/cosmwasm.wasm.v1.AllowAllMessagesFilter', + value: allowedMessages + ? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish() + : AllowAllMessagesFilter.encode({}).finish(), + }, + limit: + callsRemaining || amounts + ? { + typeUrl: + callsRemaining && amounts + ? '/cosmwasm.wasm.v1.CombinedLimit' + : callsRemaining + ? '/cosmwasm.wasm.v1.MaxCallsLimit' + : '/cosmwasm.wasm.v1.MaxFundsLimit', + value: + callsRemaining && amounts + ? CombinedLimit.encode({ + callsRemaining: BigInt(callsRemaining), + amounts, + }).finish() + : callsRemaining + ? MaxCallsLimit.encode({ + remaining: BigInt(callsRemaining), + }).finish() + : MaxFundsLimit.encode({ + amounts: amounts || [], + }).finish(), + } + : { + // limit: undefined is not accepted + typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit', + value: MaxCallsLimit.encode({ + remaining: BigInt(100000), + }).finish(), + }, + }, + ], + }), + ).finish() + + const grantValue = MsgGrant.fromPartial({ + grant: { + authorization: { + typeUrl: '/cosmwasm.wasm.v1.ContractExecutionAuthorization', + value: sendAuthValue, + }, + // TODO: fix expiration issue + expiration: expiration ? { seconds: BigInt(expiration) } : undefined, + }, + grantee, + granter, + }) + + return { + typeUrl: msgAuthzGrantTypeUrl, + value: grantValue, + } +} + export function AuthzGenericGrantMsg(granter: string, grantee: string, typeURL: string, expiration: number): Msg { return { typeUrl: msgAuthzGrantTypeUrl, diff --git a/pages/authz/grant.tsx b/pages/authz/grant.tsx index a93506f..6355a94 100644 --- a/pages/authz/grant.tsx +++ b/pages/authz/grant.tsx @@ -6,19 +6,19 @@ /* eslint-disable tailwindcss/classnames-order */ /* eslint-disable react/button-has-type */ -import { GasPrice, SigningStargateClient } from '@cosmjs/stargate' +import { coins, GasPrice, SigningStargateClient } from '@cosmjs/stargate' import { Alert } from 'components/Alert' import { Conditional } from 'components/Conditional' import { ContractPageHeader } from 'components/ContractPageHeader' import { FormControl } from 'components/FormControl' -import { NumberInput, TextInput } from 'components/forms/FormInput' +import { AddressInput, NumberInput, TextInput } from 'components/forms/FormInput' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { InputDateTime } from 'components/InputDateTime' import { LinkTabs } from 'components/LinkTabs' import { authzLinkTabs } from 'components/LinkTabs.data' import { getConfig } from 'config' import type { Msg } from 'config/authz' -import { AuthzGenericGrantMsg, AuthzSendGrantMsg } from 'config/authz' +import { AuthzExecuteContractGrantMsg, AuthzGenericGrantMsg, AuthzSendGrantMsg } from 'config/authz' import { useGlobalSettings } from 'contexts/globalSettings' import type { NextPage } from 'next' import { NextSeo } from 'next-seo' @@ -74,6 +74,22 @@ const Grant: NextPage = () => { subtitle: 'The spend limit', }) + const maxFundsLimitDenomState = useInputState({ + id: 'max-funds-limit-denom', + name: 'maxFundsLimitDenom', + title: 'Max Funds Limit Denom', + placeholder: `ustars`, + subtitle: 'The denom for max funds limit', + }) + + const maxFundsLimitState = useNumberInputState({ + id: 'max-funds-limit', + name: 'maxFundsLimit', + title: 'Max Funds Limit', + placeholder: `1000000`, + subtitle: 'The max funds limit for contract execution (leave blank for no limit)', + }) + const allowListState = useInputState({ id: 'allow-list', name: 'allowList', @@ -82,6 +98,30 @@ const Grant: NextPage = () => { subtitle: 'Comma separated list of addresses to allow transactions to', }) + const contractAddressState = useInputState({ + id: 'contract-address', + name: 'contractAddress', + title: 'Contract Address', + placeholder: `stars1...`, + subtitle: 'The contract address to authorize execution on', + }) + + const allowedMessageKeysState = useInputState({ + id: 'allowed-message-keys', + name: 'allowedMessageKeys', + title: 'Allowed Message Keys', + placeholder: `mint_to, burn, transfer`, + subtitle: 'Comma separated list of allowed message keys (leave blank to allow all)', + }) + + const callsRemainingState = useNumberInputState({ + id: 'calls-remaining', + name: 'callsRemaining', + title: 'Calls Remaining', + placeholder: `10`, + subtitle: 'The allowed number of contract execution calls (leave blank for no limit)', + }) + const messageToSign = () => { if (authType === 'Generic') { if (genericAuthType === 'MsgSend') { @@ -117,6 +157,16 @@ const Grant: NextPage = () => { (expiration?.getTime() as number) / 1000 || 0, allowListState.value ? allowListState.value.split(',').map((address) => address.trim()) : [], ) + } else if (authType === 'Execute Smart Contract') { + return AuthzExecuteContractGrantMsg( + wallet.address || '', + granteeAddressState.value, + contractAddressState.value, + (expiration?.getTime() as number) / 1000 || 0, + callsRemainingState.value ? callsRemainingState.value : undefined, + maxFundsLimitState.value > 0 ? coins(maxFundsLimitState.value, maxFundsLimitDenomState.value) : undefined, + allowedMessageKeysState.value ? allowedMessageKeysState.value.split(',').map((key) => key.trim()) : undefined, + ) } } const handleSendMessage = async () => { @@ -156,9 +206,7 @@ const Grant: NextPage = () => { > - + @@ -199,6 +247,15 @@ const Grant: NextPage = () => { {/* */} + + + + +
+ + +
+