Skip to content

Commit

Permalink
Added cross-chain polytone execute action.
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Oct 16, 2023
1 parent 369b03d commit c9d9c2b
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 43 deletions.
3 changes: 3 additions & 0 deletions packages/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
"robot": "Robot",
"suitAndTie": "Suit and tie",
"swords": "Swords",
"telescope": "Telescope",
"token": "Token",
"trash": "Trash",
"unicorn": "Unicorn",
Expand Down Expand Up @@ -766,6 +767,7 @@
"createdPressContract": "The press contract has been created.",
"createdVestingContractManager": "The vesting contract manager has been created.",
"creatingDao": "Creating DAO...",
"crossChainExecuteDescription": "This action executes a cross-chain transaction.",
"customActionDescription_dao": "Perform any custom action a wallet can.",
"customActionDescription_gov": "Perform any custom action.",
"customActionDescription_wallet": "Perform any custom action.",
Expand Down Expand Up @@ -1218,6 +1220,7 @@
"created": "Created",
"createdAccount": "Created account",
"creator": "Creator",
"crossChainExecute": "Cross-Chain Execute",
"currentWinner": "Current Winner",
"custom": "Custom",
"customValidatorAddress": "Custom Validator Address",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useTranslation } from 'react-i18next'

import {
NestedActionsEditor,
NestedActionsEditorOptions,
NestedActionsRenderer,
NestedActionsRendererProps,
} from '@dao-dao/stateless'
import { NestedActionsEditorFormData } from '@dao-dao/types'
import { ActionComponent } from '@dao-dao/types/actions'

export type CrossChainExecuteData = {
chainId: string
} & NestedActionsEditorFormData

export type CrossChainExecuteOptions = NestedActionsEditorOptions &
Omit<NestedActionsRendererProps, 'msgsFieldName'>

