diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index 5c97e7323..f620ea55b 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -844,6 +844,7 @@ "noProposalsYet": "No proposals to vote on yet.", "noSubDaosYet": "No SubDAOs yet.", "noSubmission": "No submission", + "noTokenSelected": "No token selected", "noVestingPaymentsFound": "No vesting payments found.", "noVote": "No", "noWithVeto": "No with Veto", diff --git a/packages/stateful/actions/core/authorizations/AuthzExec/Component.tsx b/packages/stateful/actions/core/authorizations/AuthzExec/Component.tsx index fdddc4fd0..18f7471e0 100644 --- a/packages/stateful/actions/core/authorizations/AuthzExec/Component.tsx +++ b/packages/stateful/actions/core/authorizations/AuthzExec/Component.tsx @@ -7,6 +7,7 @@ import { NestedActionsEditorOptions, NestedActionsRenderer, NestedActionsRendererProps, + useChain, } from '@dao-dao/stateless' import { AddressInputProps, @@ -17,9 +18,8 @@ import { import { ActionComponent } from '@dao-dao/types/actions' import { isValidBech32Address, makeValidateAddress } from '@dao-dao/utils' -import { useActionOptions } from '../../../react' - export type AuthzExecData = { + chainId: string // Set common address to use for all actions. address: string // Once created, fill group adjacent messages by sender. @@ -43,9 +43,7 @@ export const AuthzExecComponent: ActionComponent = ( props ) => { const { t } = useTranslation() - const { - chain: { bech32_prefix: bech32Prefix }, - } = useActionOptions() + const { bech32_prefix: bech32Prefix } = useChain() const { watch, register } = useFormContext() const { fieldNamePrefix, diff --git a/packages/stateful/actions/core/authorizations/AuthzExec/README.md b/packages/stateful/actions/core/authorizations/AuthzExec/README.md index 7a7fa4013..0ea9ebd83 100644 --- a/packages/stateful/actions/core/authorizations/AuthzExec/README.md +++ b/packages/stateful/actions/core/authorizations/AuthzExec/README.md @@ -16,6 +16,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). ```json { + "chainId": "", "address": "", "_actionData": [ // ACTIONS diff --git a/packages/stateful/actions/core/authorizations/AuthzExec/index.tsx b/packages/stateful/actions/core/authorizations/AuthzExec/index.tsx index 2fae8704b..374f68271 100644 --- a/packages/stateful/actions/core/authorizations/AuthzExec/index.tsx +++ b/packages/stateful/actions/core/authorizations/AuthzExec/index.tsx @@ -3,9 +3,15 @@ import { useFormContext } from 'react-hook-form' import { constSelector, useRecoilValueLoadable } from 'recoil' import { MsgExec } from '@dao-dao/protobuf/codegen/cosmos/authz/v1beta1/tx' -import { LockWithKeyEmoji, useChain } from '@dao-dao/stateless' +import { + ChainPickerInput, + ChainProvider, + LockWithKeyEmoji, + useChain, +} from '@dao-dao/stateless' import { ActionComponent, + ActionContextType, ActionKey, ActionMaker, CosmosMsgFor_Empty, @@ -15,10 +21,12 @@ import { } from '@dao-dao/types' import { cwMsgToProtobuf, + decodePolytoneExecuteMsg, isDecodedStargateMsg, isValidContractAddress, isValidWalletAddress, makeStargateMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, protobufToCwMsg, } from '@dao-dao/utils' @@ -32,6 +40,7 @@ import { import { daoInfoSelector } from '../../../../recoil' import { WalletActionsProvider, + useActionOptions, useActionsForMatching, useLoadedActionsAndCategories, } from '../../../react' @@ -41,11 +50,6 @@ import { AuthzExecComponent as StatelessAuthzExecComponent, } from './Component' -const useDefaults: UseDefaults = () => ({ - address: '', - msgs: [], -}) - type InnerOptions = Pick const InnerComponentLoading: ActionComponent = (props) => ( @@ -105,55 +109,73 @@ const InnerComponentWrapper: ActionComponent< : constSelector(undefined) ) - return isContractAddress ? ( - daoInfoLoadable.state === 'hasValue' ? ( - }> - - - - - ) : ( - - ) - ) : isWalletAddress ? ( + return isContractAddress && + daoInfoLoadable.state === 'hasValue' && + daoInfoLoadable.contents ? ( + }> + + + + + ) : (isContractAddress && + (daoInfoLoadable.state === 'hasError' || !daoInfoLoadable.contents)) || + isWalletAddress ? ( ) : ( - + ) } const Component: ActionComponent = (props) => { + const { context } = useActionOptions() + // Load DAO info for chosen DAO. const { watch } = useFormContext() const address = watch((props.fieldNamePrefix + 'address') as 'address') const msgsPerSender = watch((props.fieldNamePrefix + '_msgs') as '_msgs') ?? [] + const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId') + // When creating, just show one form for the chosen address. When not // creating, render a form for each sender message group since the component // needs to be wrapped in the providers for that sender. - return props.isCreating ? ( - - ) : ( + return ( <> - {msgsPerSender.map(({ sender }, index) => ( - - ))} + )} + + + {props.isCreating ? ( + + ) : ( + <> + {msgsPerSender.map(({ sender }, index) => ( + + ))} + + )} + ) } @@ -161,11 +183,25 @@ const Component: ActionComponent = (props) => { export const makeAuthzExecAction: ActionMaker = ({ t, address: grantee, + chain: { chain_id: currentChainId }, }) => { + const useDefaults: UseDefaults = () => ({ + chainId: currentChainId, + address: '', + msgs: [], + }) + const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( msg: Record - ) => - useMemo(() => { + ) => { + let chainId = currentChainId + const decodedPolytone = decodePolytoneExecuteMsg(chainId, msg) + if (decodedPolytone.match) { + chainId = decodedPolytone.chainId + msg = decodedPolytone.msg + } + + return useMemo(() => { if ( !isDecodedStargateMsg(msg) || msg.stargate.typeUrl !== MsgExec.typeUrl || @@ -204,6 +240,7 @@ export const makeAuthzExecAction: ActionMaker = ({ return { match: true, data: { + chainId, // Technically each message could have a different address. While we // don't support that on creation, we can still detect and render them // correctly in the component. @@ -212,20 +249,25 @@ export const makeAuthzExecAction: ActionMaker = ({ _msgs: msgsPerSender, }, } - }, [msg]) + }, [chainId, msg]) + } const useTransformToCosmos: UseTransformToCosmos = () => useCallback( - ({ address, msgs }) => - makeStargateMessage({ - stargate: { - typeUrl: MsgExec.typeUrl, - value: { - grantee, - msgs: msgs.map((msg) => cwMsgToProtobuf(msg, address)), - } as MsgExec, - }, - }), + ({ chainId, address, msgs }) => + maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeStargateMessage({ + stargate: { + typeUrl: MsgExec.typeUrl, + value: { + grantee, + msgs: msgs.map((msg) => cwMsgToProtobuf(msg, address)), + } as MsgExec, + }, + }) + ), [] ) diff --git a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.stories.tsx b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.stories.tsx index f8d4cce2e..baba359dc 100644 --- a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.stories.tsx +++ b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.stories.tsx @@ -25,6 +25,7 @@ export default { component: AuthzGrantRevokeComponent, decorators: [ makeReactHookFormDecorator({ + chainId: CHAIN_ID, mode: 'grant', authorizationTypeUrl: GenericAuthorization.typeUrl, customTypeUrl: false, diff --git a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.tsx b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.tsx index 7d972798c..33f0e0296 100644 --- a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.tsx +++ b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/Component.tsx @@ -29,12 +29,12 @@ import { SelectInput, TextInput, WarningCard, + useChain, } from '@dao-dao/stateless' import { AddressInputProps, GenericTokenBalance, LoadingData, - TokenType, } from '@dao-dao/types' import { ActionComponent } from '@dao-dao/types/actions' import { @@ -47,7 +47,6 @@ import { validateRequired, } from '@dao-dao/utils' -import { useActionOptions } from '../../../react' import { ACTION_TYPES, AUTHORIZATION_TYPES, @@ -65,9 +64,7 @@ export const AuthzGrantRevokeComponent: ActionComponent< AuthzGrantRevokeOptions > = (props) => { const { t } = useTranslation() - const { - chain: { chain_id: chainId, bech32_prefix: bech32Prefix }, - } = useActionOptions() + const { chain_id: chainId, bech32_prefix: bech32Prefix } = useChain() const { fieldNamePrefix, errors, @@ -239,14 +236,7 @@ export const AuthzGrantRevokeComponent: ActionComponent< {...({ ...props, options: { - nativeBalances: balances.loading - ? { loading: true } - : { - loading: false, - data: balances.data.filter( - ({ token }) => token.type === TokenType.Native - ), - }, + nativeBalances: balances, }, onRemove: isCreating ? () => removeCoin(index) : undefined, } as NativeCoinSelectorProps)} @@ -456,15 +446,7 @@ export const AuthzGrantRevokeComponent: ActionComponent< {...({ ...props, options: { - nativeBalances: balances.loading - ? { loading: true } - : { - loading: false, - data: balances.data.filter( - ({ token }) => - token.type === TokenType.Native - ), - }, + nativeBalances: balances, }, onRemove: isCreating ? () => removeCoin(index) diff --git a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/index.tsx b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/index.tsx index 6dd16aee7..318cf27cc 100644 --- a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/index.tsx +++ b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/index.tsx @@ -1,6 +1,7 @@ import { fromUtf8, toUtf8 } from '@cosmjs/encoding' import JSON5 from 'json5' import { useCallback } from 'react' +import { useFormContext } from 'react-hook-form' import { GenericAuthorization } from '@dao-dao/protobuf/codegen/cosmos/authz/v1beta1/authz' import { @@ -18,10 +19,16 @@ import { MaxCallsLimit, } from '@dao-dao/protobuf/codegen/cosmwasm/wasm/v1/authz' import { Any } from '@dao-dao/protobuf/codegen/google/protobuf/any' -import { KeyEmoji, Loader } from '@dao-dao/stateless' +import { + ChainPickerInput, + ChainProvider, + KeyEmoji, + Loader, +} from '@dao-dao/stateless' import { Coin } from '@dao-dao/types' import { ActionComponent, + ActionContextType, ActionKey, ActionMaker, UseDecodedCosmosMsg, @@ -31,14 +38,17 @@ import { import { convertDenomToMicroDenomWithDecimals, convertMicroDenomToDenomWithDecimals, + decodePolytoneExecuteMsg, getTokenForChainIdAndDenom, isDecodedStargateMsg, makeStargateMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' import { AddressInput, SuspenseLoader } from '../../../../components' import { useTokenBalances } from '../../../hooks' +import { useActionOptions } from '../../../react' import { AuthzGrantRevokeComponent as StatelessAuthzAuthorizationComponent } from './Component' import { ACTION_TYPES, @@ -48,51 +58,77 @@ import { LIMIT_TYPES, } from './types' -const useDefaults: UseDefaults = () => ({ - mode: 'grant', - authorizationTypeUrl: AUTHORIZATION_TYPES[0].type.typeUrl, - customTypeUrl: false, - grantee: '', - filterTypeUrl: FILTER_TYPES[0].type.typeUrl, - filterKeys: '', - filterMsgs: '{}', - funds: [], - contract: '', - calls: 10, - limitTypeUrl: LIMIT_TYPES[0].type.typeUrl, - msgTypeUrl: ACTION_TYPES[0].type.typeUrl, -}) - const Component: ActionComponent = (props) => { - const balances = useTokenBalances() + const balances = useTokenBalances({ + allChains: true, + }) + + const { context } = useActionOptions() + const { watch } = useFormContext() + const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId') return ( - } - forceFallback={ - // Manually trigger loader. - balances.loading - } - > - - + <> + {context.type === ActionContextType.Dao && ( + + )} + + + } + forceFallback={ + // Manually trigger loader. + balances.loading + } + > + + + + ) } export const makeAuthzGrantRevokeAction: ActionMaker = ({ t, address, - chain: { chain_id: chainId }, + chain: { chain_id: currentChainId }, }) => { + const useDefaults: UseDefaults = () => ({ + chainId: currentChainId, + mode: 'grant', + authorizationTypeUrl: AUTHORIZATION_TYPES[0].type.typeUrl, + customTypeUrl: false, + grantee: '', + filterTypeUrl: FILTER_TYPES[0].type.typeUrl, + filterKeys: '', + filterMsgs: '{}', + funds: [], + contract: '', + calls: 10, + limitTypeUrl: LIMIT_TYPES[0].type.typeUrl, + msgTypeUrl: ACTION_TYPES[0].type.typeUrl, + }) + const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( msg: Record ) => { + let chainId = currentChainId + const decodedPolytone = decodePolytoneExecuteMsg(chainId, msg) + if (decodedPolytone.match) { + chainId = decodedPolytone.chainId + msg = decodedPolytone.msg + } + const defaults = useDefaults() if ( @@ -130,6 +166,7 @@ export const makeAuthzGrantRevokeAction: ActionMaker = ({ match: true, data: { ...defaults, + chainId, mode: 'grant', authorizationTypeUrl, customTypeUrl: false, @@ -149,6 +186,7 @@ export const makeAuthzGrantRevokeAction: ActionMaker = ({ match: true, data: { ...defaults, + chainId, mode: 'grant', authorizationTypeUrl, customTypeUrl: false, @@ -213,6 +251,7 @@ export const makeAuthzGrantRevokeAction: ActionMaker = ({ match: true, data: { ...defaults, + chainId, mode: 'grant', authorizationTypeUrl, customTypeUrl: false, @@ -259,6 +298,7 @@ export const makeAuthzGrantRevokeAction: ActionMaker = ({ match: true, data: { ...defaults, + chainId, mode: 'revoke', customTypeUrl: false, grantee: msg.stargate.value.grantee, @@ -273,6 +313,7 @@ export const makeAuthzGrantRevokeAction: ActionMaker = ({ const useTransformToCosmos: UseTransformToCosmos = () => useCallback( ({ + chainId, mode, authorizationTypeUrl, grantee, @@ -356,25 +397,29 @@ export const makeAuthzGrantRevokeAction: ActionMaker = ({ // Encoder needs a whole number of seconds. expiration.setMilliseconds(0) - return makeStargateMessage({ - stargate: { - typeUrl: mode === 'grant' ? MsgGrant.typeUrl : MsgRevoke.typeUrl, - value: { - ...(mode === 'grant' && authorization - ? { - grant: { - authorization, - expiration, - }, - } - : { - msgTypeUrl, - }), - grantee, - granter: address, + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeStargateMessage({ + stargate: { + typeUrl: mode === 'grant' ? MsgGrant.typeUrl : MsgRevoke.typeUrl, + value: { + ...(mode === 'grant' && authorization + ? { + grant: { + authorization, + expiration, + }, + } + : { + msgTypeUrl, + }), + grantee, + granter: address, + }, }, - }, - }) + }) + ) }, [] ) diff --git a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/types.ts b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/types.ts index 87a8c233c..a60d531a2 100644 --- a/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/types.ts +++ b/packages/stateful/actions/core/authorizations/AuthzGrantRevoke/types.ts @@ -24,6 +24,7 @@ import { } from '@dao-dao/protobuf/codegen/cosmwasm/wasm/v1/tx' export type AuthzGrantRevokeData = { + chainId: string mode: 'grant' | 'revoke' authorizationTypeUrl: string customTypeUrl: boolean diff --git a/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.stories.tsx b/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.stories.tsx index 0441b5a0e..a4193bb0c 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.stories.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { ProposalStatus } from '@dao-dao/protobuf/codegen/cosmos/gov/v1beta1/gov' import { SoftwareUpgradeProposal } from '@dao-dao/protobuf/codegen/cosmos/upgrade/v1beta1/upgrade' -import { ReactHookFormDecorator } from '@dao-dao/storybook' +import { CHAIN_ID, ReactHookFormDecorator } from '@dao-dao/storybook' import { GovProposalVersion, GovProposalWithDecodedContent, @@ -72,6 +72,7 @@ Default.args = { allActionsWithData: [], index: 0, data: { + chainId: CHAIN_ID, proposalId: '', deposit: [ { diff --git a/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.tsx b/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.tsx index 2a1c16beb..52f5a29c2 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceDeposit/Component.tsx @@ -31,6 +31,7 @@ export type GovernanceDepositOptions = { } export type GovernanceDepositData = { + chainId: string proposalId: string deposit: { amount: number diff --git a/packages/stateful/actions/core/chain_governance/GovernanceDeposit/index.tsx b/packages/stateful/actions/core/chain_governance/GovernanceDeposit/index.tsx index 29da4b21c..cefbb30e6 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceDeposit/index.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceDeposit/index.tsx @@ -11,10 +11,17 @@ import { govProposalSelector, govProposalsSelector, } from '@dao-dao/state' -import { BankEmoji, Loader, useCachedLoading } from '@dao-dao/stateless' +import { + BankEmoji, + ChainPickerInput, + Loader, + useCachedLoading, + useChain, +} from '@dao-dao/stateless' import { TokenType } from '@dao-dao/types' import { ActionComponent, + ActionContextType, ActionKey, ActionMaker, UseDecodedCosmosMsg, @@ -22,8 +29,10 @@ import { UseTransformToCosmos, } from '@dao-dao/types/actions' import { + decodePolytoneExecuteMsg, isDecodedStargateMsg, makeStargateMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -36,28 +45,41 @@ import { GovernanceDepositComponent as StatelessGovernanceDepositComponent, } from './Component' -const useDefaults: UseDefaults = () => ({ - proposalId: '', - deposit: [], -}) - const Component: ActionComponent = ( props -) => ( - }> - - - - -) +) => { + const { context } = useActionOptions() + const { setValue } = useFormContext() + + return ( + <> + {context.type === ActionContextType.Dao && ( + { + // Clear fields on chain change. + setValue((props.fieldNamePrefix + 'proposalId') as 'proposalId', '') + setValue((props.fieldNamePrefix + 'deposit') as 'deposit', []) + }} + /> + )} + + }> + + + + + + ) +} const InnerComponent: ActionComponent = ( props ) => { const { isCreating, fieldNamePrefix } = props - const { - chain: { chain_id: chainId }, - } = useActionOptions() + const { chain_id: chainId } = useChain() const { watch, setValue, setError, clearErrors } = useFormContext() @@ -171,43 +193,61 @@ const InnerComponent: ActionComponent = ( export const makeGovernanceDepositAction: ActionMaker< GovernanceDepositData -> = ({ t, address }) => { +> = ({ t, address, chain: { chain_id: currentChainId } }) => { + const useDefaults: UseDefaults = () => ({ + chainId: currentChainId, + proposalId: '', + deposit: [], + }) + const useTransformToCosmos: UseTransformToCosmos< GovernanceDepositData > = () => useCallback( - ({ proposalId, deposit }) => - makeStargateMessage({ - stargate: { - typeUrl: MsgDeposit.typeUrl, - value: { - proposalId: BigInt(proposalId || '-1'), - depositor: address, - amount: deposit.map(({ denom, amount }) => ({ - denom, - amount: BigInt(amount).toString(), - })), - } as MsgDeposit, - }, - }), + ({ chainId, proposalId, deposit }) => + maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeStargateMessage({ + stargate: { + typeUrl: MsgDeposit.typeUrl, + value: { + proposalId: BigInt(proposalId || '0'), + depositor: address, + amount: deposit.map(({ denom, amount }) => ({ + denom, + amount: BigInt(amount).toString(), + })), + } as MsgDeposit, + }, + }) + ), [] ) const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( msg: Record - ) => - isDecodedStargateMsg(msg) && - objectMatchesStructure(msg.stargate.value, { - proposalId: {}, - depositor: {}, - amount: {}, - }) && - // Make sure this is a deposit message. - msg.stargate.typeUrl === MsgDeposit.typeUrl && - msg.stargate.value.depositor === address + ) => { + let chainId = currentChainId + const decodedPolytone = decodePolytoneExecuteMsg(chainId, msg) + if (decodedPolytone.match) { + chainId = decodedPolytone.chainId + msg = decodedPolytone.msg + } + + return isDecodedStargateMsg(msg) && + objectMatchesStructure(msg.stargate.value, { + proposalId: {}, + depositor: {}, + amount: {}, + }) && + // Make sure this is a deposit message. + msg.stargate.typeUrl === MsgDeposit.typeUrl && + msg.stargate.value.depositor === address ? { match: true, data: { + chainId, proposalId: msg.stargate.value.proposalId.toString(), deposit: (msg.stargate.value.amount as Coin[]).map( ({ denom, amount }) => ({ @@ -220,6 +260,7 @@ export const makeGovernanceDepositAction: ActionMaker< : { match: false, } + } return { key: ActionKey.GovernanceDeposit, diff --git a/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.stories.tsx b/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.stories.tsx index 186099fd8..382c48b3b 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.stories.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { SoftwareUpgradeProposal } from '@dao-dao/protobuf/codegen/cosmos/upgrade/v1beta1/upgrade' import { Any } from '@dao-dao/protobuf/codegen/google/protobuf/any' -import { ReactHookFormDecorator } from '@dao-dao/storybook' +import { CHAIN_ID, ReactHookFormDecorator } from '@dao-dao/storybook' import { GovProposalVersion } from '@dao-dao/types' import { @@ -30,6 +30,7 @@ Default.args = { allActionsWithData: [], index: 0, data: { + chainId: CHAIN_ID, version: GovProposalVersion.V1_BETA_1, title: 'Upgrade to v10 Alpha 1', description: @@ -71,7 +72,6 @@ Default.args = { errors: {}, options: { govModuleAddress: '', - supportsV1GovProposals: true, minDeposits: { loading: false, data: [] }, TokenAmountDisplay, AddressInput, diff --git a/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.tsx b/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.tsx index 7273e4367..272f0db20 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceProposal/Component.tsx @@ -25,6 +25,7 @@ import { TextAreaInput, TextInput, TokenInput, + useSupportedChainContext, } from '@dao-dao/stateless' import { AddressInputProps, @@ -47,7 +48,6 @@ import { convertMicroDenomToDenomWithDecimals, getChainAssets, getFundsUsedInCwMessage, - getNativeTokenForChainId, govProposalActionDataToDecodedContent, isCosmWasmStargateMsg, makeValidateAddress, @@ -56,12 +56,10 @@ import { validateRequired, } from '@dao-dao/utils' -import { useActionOptions } from '../../../react' import { CommunityPoolTransferData } from '../../treasury/CommunityPoolTransfer/Component' export type GovernanceProposalOptions = { govModuleAddress: string - supportsV1GovProposals: boolean minDeposits: LoadingData TokenAmountDisplay: ComponentType AddressInput: ComponentType> @@ -78,7 +76,6 @@ export const GovernanceProposalComponent: ActionComponent< isCreating, options: { govModuleAddress, - supportsV1GovProposals, minDeposits, GovProposalActionDisplay, TokenAmountDisplay, @@ -92,10 +89,11 @@ export const GovernanceProposalComponent: ActionComponent< useFormContext() const { - address, - chain: { chain_id: chainId, bech32_prefix: bech32Prefix }, - } = useActionOptions() - const nativeToken = getNativeTokenForChainId(chainId) + chainId, + chain: { bech32_prefix: bech32Prefix }, + config: { supportsV1GovProposals }, + nativeToken, + } = useSupportedChainContext() const selectedMinDepositToken = minDeposits.loading ? undefined @@ -114,10 +112,11 @@ export const GovernanceProposalComponent: ActionComponent< const availableTokens: GenericToken[] = [ // First native. - nativeToken, + ...(nativeToken ? [nativeToken] : []), // Then the chain assets. ...getChainAssets(chainId).filter( - ({ denomOrAddress }) => denomOrAddress !== nativeToken.denomOrAddress + ({ denomOrAddress }) => + !nativeToken || denomOrAddress !== nativeToken.denomOrAddress ), ] @@ -129,7 +128,7 @@ export const GovernanceProposalComponent: ActionComponent< // If any of these fields change, we need to re-upload the metadata. useEffect(() => { setMetadataUploaded(false) - }, [title, description, address]) + }, [title, description]) const uploadMetadata = async () => { setUploadingMetadata(true) try { @@ -589,7 +588,7 @@ export const GovernanceProposalComponent: ActionComponent< onClick={() => appendSpend({ amount: 1, - denom: nativeToken.denomOrAddress, + denom: nativeToken?.denomOrAddress || '', }) } variant="secondary" diff --git a/packages/stateful/actions/core/chain_governance/GovernanceProposal/index.tsx b/packages/stateful/actions/core/chain_governance/GovernanceProposal/index.tsx index 016609eef..cb4263b6e 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceProposal/index.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceProposal/index.tsx @@ -1,5 +1,6 @@ import { Coin } from '@cosmjs/stargate' -import { useCallback } from 'react' +import { useEffect } from 'react' +import { useFormContext } from 'react-hook-form' import { useRecoilValue, waitForAll } from 'recoil' import { CommunityPoolSpendProposal } from '@dao-dao/protobuf/codegen/cosmos/distribution/v1beta1/distribution' @@ -14,9 +15,17 @@ import { govParamsSelector, moduleAddressSelector, } from '@dao-dao/state' -import { Loader, RaisedHandEmoji, useCachedLoading } from '@dao-dao/stateless' +import { + ChainPickerInput, + ChainProvider, + Loader, + RaisedHandEmoji, + useCachedLoading, + useChain, +} from '@dao-dao/stateless' import { ActionComponent, + ActionContextType, ActionKey, ActionMaker, GOVERNANCE_PROPOSAL_TYPES, @@ -28,12 +37,13 @@ import { UseTransformToCosmos, } from '@dao-dao/types' import { - convertDenomToMicroDenomWithDecimals, cwMsgToProtobuf, decodeGovProposalV1Messages, + decodePolytoneExecuteMsg, getNativeTokenForChainId, isDecodedStargateMsg, makeStargateMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -50,24 +60,44 @@ import { GovernanceProposalComponent as StatelessGovernanceProposalComponent } f const Component: ActionComponent = ( props -) => ( - }> - - - - -) +) => { + const { watch } = useFormContext() + const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId') + const options = useActionOptions() + + return ( + <> + {options.context.type === ActionContextType.Dao && ( + + )} + + + } + > + + + + + + + ) +} const InnerComponent: ActionComponent< undefined, GovernanceProposalActionData > = (props) => { - const { - chain: { chain_id: chainId }, - chainContext: { - config: { supportsV1GovProposals }, - }, - } = useActionOptions() + const { chain_id: chainId } = useChain() + const { setValue } = useFormContext() const govModuleAddress = useRecoilValue( moduleAddressSelector({ @@ -82,6 +112,18 @@ const InnerComponent: ActionComponent< undefined ) + // On chain change, reset deposit. + useEffect(() => { + if (!govParams.loading && !govParams.updating && govParams.data) { + setValue((props.fieldNamePrefix + 'deposit') as 'deposit', [ + { + denom: govParams.data.minDeposit[0].denom, + amount: Number(govParams.data.minDeposit[0].amount), + }, + ]) + } + }, [chainId, setValue, props.fieldNamePrefix, govParams]) + const minDeposits = useCachedLoading( govParams.loading || !govParams.data ? undefined @@ -106,7 +148,6 @@ const InnerComponent: ActionComponent< {...props} options={{ govModuleAddress, - supportsV1GovProposals, minDeposits: minDeposits.loading || govParams.loading || !govParams.data ? { loading: true } @@ -169,7 +210,8 @@ export const makeGovernanceProposalAction: ActionMaker< > = ({ t, address, - chain: { chain_id: chainId }, + context, + chain: { chain_id: currentChainId }, chainContext: { config: { supportsV1GovProposals }, }, @@ -177,34 +219,18 @@ export const makeGovernanceProposalAction: ActionMaker< const useDefaults: UseDefaults = () => { const govParams = useCachedLoading( govParamsSelector({ - chainId, + chainId: currentChainId, }), undefined ) - const minDepositTokens = useCachedLoading( - govParams.loading || !govParams.data - ? undefined - : waitForAll( - govParams.data.minDeposit.map(({ denom }) => - genericTokenSelector({ - type: TokenType.Native, - denomOrAddress: denom, - chainId, - }) - ) - ), - [] - ) const deposit = govParams.loading || !govParams.data ? undefined : govParams.data.minDeposit[0] - const depositToken = minDepositTokens.loading - ? undefined - : minDepositTokens.data[0] return { + chainId: currentChainId, version: supportsV1GovProposals ? GovProposalVersion.V1 : GovProposalVersion.V1_BETA_1, @@ -219,23 +245,13 @@ export const makeGovernanceProposalAction: ActionMaker< ] : [ { - denom: getNativeTokenForChainId(chainId).denomOrAddress, + denom: getNativeTokenForChainId(currentChainId).denomOrAddress, amount: 0, }, ], legacy: { typeUrl: TextProposal.typeUrl, - spends: deposit - ? [ - { - denom: deposit.denom, - amount: convertDenomToMicroDenomWithDecimals( - 1000, - depositToken?.decimals ?? 0 - ), - }, - ] - : [], + spends: [], spendRecipient: address, parameterChanges: defaultParameterChanges, upgradePlan: defaultPlan, @@ -250,66 +266,98 @@ export const makeGovernanceProposalAction: ActionMaker< const useTransformToCosmos: UseTransformToCosmos< GovernanceProposalActionData > = () => { - const govModuleAddress = useRecoilValue( - moduleAddressSelector({ - name: 'gov', - chainId, - }) - ) - - return useCallback( - ({ - version, - title, - description, - deposit, - legacyContent, - msgs, - metadataCid, - }) => { - if (version === GovProposalVersion.V1_BETA_1) { - return makeStargateMessage({ - stargate: { - typeUrl: MsgSubmitProposalV1Beta1.typeUrl, - value: { - content: legacyContent, - initialDeposit: deposit.map(({ amount, denom }) => ({ - amount: BigInt(amount).toString(), - denom, - })), - proposer: address, - } as MsgSubmitProposalV1Beta1, - }, + const chainIds = [ + currentChainId, + ...(context.type === ActionContextType.Dao + ? Object.keys(context.info.polytoneProxies) + : []), + ] + // Map chain ID to module address. + const govModuleAddressPerChain = useRecoilValue( + waitForAll( + chainIds.map((chainId) => + moduleAddressSelector({ + name: 'gov', + chainId, }) - } else { - return makeStargateMessage({ - stargate: { - typeUrl: MsgSubmitProposalV1.typeUrl, - value: { - messages: msgs.map((msg) => - cwMsgToProtobuf(msg, govModuleAddress) - ), - initialDeposit: deposit.map(({ amount, denom }) => ({ - amount: BigInt(amount).toString(), - denom, - })), - proposer: address, - metadata: `ipfs://${metadataCid}`, - title, - summary: description, - expedited: false, - } as MsgSubmitProposalV1, - }, - }) - } - }, - [govModuleAddress] + ) + ) + ).reduce( + (acc, moduleAddress, index) => ({ + ...acc, + [chainIds[index]]: moduleAddress, + }), + {} as Record ) + + return ({ + chainId, + version, + title, + description, + deposit, + legacyContent, + msgs, + metadataCid, + }) => { + const govModuleAddress = govModuleAddressPerChain[chainId] + if (!govModuleAddress) { + throw new Error( + `Could not find gov module address for chain ID ${chainId}.` + ) + } + + let msg + if (version === GovProposalVersion.V1_BETA_1) { + msg = makeStargateMessage({ + stargate: { + typeUrl: MsgSubmitProposalV1Beta1.typeUrl, + value: { + content: legacyContent, + initialDeposit: deposit.map(({ amount, denom }) => ({ + amount: BigInt(amount).toString(), + denom, + })), + proposer: address, + } as MsgSubmitProposalV1Beta1, + }, + }) + } else { + msg = makeStargateMessage({ + stargate: { + typeUrl: MsgSubmitProposalV1.typeUrl, + value: { + messages: msgs.map((msg) => + cwMsgToProtobuf(msg, govModuleAddress) + ), + initialDeposit: deposit.map(({ amount, denom }) => ({ + amount: BigInt(amount).toString(), + denom, + })), + proposer: address, + metadata: `ipfs://${metadataCid}`, + title, + summary: description, + expedited: false, + } as MsgSubmitProposalV1, + }, + }) + } + + return maybeMakePolytoneExecuteMessage(currentChainId, chainId, msg) + } } const useDecodedCosmosMsg: UseDecodedCosmosMsg< GovernanceProposalActionData > = (msg: Record) => { + let chainId = currentChainId + const decodedPolytone = decodePolytoneExecuteMsg(chainId, msg) + if (decodedPolytone.match) { + chainId = decodedPolytone.chainId + msg = decodedPolytone.msg + } + const defaults = useDefaults() if ( @@ -351,6 +399,7 @@ export const makeGovernanceProposalAction: ActionMaker< match: true, data: { ...defaults, + chainId, version: GovProposalVersion.V1_BETA_1, title: proposal.content.title, description: proposal.content.description, @@ -396,6 +445,7 @@ export const makeGovernanceProposalAction: ActionMaker< match: true, data: { ...defaults, + chainId, version: GovProposalVersion.V1, title: proposal.title, description: proposal.summary, diff --git a/packages/stateful/actions/core/chain_governance/GovernanceVote/Component.tsx b/packages/stateful/actions/core/chain_governance/GovernanceVote/Component.tsx index 2c86acef0..96d326b09 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceVote/Component.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceVote/Component.tsx @@ -44,7 +44,8 @@ export interface GovernanceVoteOptions { GovProposalActionDisplay: ComponentType } -export interface GovernanceVoteData { +export type GovernanceVoteData = { + chainId: string proposalId: string vote: VoteOption } diff --git a/packages/stateful/actions/core/chain_governance/GovernanceVote/index.tsx b/packages/stateful/actions/core/chain_governance/GovernanceVote/index.tsx index 43bae5da1..c8fe91d6e 100644 --- a/packages/stateful/actions/core/chain_governance/GovernanceVote/index.tsx +++ b/packages/stateful/actions/core/chain_governance/GovernanceVote/index.tsx @@ -12,9 +12,15 @@ import { govProposalVoteSelector, govProposalsSelector, } from '@dao-dao/state' -import { BallotDepositEmoji, Loader } from '@dao-dao/stateless' +import { + BallotDepositEmoji, + ChainPickerInput, + ChainProvider, + Loader, +} from '@dao-dao/stateless' import { ActionComponent, + ActionContextType, ActionKey, ActionMaker, UseDecodedCosmosMsg, @@ -23,14 +29,19 @@ import { } from '@dao-dao/types/actions' import { cwVoteOptionToGovVoteOption, + decodePolytoneExecuteMsg, + getChainAddressForActionOptions, govVoteOptionToCwVoteOption, isDecodedStargateMsg, loadableToLoadingData, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' -import { GovProposalActionDisplay } from '../../../../components' -import { SuspenseLoader } from '../../../../components/SuspenseLoader' +import { + GovProposalActionDisplay, + SuspenseLoader, +} from '../../../../components' import { TokenAmountDisplay } from '../../../../components/TokenAmountDisplay' import { GovActionsProvider, useActionOptions } from '../../../react' import { @@ -38,35 +49,18 @@ import { GovernanceVoteComponent as StatelessGovernanceVoteComponent, } from './Component' -const useDefaults: UseDefaults = () => ({ - proposalId: '', - vote: VoteOption.VOTE_OPTION_ABSTAIN, -}) - -const Component: ActionComponent = (props) => ( - }> - - - - -) - -const InnerComponent: ActionComponent = ( - props -) => { +const Component: ActionComponent = (props) => { const { isCreating, fieldNamePrefix } = props - const { - address, - chain: { chain_id: chainId }, - } = useActionOptions() + const options = useActionOptions() const { watch, setValue, setError, clearErrors } = useFormContext() + const chainId = watch((fieldNamePrefix + 'chainId') as 'chainId') const proposalId = watch( (props.fieldNamePrefix + 'proposalId') as 'proposalId' ) - const openProposals = useRecoilValue( + const openProposalsLoadable = useRecoilValueLoadable( isCreating ? govProposalsSelector({ status: ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD, @@ -77,14 +71,17 @@ const InnerComponent: ActionComponent = ( // Prevent action from being submitted if there are no open proposals. useEffect(() => { - if (openProposals && openProposals.proposals.length === 0) { + if ( + openProposalsLoadable.state === 'hasValue' && + openProposalsLoadable.contents?.proposals.length === 0 + ) { setError((fieldNamePrefix + 'proposalId') as 'proposalId', { type: 'manual', }) } else { clearErrors((fieldNamePrefix + 'proposalId') as 'proposalId') } - }, [openProposals, setError, clearErrors, fieldNamePrefix]) + }, [openProposalsLoadable, setError, clearErrors, fieldNamePrefix]) // If viewing an action where we already selected and voted on a proposal, // load just the one we voted on and add it to the list so we can display it. @@ -102,7 +99,7 @@ const InnerComponent: ActionComponent = ( proposalId ? govProposalVoteSelector({ proposalId: Number(proposalId), - voter: address, + voter: getChainAddressForActionOptions(options, chainId), chainId, }) : constSelector(undefined) @@ -112,65 +109,112 @@ const InnerComponent: ActionComponent = ( // Select first proposal once loaded if nothing selected. useEffect(() => { - if (isCreating && openProposals?.proposals.length && !proposalId) { + if ( + isCreating && + openProposalsLoadable.state === 'hasValue' && + openProposalsLoadable.contents?.proposals.length && + !proposalId + ) { setValue( (fieldNamePrefix + 'proposalId') as 'proposalId', - openProposals.proposals[0].id.toString() + openProposalsLoadable.contents.proposals[0].id.toString() ) } - }, [isCreating, openProposals, proposalId, setValue, fieldNamePrefix]) + }, [isCreating, openProposalsLoadable, proposalId, setValue, fieldNamePrefix]) return ( - + <> + {options.context.type === ActionContextType.Dao && ( + + // Clear proposal on chain change. + setValue((fieldNamePrefix + 'proposalId') as 'proposalId', '') + } + /> + )} + + } + forceFallback={openProposalsLoadable.state !== 'hasValue'} + > + + + + + + + ) } export const makeGovernanceVoteAction: ActionMaker = ({ t, + chain: { chain_id: currentChainId }, }) => { + const useDefaults: UseDefaults = () => ({ + chainId: currentChainId, + proposalId: '', + vote: VoteOption.VOTE_OPTION_ABSTAIN, + }) + const useTransformToCosmos: UseTransformToCosmos = () => useCallback( - ({ proposalId, vote }) => ({ - gov: { - vote: { - proposal_id: Number(proposalId || '-1'), - vote: govVoteOptionToCwVoteOption(vote), + ({ chainId, proposalId, vote }) => + maybeMakePolytoneExecuteMessage(currentChainId, chainId, { + gov: { + vote: { + proposal_id: Number(proposalId || '-1'), + vote: govVoteOptionToCwVoteOption(vote), + }, }, - }, - }), + }), [] ) const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( msg: Record - ) => - isDecodedStargateMsg(msg) && - objectMatchesStructure(msg.stargate.value, { - proposalId: {}, - voter: {}, - option: {}, - }) && - // Make sure this is a vote message. - msg.stargate.typeUrl === MsgVote.typeUrl + ) => { + let chainId = currentChainId + const decodedPolytone = decodePolytoneExecuteMsg(chainId, msg) + if (decodedPolytone.match) { + chainId = decodedPolytone.chainId + msg = decodedPolytone.msg + } + + return isDecodedStargateMsg(msg) && + objectMatchesStructure(msg.stargate.value, { + proposalId: {}, + voter: {}, + option: {}, + }) && + // If vote Stargate message. + msg.stargate.typeUrl === MsgVote.typeUrl ? { match: true, data: { + chainId, proposalId: msg.stargate.value.proposalId.toString(), vote: msg.stargate.value.option, }, } - : objectMatchesStructure(msg, { + : // If vote gov CosmWasm message. + objectMatchesStructure(msg, { gov: { vote: { proposal_id: {}, @@ -181,6 +225,7 @@ export const makeGovernanceVoteAction: ActionMaker = ({ ? { match: true, data: { + chainId, proposalId: msg.gov.vote.proposal_id.toString(), vote: cwVoteOptionToGovVoteOption(msg.gov.vote.vote), }, @@ -188,6 +233,7 @@ export const makeGovernanceVoteAction: ActionMaker = ({ : { match: false, } + } return { key: ActionKey.GovernanceVote, diff --git a/packages/stateful/actions/core/chain_governance/ValidatorActions/Component.tsx b/packages/stateful/actions/core/chain_governance/ValidatorActions/Component.tsx index 274508b98..f2c1f1cbb 100644 --- a/packages/stateful/actions/core/chain_governance/ValidatorActions/Component.tsx +++ b/packages/stateful/actions/core/chain_governance/ValidatorActions/Component.tsx @@ -14,8 +14,9 @@ import { InputLabel, SelectInput, } from '@dao-dao/stateless' -import { ActionComponent, ActionContextType } from '@dao-dao/types/actions' +import { ActionComponent } from '@dao-dao/types/actions' import { + getChainAddressForActionOptions, getChainForChainId, getNativeTokenForChainId, toValidatorAddress, @@ -56,11 +57,7 @@ export const ValidatorActionsComponent: ActionComponent = ({ isCreating, }) => { const { t } = useTranslation() - const { - chain: { chain_id: currentChainId }, - address: _address, - context, - } = useActionOptions() + const options = useActionOptions() const { control, register, watch, getValues, setValue } = useFormContext() @@ -70,10 +67,7 @@ export const ValidatorActionsComponent: ActionComponent = ({ ) const updateChainValues = (chainId: string, typeUrl: string) => { - const address = - context.type === ActionContextType.Dao && currentChainId !== chainId - ? context.info.polytoneProxies[chainId] || '' - : _address + const address = getChainAddressForActionOptions(options, chainId) const validatorAddress = address && toValidatorAddress(address, getChainForChainId(chainId).bech32_prefix) diff --git a/packages/stateful/actions/core/chain_governance/ValidatorActions/index.tsx b/packages/stateful/actions/core/chain_governance/ValidatorActions/index.tsx index 3b8f8e0a9..3648f5472 100644 --- a/packages/stateful/actions/core/chain_governance/ValidatorActions/index.tsx +++ b/packages/stateful/actions/core/chain_governance/ValidatorActions/index.tsx @@ -11,7 +11,6 @@ import { } from '@dao-dao/protobuf/codegen/cosmos/staking/v1beta1/tx' import { PickEmoji } from '@dao-dao/stateless' import { - ActionContextType, ActionKey, ActionMaker, UseDecodedCosmosMsg, @@ -20,11 +19,12 @@ import { } from '@dao-dao/types/actions' import { decodePolytoneExecuteMsg, + getChainAddressForActionOptions, getChainForChainId, getNativeTokenForChainId, isDecodedStargateMsg, - makePolytoneExecuteMessage, makeStargateMessage, + maybeMakePolytoneExecuteMessage, toValidatorAddress, } from '@dao-dao/utils' @@ -34,19 +34,17 @@ import { ValidatorActionsData, } from './Component' -export const makeValidatorActionsAction: ActionMaker = ({ - t, - address, - chain: { chain_id: currentChainId }, - context, -}) => { - const getAddress = (chainId: string) => - context.type === ActionContextType.Dao && currentChainId !== chainId - ? context.info.polytoneProxies[chainId] || '' - : address +export const makeValidatorActionsAction: ActionMaker = ( + options +) => { + const { + t, + chain: { chain_id: currentChainId }, + } = options + const getValidatorAddress = (chainId: string) => toValidatorAddress( - getAddress(chainId), + getChainAddressForActionOptions(options, chainId), getChainForChainId(chainId).bech32_prefix ) @@ -108,11 +106,7 @@ export const makeValidatorActionsAction: ActionMaker = ({ throw Error('Unrecogonized validator action type') } - if (chainId === currentChainId) { - return msg - } else { - return makePolytoneExecuteMessage(currentChainId, chainId, msg) - } + return maybeMakePolytoneExecuteMessage(currentChainId, chainId, msg) }, [] ) @@ -135,7 +129,10 @@ export const makeValidatorActionsAction: ActionMaker = ({ maxChangeRate: '100000000000000000', }, minSelfDelegation: '1', - delegatorAddress: getAddress(currentChainId), + delegatorAddress: getChainAddressForActionOptions( + options, + currentChainId + ), validatorAddress: getValidatorAddress(currentChainId), pubkey: { typeUrl: PubKey.typeUrl, @@ -179,7 +176,7 @@ export const makeValidatorActionsAction: ActionMaker = ({ msg = decodedPolytone.msg } - const thisAddress = getAddress(chainId) + const thisAddress = getChainAddressForActionOptions(options, chainId) const validatorAddress = getValidatorAddress(chainId) const data = useDefaults() diff --git a/packages/stateful/actions/core/dao_governance/CreateCrossChainAccount/index.tsx b/packages/stateful/actions/core/dao_governance/CreateCrossChainAccount/index.tsx index 44e5873ea..298cc1587 100644 --- a/packages/stateful/actions/core/dao_governance/CreateCrossChainAccount/index.tsx +++ b/packages/stateful/actions/core/dao_governance/CreateCrossChainAccount/index.tsx @@ -11,7 +11,7 @@ import { } from '@dao-dao/types' import { decodePolytoneExecuteMsg, - makePolytoneExecuteMessage, + maybeMakePolytoneExecuteMessage, } from '@dao-dao/utils' import { @@ -46,7 +46,7 @@ export const makeCreateCrossChainAccountAction: ActionMaker< CreateCrossChainAccountData > = () => useCallback( - ({ chainId }) => makePolytoneExecuteMessage(chain.chain_id, chainId), + ({ chainId }) => maybeMakePolytoneExecuteMessage(chain.chain_id, chainId), [] ) diff --git a/packages/stateful/actions/core/nfts/BurnNft/index.tsx b/packages/stateful/actions/core/nfts/BurnNft/index.tsx index ee9ddb47d..0f82bd590 100644 --- a/packages/stateful/actions/core/nfts/BurnNft/index.tsx +++ b/packages/stateful/actions/core/nfts/BurnNft/index.tsx @@ -17,8 +17,8 @@ import { import { combineLoadingDataWithErrors, decodePolytoneExecuteMsg, - makePolytoneExecuteMessage, makeWasmMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -37,25 +37,24 @@ const useTransformToCosmos: UseTransformToCosmos = () => { } = useActionOptions() return useCallback( - ({ chainId, collection, tokenId }: BurnNftData) => { - const msg = makeWasmMessage({ - wasm: { - execute: { - contract_addr: collection, - funds: [], - msg: { - burn: { - token_id: tokenId, + ({ chainId, collection, tokenId }: BurnNftData) => + maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeWasmMessage({ + wasm: { + execute: { + contract_addr: collection, + funds: [], + msg: { + burn: { + token_id: tokenId, + }, }, }, }, - }, - }) - - return chainId === currentChainId - ? msg - : makePolytoneExecuteMessage(currentChainId, chainId, msg) - }, + }) + ), [currentChainId] ) } diff --git a/packages/stateful/actions/core/nfts/MintNft/index.tsx b/packages/stateful/actions/core/nfts/MintNft/index.tsx index 5c678ed9e..a748352e7 100644 --- a/packages/stateful/actions/core/nfts/MintNft/index.tsx +++ b/packages/stateful/actions/core/nfts/MintNft/index.tsx @@ -20,8 +20,9 @@ import { } from '@dao-dao/types/actions' import { decodePolytoneExecuteMsg, - makePolytoneExecuteMessage, + getChainAddressForActionOptions, makeWasmMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -34,11 +35,7 @@ import { MintNftData } from './types' const Component: ActionComponent = (props) => { const { t } = useTranslation() - const { - context, - address, - chain: { chain_id: currentChainId }, - } = useActionOptions() + const options = useActionOptions() const { watch, register, setValue } = useFormContext() const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId') @@ -67,17 +64,13 @@ const Component: ActionComponent = (props) => { return ( <> - {context.type === ActionContextType.Dao && props.isCreating && ( + {options.context.type === ActionContextType.Dao && props.isCreating && ( { // Update minter and recipient to correct address. - const newAddress = - chainId === currentChainId - ? address - : // Use DAO's polytone proxy if exists. - context.info.polytoneProxies[chainId] || '' + const newAddress = getChainAddressForActionOptions(options, chainId) setValue( (props.fieldNamePrefix + @@ -182,21 +175,21 @@ export const makeMintNftAction: ActionMaker = ({ throw new Error(t('error.loadingData')) } - const msg = makeWasmMessage({ - wasm: { - execute: { - contract_addr: collectionAddress, - funds: [], - msg: { - mint: mintMsg, + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeWasmMessage({ + wasm: { + execute: { + contract_addr: collectionAddress, + funds: [], + msg: { + mint: mintMsg, + }, }, }, - }, - }) - - return chainId === currentChainId - ? msg - : makePolytoneExecuteMessage(currentChainId, chainId, msg) + }) + ) }, []) const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( diff --git a/packages/stateful/actions/core/nfts/TransferNft/index.tsx b/packages/stateful/actions/core/nfts/TransferNft/index.tsx index 045521859..0668eb126 100644 --- a/packages/stateful/actions/core/nfts/TransferNft/index.tsx +++ b/packages/stateful/actions/core/nfts/TransferNft/index.tsx @@ -18,8 +18,8 @@ import { import { combineLoadingDataWithErrors, decodePolytoneExecuteMsg, - makePolytoneExecuteMessage, makeWasmMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, parseEncodedMessage, } from '@dao-dao/utils' @@ -65,34 +65,33 @@ const useTransformToCosmos: UseTransformToCosmos = () => { recipient, executeSmartContract, smartContractMsg, - }: TransferNftData) => { - const msg = makeWasmMessage({ - wasm: { - execute: { - contract_addr: collection, - funds: [], - msg: executeSmartContract - ? { - send_nft: { - contract: recipient, - msg: toBase64(toUtf8(JSON.stringify(smartContractMsg))), - token_id: tokenId, + }: TransferNftData) => + maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeWasmMessage({ + wasm: { + execute: { + contract_addr: collection, + funds: [], + msg: executeSmartContract + ? { + send_nft: { + contract: recipient, + msg: toBase64(toUtf8(JSON.stringify(smartContractMsg))), + token_id: tokenId, + }, + } + : { + transfer_nft: { + recipient, + token_id: tokenId, + }, }, - } - : { - transfer_nft: { - recipient, - token_id: tokenId, - }, - }, + }, }, - }, - }) - - return chainId === currentChainId - ? msg - : makePolytoneExecuteMessage(currentChainId, chainId, msg) - }, + }) + ), [currentChainId] ) } diff --git a/packages/stateful/actions/core/smart_contracting/Execute/Component.tsx b/packages/stateful/actions/core/smart_contracting/Execute/Component.tsx index f51c1c622..d4ee08d5f 100644 --- a/packages/stateful/actions/core/smart_contracting/Execute/Component.tsx +++ b/packages/stateful/actions/core/smart_contracting/Execute/Component.tsx @@ -190,16 +190,7 @@ export const ExecuteComponent: ActionComponent = (props) => { {...({ ...props, options: { - nativeBalances: balances.loading - ? { loading: true } - : { - loading: false, - data: balances.data.filter( - ({ token }) => - token.chainId === chainId && - token.type === TokenType.Native - ), - }, + nativeBalances: balances, }, onRemove: props.isCreating ? () => removeCoin(index) diff --git a/packages/stateful/actions/core/smart_contracting/Execute/index.tsx b/packages/stateful/actions/core/smart_contracting/Execute/index.tsx index 8065617cc..1bc6347ff 100644 --- a/packages/stateful/actions/core/smart_contracting/Execute/index.tsx +++ b/packages/stateful/actions/core/smart_contracting/Execute/index.tsx @@ -27,8 +27,8 @@ import { decodePolytoneExecuteMsg, encodeMessageAsBase64, getTokenForChainIdAndDenom, - makePolytoneExecuteMessage, makeWasmMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, parseEncodedMessage, } from '@dao-dao/utils' @@ -122,11 +122,11 @@ const useTransformToCosmos: UseTransformToCosmos = () => { }) } - if (chainId === currentChainId) { - return executeMsg - } else { - return makePolytoneExecuteMessage(currentChainId, chainId, executeMsg) - } + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + executeMsg + ) }, [currentChainId, t, tokenBalances] ) diff --git a/packages/stateful/actions/core/smart_contracting/Instantiate/index.tsx b/packages/stateful/actions/core/smart_contracting/Instantiate/index.tsx index 8d8d558a3..f7329c726 100644 --- a/packages/stateful/actions/core/smart_contracting/Instantiate/index.tsx +++ b/packages/stateful/actions/core/smart_contracting/Instantiate/index.tsx @@ -25,9 +25,10 @@ import { convertDenomToMicroDenomWithDecimals, convertMicroDenomToDenomWithDecimals, decodePolytoneExecuteMsg, + getChainAddressForActionOptions, getNativeTokenForChainId, - makePolytoneExecuteMessage, makeWasmMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -65,33 +66,27 @@ const useTransformToCosmos: UseTransformToCosmos = () => { return } - const instantiateMsg = makeWasmMessage({ - wasm: { - instantiate: { - admin: admin || null, - code_id: codeId, - funds: funds.map(({ denom, amount }) => ({ - denom, - amount: convertDenomToMicroDenomWithDecimals( - amount, - getNativeTokenForChainId(chainId).decimals - ).toString(), - })), - label, - msg, + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeWasmMessage({ + wasm: { + instantiate: { + admin: admin || null, + code_id: codeId, + funds: funds.map(({ denom, amount }) => ({ + denom, + amount: convertDenomToMicroDenomWithDecimals( + amount, + getNativeTokenForChainId(chainId).decimals + ).toString(), + })), + label, + msg, + }, }, - }, - }) - - if (chainId === currentChainId) { - return instantiateMsg - } else { - return makePolytoneExecuteMessage( - currentChainId, - chainId, - instantiateMsg - ) - } + }) + ) }, [currentChainId] ) @@ -151,11 +146,12 @@ const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( } const Component: ActionComponent = (props) => { + const options = useActionOptions() const { context, address, chain: { chain_id: currentChainId }, - } = useActionOptions() + } = options const { watch, setValue } = useFormContext() const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId') @@ -295,9 +291,7 @@ const Component: ActionComponent = (props) => { setValue((props.fieldNamePrefix + 'funds') as 'funds', []) setValue( (props.fieldNamePrefix + 'admin') as 'admin', - chainId === currentChainId - ? address - : context.info.polytoneProxies[chainId] ?? '' + getChainAddressForActionOptions(options, chainId) ) }} /> diff --git a/packages/stateful/actions/core/smart_contracting/Migrate/index.tsx b/packages/stateful/actions/core/smart_contracting/Migrate/index.tsx index 1f68f3fd8..1d0e647d1 100644 --- a/packages/stateful/actions/core/smart_contracting/Migrate/index.tsx +++ b/packages/stateful/actions/core/smart_contracting/Migrate/index.tsx @@ -16,8 +16,8 @@ import { } from '@dao-dao/types/actions' import { decodePolytoneExecuteMsg, - makePolytoneExecuteMessage, makeWasmMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -72,11 +72,11 @@ const useTransformToCosmos: UseTransformToCosmos = () => { }, }) - if (chainId === currentChainId) { - return migrateMsg - } else { - return makePolytoneExecuteMessage(currentChainId, chainId, migrateMsg) - } + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + migrateMsg + ) }, [currentChainId] ) diff --git a/packages/stateful/actions/core/smart_contracting/UpdateAdmin/index.tsx b/packages/stateful/actions/core/smart_contracting/UpdateAdmin/index.tsx index 4f912e24f..c408f315e 100644 --- a/packages/stateful/actions/core/smart_contracting/UpdateAdmin/index.tsx +++ b/packages/stateful/actions/core/smart_contracting/UpdateAdmin/index.tsx @@ -21,7 +21,7 @@ import { decodePolytoneExecuteMsg, getChainForChainId, isValidContractAddress, - makePolytoneExecuteMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -50,22 +50,15 @@ const useTransformToCosmos: UseTransformToCosmos = () => { const currentChainId = useActionOptions().chain.chain_id return useCallback( - ({ chainId, contract, newAdmin }: UpdateAdminData) => { - const updateMsg = { + ({ chainId, contract, newAdmin }: UpdateAdminData) => + maybeMakePolytoneExecuteMessage(currentChainId, chainId, { wasm: { update_admin: { contract_addr: contract, admin: newAdmin, }, }, - } - - if (chainId === currentChainId) { - return updateMsg - } else { - return makePolytoneExecuteMessage(currentChainId, chainId, updateMsg) - } - }, + }), [currentChainId] ) } diff --git a/packages/stateful/actions/core/treasury/CommunityPoolDeposit/index.tsx b/packages/stateful/actions/core/treasury/CommunityPoolDeposit/index.tsx index bf1540352..b21cb1eca 100644 --- a/packages/stateful/actions/core/treasury/CommunityPoolDeposit/index.tsx +++ b/packages/stateful/actions/core/treasury/CommunityPoolDeposit/index.tsx @@ -19,8 +19,8 @@ import { convertMicroDenomToDenomWithDecimals, decodePolytoneExecuteMsg, isDecodedStargateMsg, - makePolytoneExecuteMessage, makeStargateMessage, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -110,19 +110,19 @@ export const makeCommunityPoolDepositAction: ActionMaker< token.decimals ) - const msg = makeStargateMessage({ - stargate: { - typeUrl: MsgFundCommunityPool.typeUrl, - value: MsgFundCommunityPool.fromPartial({ - depositor: address, - amount: coins(microAmount, denom), - }), - }, - }) - - return chainId === currentChainId - ? msg - : makePolytoneExecuteMessage(currentChainId, chainId, msg) + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeStargateMessage({ + stargate: { + typeUrl: MsgFundCommunityPool.typeUrl, + value: MsgFundCommunityPool.fromPartial({ + depositor: address, + amount: coins(microAmount, denom), + }), + }, + }) + ) }, [tokens] ) diff --git a/packages/stateful/actions/core/treasury/ManageStaking/index.tsx b/packages/stateful/actions/core/treasury/ManageStaking/index.tsx index 793dd7056..378360fc6 100644 --- a/packages/stateful/actions/core/treasury/ManageStaking/index.tsx +++ b/packages/stateful/actions/core/treasury/ManageStaking/index.tsx @@ -31,9 +31,10 @@ import { convertDenomToMicroDenomWithDecimals, convertMicroDenomToDenomWithDecimals, decodePolytoneExecuteMsg, + getChainAddressForActionOptions, getNativeTokenForChainId, - makePolytoneExecuteMessage, makeStakingActionMessage, + maybeMakePolytoneExecuteMessage, } from '@dao-dao/utils' import { AddressInput } from '../../../../components/AddressInput' @@ -64,20 +65,19 @@ const useTransformToCosmos: UseTransformToCosmos = () => { amount, nativeToken.decimals ) - const msg = makeStakingActionMessage( - stakeType, - microAmount.toString(), - nativeToken.denomOrAddress, - validator, - toValidator, - withdrawAddress - ) - if (chainId === currentChainId) { - return msg - } else { - return makePolytoneExecuteMessage(currentChainId, chainId, msg) - } + return maybeMakePolytoneExecuteMessage( + currentChainId, + chainId, + makeStakingActionMessage( + stakeType, + microAmount.toString(), + nativeToken.denomOrAddress, + validator, + toValidator, + withdrawAddress + ) + ) }, [currentChainId] ) @@ -177,11 +177,11 @@ const useDecodedCosmosMsg: UseDecodedCosmosMsg = ( const InnerComponent: ActionComponent = (props) => { const { t } = useTranslation() - const { address: _address, context, chain } = useActionOptions() + const options = useActionOptions() const { watch } = useFormContext() const { - chain: { chain_id: currentChainId }, + chain: { chain_id: chainId }, nativeToken, } = useChainContext() @@ -189,10 +189,7 @@ const InnerComponent: ActionComponent = (props) => { throw new Error(t('error.missingNativeToken')) } - const address = - context.type === ActionContextType.Dao && currentChainId !== chain.chain_id - ? context.info.polytoneProxies[currentChainId] || '' - : _address + const address = getChainAddressForActionOptions(options, chainId) // These need to be loaded via cached loadables to avoid displaying a loader // when this data updates on a schedule. Manually trigger a suspense loader @@ -218,7 +215,7 @@ const InnerComponent: ActionComponent = (props) => { const loadingNativeDelegationInfo = useCachedLoading( nativeDelegationInfoSelector({ - chainId: currentChainId, + chainId, address, }), { @@ -229,14 +226,14 @@ const InnerComponent: ActionComponent = (props) => { const loadingValidators = useCachedLoading( validatorsSelector({ - chainId: currentChainId, + chainId, }), [] ) const nativeUnstakingDurationSeconds = useCachedLoading( nativeUnstakingDurationSecondsSelector({ - chainId: currentChainId, + chainId, }), -1 ) diff --git a/packages/stateful/actions/core/treasury/Spend/index.tsx b/packages/stateful/actions/core/treasury/Spend/index.tsx index f23fa1ff7..5138479e3 100644 --- a/packages/stateful/actions/core/treasury/Spend/index.tsx +++ b/packages/stateful/actions/core/treasury/Spend/index.tsx @@ -32,10 +32,10 @@ import { isValidBech32Address, isValidContractAddress, makeBankMessage, - makePolytoneExecuteMessage, makeStargateMessage, makeWasmMessage, maybeGetNativeTokenForChainId, + maybeMakePolytoneExecuteMessage, objectMatchesStructure, transformBech32Address, } from '@dao-dao/utils' @@ -215,11 +215,7 @@ const useTransformToCosmos: UseTransformToCosmos = () => { throw new Error(`Unknown token type: ${token.type}`) } - if (fromChainId === currentChainId) { - return msg - } else { - return makePolytoneExecuteMessage(currentChainId, fromChainId, msg) - } + return maybeMakePolytoneExecuteMessage(currentChainId, fromChainId, msg) }, [address, context, currentChainId, loadingTokenBalances] ) diff --git a/packages/stateless/components/actions/NativeCoinSelector.tsx b/packages/stateless/components/actions/NativeCoinSelector.tsx index 3e737b61d..f0e8d0c03 100644 --- a/packages/stateless/components/actions/NativeCoinSelector.tsx +++ b/packages/stateless/components/actions/NativeCoinSelector.tsx @@ -8,6 +8,7 @@ import { ActionComponent, GenericTokenBalance, LoadingData, + TokenType, } from '@dao-dao/types' import { convertDenomToMicroDenomWithDecimals, @@ -48,7 +49,9 @@ export const NativeCoinSelector = ({ ? undefined : nativeBalances.data.find( ({ token }) => - token.chainId === chainId && token.denomOrAddress === watchDenom + token.type === TokenType.Native && + token.chainId === chainId && + token.denomOrAddress === watchDenom ) const validatePossibleSpend = useCallback( @@ -58,7 +61,10 @@ export const NativeCoinSelector = ({ } const native = nativeBalances.data.find( - ({ token }) => token.chainId === chainId && token.denomOrAddress === id + ({ token }) => + token.type === TokenType.Native && + token.chainId === chainId && + token.denomOrAddress === id ) if (native) { const microAmount = convertDenomToMicroDenomWithDecimals( @@ -166,7 +172,11 @@ export const NativeCoinSelector = ({ : { loading: false, data: nativeBalances.data - .filter(({ token }) => token.chainId === chainId) + .filter( + ({ token }) => + token.type === TokenType.Native && + token.chainId === chainId + ) .map(({ token }) => token), } } diff --git a/packages/stateless/components/inputs/TokenInput.tsx b/packages/stateless/components/inputs/TokenInput.tsx index 7fa312bee..2d40f547d 100644 --- a/packages/stateless/components/inputs/TokenInput.tsx +++ b/packages/stateless/components/inputs/TokenInput.tsx @@ -175,11 +175,21 @@ export const TokenInput = <

{readOnly ? t('info.token', { count: amount }) + : disabled + ? t('info.noTokenSelected') : t('button.selectToken')}

) ), - [amount, readOnly, selectedToken, showChainImage, t, tokenFallback] + [ + amount, + disabled, + readOnly, + selectedToken, + showChainImage, + t, + tokenFallback, + ] ) const selectDisabled = // Disable if there is only one token to choose from. diff --git a/packages/types/gov.ts b/packages/types/gov.ts index 8a033fc40..b019b0f8e 100644 --- a/packages/types/gov.ts +++ b/packages/types/gov.ts @@ -154,6 +154,7 @@ export const GOVERNANCE_PROPOSAL_TYPES = [ export const GOVERNANCE_PROPOSAL_TYPE_CUSTOM = 'CUSTOM' export type GovernanceProposalActionData = { + chainId: string // If true, will hide title, description, and deposit. _onlyShowActions?: boolean version: GovProposalVersion diff --git a/packages/utils/actions.ts b/packages/utils/actions.ts index 3155d3a68..8714d1bd4 100644 --- a/packages/utils/actions.ts +++ b/packages/utils/actions.ts @@ -1,9 +1,13 @@ import { + ActionContextType, + ActionOptions, CosmosMsgForEmpty, LoadedActions, PartialCategorizedActionKeyAndData, } from '@dao-dao/types' +import { transformBech32Address } from './conversion' + // Convert action data to a Cosmos message given all loaded actions. export const convertActionsToMessages = ( loadedActions: LoadedActions, @@ -44,3 +48,21 @@ export const convertActionsToMessages = ( }) // Filter out undefined messages. .filter(Boolean) as CosmosMsgForEmpty[] + +// Get the address for the given action options for the given chain. If a DAO, +// this is the address of the polytone proxy on that chain. For a wallet, this +// is the transformed bech32 address. +export const getChainAddressForActionOptions = ( + { context, chain, address }: ActionOptions, + chainId: string +) => + // If on same chain, return address. + chain.chain_id === chainId + ? address + : // If on different chain, return DAO's polytone proxy address. + context.type === ActionContextType.Dao + ? context.info.polytoneProxies[chainId] || '' + : // If on different chain, return wallet's transformed bech32 address. + context.type === ActionContextType.Wallet + ? transformBech32Address(address, chainId) + : '' diff --git a/packages/utils/messages/cw.ts b/packages/utils/messages/cw.ts index 19d0e2a87..ced8014c4 100644 --- a/packages/utils/messages/cw.ts +++ b/packages/utils/messages/cw.ts @@ -266,12 +266,21 @@ export const makeStakingActionMessage = ( return msg } -export const makePolytoneExecuteMessage = ( +// If the source and destination chains are different, this wraps the message in +// a polytone execution message. Otherwise, it just returns the message. If the +// chains are different but the message is undefined, this will return a message +// that just creates a Polytone account. +export const maybeMakePolytoneExecuteMessage = ( srcChainId: string, destChainId: string, // Allow passing no message, which just creates an account. msg?: CosmosMsgFor_Empty ): CosmosMsgFor_Empty => { + // If on same chain, just return the message. + if (srcChainId === destChainId && msg) { + return msg + } + const polytoneConnection = getSupportedChainConfig(srcChainId)?.polytone?.[destChainId] diff --git a/packages/utils/messages/protobuf.ts b/packages/utils/messages/protobuf.ts index 419f348dd..d9f557aec 100644 --- a/packages/utils/messages/protobuf.ts +++ b/packages/utils/messages/protobuf.ts @@ -645,7 +645,7 @@ export const prepareProtobufJson = (msg: any): any => : // Rule (1) typeof msg === 'string' && msg.startsWith('DATE:') ? new Date(msg.replace('DATE:', '')) - : typeof msg !== 'object' || msg === null + : typeof msg !== 'object' || msg === null || msg.constructor !== Object ? msg : Object.entries(msg).reduce( (acc, [key, value]) => ({