Skip to content

Commit

Permalink
Added create ICA account advanced action.
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Oct 20, 2023
1 parent 57ca220 commit fb2c47c
Show file tree
Hide file tree
Showing 12 changed files with 485 additions and 97 deletions.
4 changes: 4 additions & 0 deletions packages/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"saveDraft": "Save draft",
"searchDaos": "Search DAOs",
"selectAllNfts": "Select all {{count}} NFTs",
"selectChain": "Select chain",
"selectNft": "Select NFT",
"selectToken": "Select token",
"selectValidator": "Select validator",
Expand Down Expand Up @@ -330,6 +331,7 @@
"failedToRelayPackets": "Failed to relay packets to {{chain}}.",
"feeTokenNotFound": "Fee token not found.",
"govTokenBalancesDoNotSumTo100": "Total token distribution percentage must equal 100%, but it currently sums to {{totalPercent}}.",
"icaAccountAlreadyExists": "ICA account already exists on {{chain}}.",
"icaAddressNotLoaded": "ICA address not loaded.",
"insufficientForDeposit": "You do not have enough funds to cover a deposit of {{amount}} ${{tokenSymbol}}.",
"insufficientFunds": "Insufficient funds.",
Expand Down Expand Up @@ -765,6 +767,7 @@
"createCrossChainAccountDescription": "Create an account for this DAO on another chain.",
"createCrossChainAccountExplanation": "This action creates an account on another chain, allowing this DAO to perform actions on that chain.",
"createFirstOneQuestion": "Create the first one?",
"createICAAccountDescription": "Create an account with the Interchain Accounts SDK module.",
"createNftCollectionDescription_dao": "Create a new NFT collection controlled by the DAO.",
"createNftCollectionDescription_gov": "Create a new NFT collection controlled by the chain.",
"createNftCollectionDescription_wallet": "Create a new NFT collection controlled by you.",
Expand Down Expand Up @@ -1226,6 +1229,7 @@
"createAProposal": "Create a proposal",
"createASubDao": "Create a SubDAO",
"createCrossChainAccount": "Create Cross-Chain Account",
"createICAAccount": "Create ICA Account",
"createNftCollection": "Create NFT Collection",
"createPost": "Create Post",
"createProposal": "Create proposal",
Expand Down
4 changes: 2 additions & 2 deletions packages/state/recoil/selectors/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
getChainForChainId,
getChainForChainName,
getFallbackImage,
getIbcTransferInfoFromChainSource,
getIbcTransferInfoFromChannel,
getTokenForChainIdAndDenom,
isValidContractAddress,
isValidTokenFactoryDenom,
Expand Down Expand Up @@ -430,7 +430,7 @@ export const sourceChainAndDenomSelector = selectorFamily<
sourceChainId = channels.reduce(
(currentChainId, channel) =>
getChainForChainName(
getIbcTransferInfoFromChainSource(currentChainId, channel)
getIbcTransferInfoFromChannel(currentChainId, channel)
.destinationChain.chain_name
).chain_id,
chainId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'

import {
CopyToClipboard,
IbcDestinationChainPicker,
InputErrorMessage,
Loader,
WarningCard,
useChain,
} from '@dao-dao/stateless'
import { LoadingDataWithError } from '@dao-dao/types'
import { ActionComponent } from '@dao-dao/types/actions'
import { getDisplayNameForChainId, getImageUrlForChainId } from '@dao-dao/utils'

export type CreateIcaAccountData = {
chainId: string
}

export type CreateIcaAccountOptions = {
createdAddressLoading: LoadingDataWithError<string | undefined>
}

export const CreateIcaAccountComponent: ActionComponent<
CreateIcaAccountOptions
> = ({
fieldNamePrefix,
isCreating,
errors,
options: { createdAddressLoading },
}) => {
const { t } = useTranslation()
const { watch, setValue } = useFormContext<CreateIcaAccountData>()
const { chain_id: sourceChainId } = useChain()

const destinationChainId = watch((fieldNamePrefix + 'chainId') as 'chainId')
const imageUrl = getImageUrlForChainId(destinationChainId)

return (
<>
{isCreating ? (
<>
<IbcDestinationChainPicker
buttonClassName="self-start"
includeSourceChain={false}
onChainSelected={(chainId) =>
setValue((fieldNamePrefix + 'chainId') as 'chainId', chainId)
}
selectedChainId={destinationChainId}
sourceChainId={sourceChainId}
/>

<InputErrorMessage className="-mt-2" error={errors?.chainId} />
</>
) : (
<div className="flex flex-row flex-wrap items-center justify-between gap-x-4 gap-y-2 rounded-md bg-background-secondary px-4 py-3">
<div className="flex flex-row items-center gap-2">
{imageUrl && (
<div
className="h-6 w-6 bg-contain bg-center bg-no-repeat"
style={{
backgroundImage: `url(${imageUrl})`,
}}
></div>
)}

<p className="primary-text shrink-0">
{getDisplayNameForChainId(destinationChainId)}
</p>
</div>

{createdAddressLoading.loading ? (
<Loader />
) : createdAddressLoading.errored ? (
<WarningCard
content={
createdAddressLoading.error instanceof Error
? createdAddressLoading.error.message
: `${createdAddressLoading.error}`
}
/>
) : createdAddressLoading.data ? (
<CopyToClipboard
className="min-w-0"
takeN={18}
tooltip={t('button.clickToCopyAddress')}
value={createdAddressLoading.data}
/>
) : (
<p className="secondary-text">{t('info.pending')}</p>
)}
</div>
)}
</>
)
}
20 changes: 20 additions & 0 deletions packages/stateful/actions/core/advanced/CreateIcaAccount/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# CreateIcaAccount

Create an account via Interchain Accounts IBC module.

## 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

`createIcaAccount`

### Data format

```json
{
"chainId": "<CHAIN ID>"
}
```
169 changes: 169 additions & 0 deletions packages/stateful/actions/core/advanced/CreateIcaAccount/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { useCallback, useEffect } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'

import { MsgRegisterInterchainAccount } from '@dao-dao/protobuf/codegen/ibc/applications/interchain_accounts/controller/v1/tx'
import { Metadata } from '@dao-dao/protobuf/codegen/ibc/applications/interchain_accounts/v1/metadata'
import { icaRemoteAddressSelector } from '@dao-dao/state/recoil'
import { ChainEmoji, useCachedLoadingWithError } from '@dao-dao/stateless'
import {
ActionComponent,
ActionKey,
ActionMaker,
UseDecodedCosmosMsg,
UseDefaults,
UseTransformToCosmos,
} from '@dao-dao/types'
import {
getChainForChainName,
getDisplayNameForChainId,
getIbcTransferInfoBetweenChains,
getIbcTransferInfoFromConnection,
isDecodedStargateMsg,
makeStargateMessage,
} from '@dao-dao/utils'

import { useActionOptions } from '../../../react'
import { CreateIcaAccountComponent, CreateIcaAccountData } from './Component'

const Component: ActionComponent = (props) => {
const { t } = useTranslation()
const {
address,
chain: { chain_id: srcChainId },
} = useActionOptions()

const { watch, setError, clearErrors } =
useFormContext<CreateIcaAccountData>()
const destChainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId')

const createdAddressLoading = useCachedLoadingWithError(
icaRemoteAddressSelector({
address,
srcChainId,
destChainId,
})
)

// If ICA account already exists for this chain during creation, add error
// preventing submission.
useEffect(() => {
if (
!createdAddressLoading.loading &&
!createdAddressLoading.errored &&
createdAddressLoading.data &&
props.isCreating
) {
setError((props.fieldNamePrefix + 'chainId') as 'chainId', {
type: 'manual',
message: t('error.icaAccountAlreadyExists', {
chain: getDisplayNameForChainId(destChainId),
}),
})
} else {
clearErrors((props.fieldNamePrefix + 'chainId') as 'chainId')
}
}, [
clearErrors,
createdAddressLoading,
destChainId,
props.fieldNamePrefix,
props.isCreating,
setError,
t,
])

return (
<CreateIcaAccountComponent
{...props}
options={{
createdAddressLoading,
}}
/>
)
}

export const makeCreateIcaAccountAction: ActionMaker<CreateIcaAccountData> = ({
t,
chain: { chain_id: sourceChainId },
address,
}) => {
const useDefaults: UseDefaults<CreateIcaAccountData> = () => ({
chainId: '',
})

const useTransformToCosmos: UseTransformToCosmos<CreateIcaAccountData> = () =>
useCallback(({ chainId }) => {
if (!chainId) {
return
}

const info = getIbcTransferInfoBetweenChains(sourceChainId, chainId)

return chainId
? makeStargateMessage({
stargate: {
typeUrl: MsgRegisterInterchainAccount.typeUrl,
value: MsgRegisterInterchainAccount.fromPartial({
owner: address,
connectionId: info.sourceChain.connection_id,
version: JSON.stringify(
Metadata.fromPartial({
version: 'ics27-1',
controllerConnectionId: info.sourceChain.connection_id,
hostConnectionId: info.destinationChain.connection_id,
// Empty when registering a new address.
address: '',
encoding: 'proto3',
txType: 'sdk_multi_msg',
})
),
}),
},
})
: undefined
}, [])

const useDecodedCosmosMsg: UseDecodedCosmosMsg<CreateIcaAccountData> = (
msg: Record<string, any>
) => {
if (
!isDecodedStargateMsg(msg) ||
msg.stargate.typeUrl !== MsgRegisterInterchainAccount.typeUrl
) {
return {
match: false,
}
}

try {
const { connectionId } = msg.stargate.value
const { destinationChain } = getIbcTransferInfoFromConnection(
sourceChainId,
connectionId
)

return {
match: true,
data: {
chainId: getChainForChainName(destinationChain.chain_name).chain_id,
},
}
} catch (err) {
return {
match: false,
}
}
}

return {
key: ActionKey.CreateIcaAccount,
Icon: ChainEmoji,
label: t('title.createICAAccount'),
description: t('info.createICAAccountDescription'),
Component,
useDefaults,
useTransformToCosmos,
useDecodedCosmosMsg,
}
}
2 changes: 2 additions & 0 deletions packages/stateful/actions/core/advanced/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ActionCategoryKey, ActionCategoryMaker } from '@dao-dao/types'

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

Expand All @@ -11,6 +12,7 @@ export const makeAdvancedActionCategory: ActionCategoryMaker = ({ t }) => ({
actionMakers: [
makeCustomAction,
makeCrossChainExecuteAction,
makeCreateIcaAccountAction,
makeBulkImportAction,
],
})
Loading

0 comments on commit fb2c47c

Please sign in to comment.