Skip to content

Commit

Permalink
Create Token Factory DAO Mint action (#1464)
Browse files Browse the repository at this point in the history
* Started fixing mint.

* Fixed token factory mint action.
  • Loading branch information
NoahSaso authored Nov 10, 2023
1 parent d5f95b9 commit 79c9815
Show file tree
Hide file tree
Showing 16 changed files with 576 additions and 148 deletions.
10 changes: 8 additions & 2 deletions packages/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@
"pencil": "Pencil",
"people": "People",
"pick": "Mining pick",
"printer": "Printer",
"raisedHand": "Raised hand",
"recycle": "Recycle",
"robot": "Robot",
Expand Down Expand Up @@ -509,6 +510,7 @@
"grantOrRevokeAuthz": "Grant or revoke authorization",
"granteeAddress": "Grantee address",
"granteeAddressTooltip": "The address you are granting or revoking to execute a message on behalf of the DAO.",
"howManyTokensCanTheyMint": "How many ${{tokenSymbol}} can they mint?",
"iContributedPlaceholder": "I contributed...",
"image": "Image",
"imageUrlTooltip": "A link to an image. For example: https://moonphase.is/image.svg",
Expand All @@ -530,6 +532,7 @@
"messageType": "Message type",
"migrateDescription": "This will <1>migrate</1> the selected contract to a new code ID.",
"migrateMessage": "Migrate message",
"minter": "Minter",
"minterContract": "Minter contract",
"minterContractMessage": "Minter contract message",
"multipleChoiceDescription": "This allows proposals to contain multiple choices instead of just `Yes` and `No`.\n\n**CAUTION:** Using more features increases the risk to a DAO because there are more things that can go wrong. You can always enable this later.",
Expand Down Expand Up @@ -869,6 +872,7 @@
"minimumOutputRequiredDescription_gov": "Before the proposal is passed and executed, the swap price will fluctuate. If the price drops and no longer satisfies this minimum output required, the swap will not occur.",
"minimumOutputRequiredDescription_wallet": "The exact swap price will fluctuate during the transaction, but the minimum output amount is guaranteed. If the price drops and no longer satisfies this minimum output required, the swap will not occur.",
"mintActionDescription": "Mint new governance tokens.",
"mintExplanation": "This action mints new tokens, increasing the token supply. With great power comes great responsibility; be careful! If you have an active threshold set, this may lock the DAO.",
"mintNftDescription": "Create a new NFT.",
"mustViewAllActionPagesBeforeVoting": "You must view all action pages before voting.",
"name": "Name",
Expand Down Expand Up @@ -1027,8 +1031,7 @@
"token_one": "token",
"token_other": "tokens",
"tokens": "tokens",
"tokensWillBeSentToTreasury_dao": "These tokens will be sent to the DAO's treasury.",
"tokensWillBeSentToTreasury_wallet": "These tokens will be sent to your wallet.",
"tokensWillBeSentToTreasury": "These tokens will be sent to the DAO's treasury.",
"tokensWillBeSplitAmongContributors": "{{tokens}} will be split among contributors.",
"tos": "DAO DAO TOOLING IS PROVIDED \"AS IS\", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. No developer or entity involved in creating the DAO DAO UI or smart contracts will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of DAO DAO tooling, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.",
"totalHoldings": "Total holdings",
Expand All @@ -1055,6 +1058,8 @@
},
"updateContractAdminActionDescription": "Update the CosmWasm level admin of a smart contract.",
"updateInfoActionDescription": "Update your DAO's name, image, and description.",
"updateMinterAllowanceDescription": "Allow an account to mint tokens, or remove the allowance.",
"updateMinterAllowanceExplanation": "This action is needed to allow an account to mint tokens.",
"updatePostDescription": "Update a post on the DAO's press.",
"updateProposalSubmissionConfigActionDescription": "Update the proposal submission paramaters for your DAO.",
"updateVotingConfigActionDescription": "Update the voting parameters for your DAO.",
Expand Down Expand Up @@ -1473,6 +1478,7 @@
"upcoming": "Upcoming",
"updateContractAdmin": "Update Contract Admin",
"updateInfo": "Update Info",
"updateMinterAllowance": "Update Minter Allowance",
"updatePost": "Update Post",
"upgradeToV2": "Upgrade to V2",
"validatorActions": "Validator Actions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
validateRequired,
} from '@dao-dao/utils'