export const CrossChainExecuteComponent: ActionComponent<
CrossChainExecuteOptions
> = (props) => {
const { t } = useTranslation()
const { fieldNamePrefix, isCreating, options } = props

return (
<>
<p className="title-text -mb-1 mt-1">{t('title.actions')}</p>

{isCreating ? (
<NestedActionsEditor {...props} />
) : (
<div className="flex flex-col gap-2">
<NestedActionsRenderer
{...options}
msgsFieldName={fieldNamePrefix + 'msgs'}
/>
</div>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# CrossChainExecute

Execute cross-chain messages via a Polytone account.

## Bulk import format

This is relevant when bulk importing actions, as described in [this
guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions).

### Key

`crossChainExecute`

### Data format

```json
{
"chainId": "<CHAIN ID>",
"_actionData": [
// ACTIONS
]
}
```

Each action in `_actionData` should be formatted similar to how bulk actions are
formatted, with `key` and `data` fields, except `key` should be replaced with
`actionKey`. Thus, each object in `_actionData` should be an object with
`actionKey` and `data`, formatted the same as the `actions` array in the bulk
JSON format.
171 changes: 171 additions & 0 deletions packages/stateful/actions/core/advanced/CrossChainExecute/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { useCallback } from 'react'
import { useFormContext } from 'react-hook-form'

import {
ChainPickerInput,
ChainProvider,
TelescopeEmoji,
useChain,
} from '@dao-dao/stateless'
import {
ActionComponent,
ActionContextType,
ActionKey,
ActionMaker,
UseDecodedCosmosMsg,
UseDefaults,
UseTransformToCosmos,
} from '@dao-dao/types'
import {
decodePolytoneExecuteMsg,
maybeMakePolytoneExecuteMessage,
} from '@dao-dao/utils'

import { SuspenseLoader } from '../../../../components'
import {
WalletActionsProvider,
useActionOptions,
useActionsForMatching,
useLoadedActionsAndCategories,
} from '../../../react'
import {
CrossChainExecuteData,
CrossChainExecuteComponent as StatelessCrossChainExecuteComponent,
} from './Component'

const InnerComponentLoading: ActionComponent = (props) => (
<StatelessCrossChainExecuteComponent
{...props}
options={{
categories: [],
loadedActions: {},
actionsForMatching: [],
SuspenseLoader,
}}
/>
)

const InnerComponent: ActionComponent = (props) => {
const { categories, loadedActions } = useLoadedActionsAndCategories({
isCreating: props.isCreating,
})
const actionsForMatching = useActionsForMatching()

return (
<StatelessCrossChainExecuteComponent
{...props}
options={{
categories,
loadedActions,
actionsForMatching,
SuspenseLoader,
}}
/>
)
}

const InnerComponentWrapper: ActionComponent = (props) => {
const { chain_id: chainId } = useChain()
const { context } = useActionOptions()

if (context.type !== ActionContextType.Dao) {
return null
}

const address =
chainId === context.info.chainId
? context.info.coreAddress
: context.info.polytoneProxies[chainId] || ''

return address ? (
<WalletActionsProvider address={address}>
<InnerComponent {...props} />
</WalletActionsProvider>
) : (
<InnerComponentLoading {...props} />
)
}

const Component: ActionComponent = (props) => {
const {
context,
chain: { chain_id: currentChainId },
} = useActionOptions()

const { watch } = useFormContext<CrossChainExecuteData>()
const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId')

return (
<>
{context.type === ActionContextType.Dao && (
<ChainPickerInput
className="mb-4"
disabled={!props.isCreating}
excludeChainIds={[currentChainId]}
fieldName={props.fieldNamePrefix + 'chainId'}
/>
)}

{chainId !== currentChainId && (
// Re-render when chain changes so hooks and state reset.
<ChainProvider key={chainId} chainId={chainId}>
<InnerComponentWrapper {...props} />
</ChainProvider>
)}
</>
)
}

export const makeCrossChainExecuteAction: ActionMaker<
CrossChainExecuteData
> = ({ t, context, chain: { chain_id: currentChainId } }) => {
// Only support DAO polytone.
if (context.type !== ActionContextType.Dao) {
return null
}

const useDefaults: UseDefaults<CrossChainExecuteData> = () => ({
chainId: currentChainId,
msgs: [],
})

const useDecodedCosmosMsg: UseDecodedCosmosMsg<CrossChainExecuteData> = (
msg: Record<string, any>
) => {
const decodedPolytone = decodePolytoneExecuteMsg(currentChainId, msg)

return decodedPolytone.match
? {
match: true,
data: {
chainId: decodedPolytone.chainId,
msgs: decodedPolytone.cosmosMsgs,
},
}
: {
match: false,
}
}

const useTransformToCosmos: UseTransformToCosmos<
CrossChainExecuteData
> = () =>
useCallback(
({ chainId, msgs }) =>
currentChainId === chainId
? undefined
: maybeMakePolytoneExecuteMessage(currentChainId, chainId, msgs),
[]
)

return {
key: ActionKey.CrossChainExecute,
Icon: TelescopeEmoji,
label: t('title.crossChainExecute'),
description: t('info.crossChainExecuteDescription'),
Component,
useDefaults,
useTransformToCosmos,
useDecodedCosmosMsg,
}
}
7 changes: 6 additions & 1 deletion packages/stateful/actions/core/advanced/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { ActionCategoryKey, ActionCategoryMaker } from '@dao-dao/types'

import { makeBulkImportAction } from './BulkImport'
import { makeCrossChainExecuteAction } from './CrossChainExecute'
import { makeCustomAction } from './Custom'

export const makeAdvancedActionCategory: ActionCategoryMaker = ({ t }) => ({
key: ActionCategoryKey.Custom,
label: t('actionCategory.advancedLabel'),
description: t('actionCategory.advancedDescription'),
actionMakers: [makeCustomAction, makeBulkImportAction],
actionMakers: [
makeCustomAction,
makeCrossChainExecuteAction,
makeBulkImportAction,
],
})
2 changes: 2 additions & 0 deletions packages/stateful/actions/react/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const actionKeyToMatchOrder = (key: ActionKey) =>
// The upgrade action (and likely future upgrade actions) are a specific
// migrate action, so this needs to be after all those.
ActionKey.Migrate,
// This is a more specific execute action, so it must be before execute.
ActionKey.CrossChainExecute,
// Most messages are some form of execute. Migrate and execute are
// different, so the order between these two is irrelevant.
ActionKey.Execute,
Expand Down
4 changes: 4 additions & 0 deletions packages/stateless/components/emoji.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ export const ChainEmoji = () => (
<EmojiWrapper emoji="⛓️" labelI18nKey="emoji.chains" />
)

export const TelescopeEmoji = () => (
<EmojiWrapper emoji="🔭" labelI18nKey="emoji.telescope" />
)

export const CurvedDownArrowEmoji = () => (
<EmojiWrapper emoji="⤵️" labelI18nKey="emoji.curvedDownArrow" />
)
Expand Down
1 change: 1 addition & 0 deletions packages/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export enum ActionKey {
Mint = 'mint',
ManageVesting = 'manageVesting',
CreateCrossChainAccount = 'createCrossChainAccount',
CrossChainExecute = 'crossChainExecute',
// DaoProposalSingle
UpdatePreProposeSingleConfig = 'updatePreProposeSingleConfig',
UpdateProposalSingleConfig = 'updateProposalSingleConfig',
Expand Down
5 changes: 5 additions & 0 deletions packages/types/proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ export type DecodedPolytoneMsgMatch = {
match: true
chainId: string
polytoneConnection: PolytoneConnection
// The first message, or empty object if none.
msg: Record<string, any>
// The first message, or undefined if none.
cosmosMsg: CosmosMsgFor_Empty | undefined
// All messages.
msgs: Record<string, any>[]
cosmosMsgs: CosmosMsgFor_Empty[]
initiatorMsg: string
}

Expand Down
Loading

0 comments on commit c9d9c2b

Please sign in to comment.