import { useActionOptions } from '../../../../../actions'

export interface MintOptions {
govToken: GenericToken
}
Expand All @@ -22,7 +20,6 @@ export const MintComponent: ActionComponent<MintOptions> = ({
options: { govToken },
}) => {
const { t } = useTranslation()
const { context } = useActionOptions()
const { register, watch, setValue } = useFormContext()

return (
Expand All @@ -49,9 +46,7 @@ export const MintComponent: ActionComponent<MintOptions> = ({
)}

<p className="caption-text italic">
{t('info.tokensWillBeSentToTreasury', {
type: context.type,
})}
{t('info.tokensWillBeSentToTreasury')}
</p>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'

import { AddressInput } from '@dao-dao/stateless'
import { CHAIN_ID, makeReactHookFormDecorator } from '@dao-dao/storybook'
import { TokenType } from '@dao-dao/types'

import { MintData } from '.'
import { MintComponent } from './MintComponent'
import { MintComponent, MintData } from './MintComponent'

export default {
title:
'DAO DAO / packages / stateful / voting-module-adapter / adapters / DaoVotingNativeStaked / actions / Mint',
'DAO DAO / packages / stateful / voting-module-adapter / adapters / DaoVotingTokenStaked / actions / Mint',
component: MintComponent,
decorators: [
makeReactHookFormDecorator<MintData>({
recipient: 'address',
amount: 100000,
}),
],
Expand All @@ -37,5 +38,6 @@ Default.args = {
decimals: 6,
imageUrl: '',
},
AddressInput,
},
}
Original file line number Diff line number Diff line change
@@ -1,58 +1,189 @@
import {
ArrowRightAltRounded,
SubdirectoryArrowRightRounded,
} from '@mui/icons-material'
import clsx from 'clsx'
import { ComponentType, useRef } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import useDeepCompareEffect from 'use-deep-compare-effect'

import { InputErrorMessage, NumberInput } from '@dao-dao/stateless'
import { ActionComponent, GenericToken } from '@dao-dao/types'
import {
InputErrorMessage,
NumberInput,
WarningCard,
useChain,
useDetectWrap,
} from '@dao-dao/stateless'
import {
ActionComponent,
ActionKey,
AddressInputProps,
GenericToken,
} from '@dao-dao/types'
import {
convertMicroDenomToDenomWithDecimals,
makeValidateAddress,
validatePositive,
validateRequired,
} from '@dao-dao/utils'

import { useActionOptions } from '../../../../../actions'
import { UpdateMinterAllowanceData } from '../UpdateMinterAllowance/UpdateMinterAllowanceComponent'

export type MintData = {
recipient: string
amount: number
}

export interface MintOptions {
export type MintOptions = {
govToken: GenericToken
AddressInput: ComponentType<AddressInputProps<MintData>>
}

export const MintComponent: ActionComponent<MintOptions> = ({
fieldNamePrefix,
errors,
isCreating,
options: { govToken },
options: { govToken, AddressInput },
allActionsWithData,
index,
addAction,
}) => {
const { t } = useTranslation()
const { context } = useActionOptions()
const { register, watch, setValue } = useFormContext()
const { address } = useActionOptions()
const { register, watch, setValue, getValues } = useFormContext<MintData>()
const { bech32_prefix: bech32Prefix } = useChain()

const amount = watch((fieldNamePrefix + 'amount') as 'amount')

const { containerRef, childRef, wrapped } = useDetectWrap()
const Icon = wrapped ? SubdirectoryArrowRightRounded : ArrowRightAltRounded

// Ensure an UpdateMinterAllowance action exists before this one for the
// needed amount, or create/update otherwise. The needed amount is the sum of
// all mint actions.
const totalAmountNeeded = allActionsWithData
.filter(({ actionKey }) => actionKey === ActionKey.Mint)
.reduce(
(acc, { data }) => acc + ((data as MintData | undefined)?.amount || 0),
0
)
const firstMintActionIndex = allActionsWithData.findIndex(
({ actionKey }) => actionKey === ActionKey.Mint
)
const updateMinterAllowanceActionIndex = allActionsWithData.findIndex(
({ actionKey, data }) =>
actionKey === ActionKey.UpdateMinterAllowance &&
(data as UpdateMinterAllowanceData | undefined)?.minter === address
)
// Prevents double-add on initial render.
const created = useRef(false)
useDeepCompareEffect(() => {
if (
!isCreating ||
!addAction ||
// If this is not the first mint action, don't do anything.
firstMintActionIndex !== index
) {
return
}

// If no action exists, create one right before.
if (updateMinterAllowanceActionIndex === -1) {
// Prevents double-add on initial render.
if (created.current) {
return
}
created.current = true

addAction(
{
actionKey: ActionKey.UpdateMinterAllowance,
data: {
minter: address,
allowance: amount,
} as UpdateMinterAllowanceData,
},
index
)
} else {
// Path to the allowance field on the update minter allowance action.
const existingAllowanceFieldName = fieldNamePrefix.replace(
new RegExp(`${index}\\.data.$`),
`${updateMinterAllowanceActionIndex}.data.allowance`
)

// Otherwise if the amount isn't correct, update the existing one.
if (getValues(existingAllowanceFieldName as any) !== totalAmountNeeded) {
setValue(existingAllowanceFieldName as any, totalAmountNeeded)
}
}
}, [
addAction,
address,
amount,
fieldNamePrefix,
firstMintActionIndex,
getValues,
index,
isCreating,
setValue,
totalAmountNeeded,
updateMinterAllowanceActionIndex,
])

return (
<>
<NumberInput
containerClassName="w-full"
disabled={!isCreating}
error={errors?.amount}
fieldName={fieldNamePrefix + 'amount'}
min={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)}
register={register}
setValue={setValue}
sizing="none"
step={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)}
unit={'$' + govToken.symbol}
validation={[validateRequired, validatePositive]}
watch={watch}
<WarningCard
className="max-w-prose"
content={t('info.mintExplanation')}
/>

{errors?.amount && (
<div className="-mt-2 flex flex-col gap-1">
<div
className="flex min-w-0 flex-row flex-wrap items-stretch justify-between gap-x-3 gap-y-1"
ref={containerRef}
>
<NumberInput
disabled={!isCreating}
error={errors?.amount}
fieldName={(fieldNamePrefix + 'amount') as 'amount'}
min={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)}
register={register}
setValue={setValue}
step={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)}
unit={'$' + govToken.symbol}
validation={[validateRequired, validatePositive]}
watch={watch}
/>

<div
className="flex min-w-0 grow flex-row items-stretch gap-2 sm:gap-3"
ref={childRef}
>
<div
className={clsx('flex flex-row items-center', wrapped && 'pl-1')}
>
<Icon className="!h-6 !w-6 text-text-secondary" />
</div>

<AddressInput
containerClassName="grow"
disabled={!isCreating}
error={errors?.recipient}
fieldName={(fieldNamePrefix + 'recipient') as 'recipient'}
register={register}
validation={[validateRequired, makeValidateAddress(bech32Prefix)]}
/>
</div>
</div>

{(errors?.amount || errors?.recipient) && (
<div className="-mt-4 flex flex-col gap-1">
<InputErrorMessage error={errors?.amount} />
<InputErrorMessage error={errors?.recipient} />
</div>
)}

<p className="caption-text italic">
{t('info.tokensWillBeSentToTreasury', {
type: context.type,
})}
</p>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions).

```json
{
"to": "<RECIPIENT ADDRESS>",
"recipient": "<RECIPIENT ADDRESS>",
"amount": "<AMOUNT>"
}
```
Loading

1 comment on commit 79c9815

@vercel
Copy link

@vercel vercel bot commented on 79c9815 Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.