diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index 263268731..168477717 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -463,6 +463,7 @@ "insufficientBalance": "Insufficient balance of {{amount}} ${{tokenSymbol}}.", "insufficientFunds": "Insufficient funds.", "insufficientFundsWarning": "You currently have {{amount}} ${{tokenSymbol}}, which may not be sufficient unless another action transfers funds to the DAO before this one.", + "insufficientFundsWarningMinusServiceFee": "You currently have {{amount}} ${{tokenSymbol}} available (accounting for the service fee), which may not be sufficient unless another action transfers funds to the DAO before this one.", "insufficientWalletBalance": "Insufficient wallet balance of {{amount}} ${{tokenSymbol}}.", "invalidAccount": "At least one of the specified accounts is invalid.", "invalidActionKeys": "Invalid action keys found: {{keys}}", diff --git a/packages/state/recoil/selectors/account.ts b/packages/state/recoil/selectors/account.ts index 1b7806d91..497602704 100644 --- a/packages/state/recoil/selectors/account.ts +++ b/packages/state/recoil/selectors/account.ts @@ -79,6 +79,8 @@ export const allBalancesSelector = selectorFamily< includeAccountTypes?: AccountType[] // Exclude these account types. excludeAccountTypes?: AccountType[] + // Include only these chain IDs. + includeChainIds?: string[] }> >({ key: 'allBalances', @@ -94,6 +96,7 @@ export const allBalancesSelector = selectorFamily< ignoreStaked, includeAccountTypes, excludeAccountTypes = [AccountType.Valence], + includeChainIds, }) => ({ get }) => { const allAccounts = get( @@ -102,10 +105,15 @@ export const allBalancesSelector = selectorFamily< address: mainAddress, includeIcaChains, }) - ).filter(({ type }) => { + ).filter(({ chainId, type }) => { + if (includeChainIds && !includeChainIds.includes(chainId)) { + return false + } + if (includeAccountTypes) { return includeAccountTypes.includes(type) } + if (excludeAccountTypes) { return !excludeAccountTypes.includes(type) } diff --git a/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.stories.tsx b/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.stories.tsx index a2c2387dc..e095c2c08 100644 --- a/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.stories.tsx +++ b/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.stories.tsx @@ -20,6 +20,10 @@ export default { component: ConfigureRebalancerComponent, decorators: [ makeReactHookFormDecorator({ + newValenceAccount: { + creating: true, + funds: [], + }, chainId: ChainId.NeutronMainnet, baseDenom: getNativeIbcUsdc(ChainId.NeutronMainnet)!.denomOrAddress, tokens: [ @@ -98,6 +102,54 @@ Default.args = { }, ], }, + serviceFee: { + loading: false, + errored: false, + data: null, + }, + currentValenceBalances: { + loading: false, + data: [ + { + token: getNativeTokenForChainId(ChainId.NeutronMainnet), + balance: '46252349169321', + }, + { + token: { + chainId: ChainId.NeutronMainnet, + type: TokenType.Native, + denomOrAddress: getNativeIbcUsdc(ChainId.NeutronMainnet)! + .denomOrAddress, + decimals: 6, + symbol: 'USDC', + imageUrl: '', + source: { + chainId: ChainId.NeutronMainnet, + type: TokenType.Native, + denomOrAddress: getNativeIbcUsdc(ChainId.NeutronMainnet)! + .denomOrAddress, + }, + }, + balance: '102948124125', + }, + { + token: { + chainId: ChainId.NeutronMainnet, + type: TokenType.Native, + denomOrAddress: 'uatom', + decimals: 6, + symbol: 'ATOM', + imageUrl: '', + source: { + chainId: ChainId.NeutronMainnet, + type: TokenType.Native, + denomOrAddress: 'uatom', + }, + }, + balance: '1284135723893', + }, + ], + }, denomWhitelistTokens: { loading: false, data: [ diff --git a/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.tsx b/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.tsx index 1ac4f196f..6b3ef761e 100644 --- a/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.tsx +++ b/packages/stateful/actions/core/actions/ConfigureRebalancer/Component.tsx @@ -7,11 +7,13 @@ import { HugeDecimal } from '@dao-dao/math' import { Button, ErrorPage, + FormCheckbox, IconButton, InputErrorMessage, InputLabel, Loader, MarkdownRenderer, + NativeCoinSelector, NumericInput, RebalancerProjector, RebalancerProjectorAsset, @@ -29,12 +31,12 @@ import { GenericTokenWithUsdPrice, LoadingData, LoadingDataWithError, - ValenceAccount, } from '@dao-dao/types' import { ActionComponent } from '@dao-dao/types/actions' import { TargetOverrideStrategy } from '@dao-dao/types/contracts/ValenceRebalancer' import { formatPercentOf100, + getNativeTokenForChainId, makeValidateAddress, validatePositive, validateRequired, @@ -68,10 +70,17 @@ export const pidPresets: { ] export type ConfigureRebalancerData = { - // Will be set when a valence account is found so the transformation function - // has the address. - valenceAccount?: ValenceAccount chainId: string + newValenceAccount: { + creating: boolean + funds: { + denom: string + amount: string + // Will multiply `amount` by 10^decimals when generating the message. + decimals: number + }[] + acknowledgedServiceFee?: boolean + } trustee?: string baseDenom: string tokens: { @@ -97,6 +106,8 @@ export type ConfigureRebalancerData = { export type ConfigureRebalancerOptions = { nativeBalances: LoadingData + serviceFee: LoadingDataWithError + currentValenceBalances: LoadingData baseDenomWhitelistTokens: LoadingData denomWhitelistTokens: LoadingData prices: LoadingDataWithError @@ -112,6 +123,8 @@ export const ConfigureRebalancerComponent: ActionComponent< isCreating, options: { nativeBalances, + serviceFee, + currentValenceBalances, baseDenomWhitelistTokens, denomWhitelistTokens, prices, @@ -133,6 +146,7 @@ export const ConfigureRebalancerComponent: ActionComponent< clearErrors, setError, } = useFormContext() + const { fields: tokensFields, append: appendToken, @@ -142,6 +156,17 @@ export const ConfigureRebalancerComponent: ActionComponent< name: (fieldNamePrefix + 'tokens') as 'tokens', }) + const { + fields: newValenceAccountFunds, + append: appendNewValenceAccountFund, + remove: removeNewValenceAccountFund, + } = useFieldArray({ + control, + name: (fieldNamePrefix + + 'newValenceAccount.funds') as 'newValenceAccount.funds', + }) + + const chainId = watch((fieldNamePrefix + 'chainId') as 'chainId') const baseDenom = watch((fieldNamePrefix + 'baseDenom') as 'baseDenom') const targetOverrideStrategy = watch( (fieldNamePrefix + 'targetOverrideStrategy') as 'targetOverrideStrategy' @@ -152,6 +177,10 @@ export const ConfigureRebalancerComponent: ActionComponent< const showCustomPid = watch( (fieldNamePrefix + 'showCustomPid') as 'showCustomPid' ) + const creatingNewValenceAccount = !!watch( + (fieldNamePrefix + + 'newValenceAccount.creating') as 'newValenceAccount.creating' + ) // Get selected whitelist tokens. const denomWhitelistTokensSelected = denomWhitelistTokens.loading @@ -198,8 +227,152 @@ export const ConfigureRebalancerComponent: ActionComponent< preset.kp === pid.kp && preset.ki === pid.ki && preset.kd === pid.kd )?.preset + const acknowledgedServiceFee = watch( + (fieldNamePrefix + + 'newValenceAccount.acknowledgedServiceFee') as 'newValenceAccount.acknowledgedServiceFee' + ) + + useEffect(() => { + if (!isCreating) { + return + } + + if (acknowledgedServiceFee) { + if (errors?.newValenceAccount?.acknowledgedServiceFee) { + clearErrors( + (fieldNamePrefix + + 'newValenceAccount.acknowledgedServiceFee') as 'newValenceAccount.acknowledgedServiceFee' + ) + } + } else { + if (!errors?.newValenceAccount?.acknowledgedServiceFee) { + setError( + (fieldNamePrefix + + 'newValenceAccount.acknowledgedServiceFee') as 'newValenceAccount.acknowledgedServiceFee', + { + type: 'required', + message: t('error.acknowledgeServiceFee'), + } + ) + } + } + }, [ + isCreating, + acknowledgedServiceFee, + fieldNamePrefix, + clearErrors, + setError, + t, + errors?.newValenceAccount?.acknowledgedServiceFee, + ]) + return ( <> + {creatingNewValenceAccount && ( +
+ + + {newValenceAccountFunds.map(({ id }, index) => ( + 1 + ? () => removeNewValenceAccountFund(index) + : undefined + } + overrideInsufficientFundsWarning={(amount, tokenSymbol) => + t('error.insufficientFundsWarningMinusServiceFee', { + amount, + tokenSymbol, + }) + } + tokens={nativeBalances} + /> + ))} + + {!isCreating && newValenceAccountFunds.length === 0 && ( +

+ {t('info.none')} +

+ )} + + {isCreating && ( + <> + + +
+ + +

+ setValue( + (fieldNamePrefix + + 'newValenceAccount.acknowledgedServiceFee') as 'newValenceAccount.acknowledgedServiceFee', + !acknowledgedServiceFee + ) + } + > + {t('info.acknowledgeServiceFee', { + fee: serviceFee.loading + ? '...' + : serviceFee.errored + ? '' + : serviceFee.data + ? t('format.token', { + amount: HugeDecimal.from( + serviceFee.data.balance + ).toInternationalizedHumanReadableString({ + decimals: serviceFee.data.token.decimals, + }), + symbol: serviceFee.data.token.symbol, + }) + : '', + context: + serviceFee.loading || + serviceFee.errored || + serviceFee.data + ? undefined + : 'none', + })} +

+
+ + + + )} +
+ )} +
@@ -598,7 +771,7 @@ export const ConfigureRebalancerComponent: ActionComponent< numericValue register={register} setValue={setValue} - sizing="auto" + sizing="md" step={0.01} unit="%" validation={[validateRequired, validatePositive]} @@ -629,7 +802,7 @@ export const ConfigureRebalancerComponent: ActionComponent<

- {nativeBalances.loading || + {currentValenceBalances.loading || prices.loading || denomWhitelistTokens.loading ? ( @@ -643,7 +816,7 @@ export const ConfigureRebalancerComponent: ActionComponent< ({ denomOrAddress }) => denomOrAddress === denom ) const { balance: _balance } = - nativeBalances.data.find( + currentValenceBalances.data.find( ({ token }) => token.denomOrAddress === denom ) ?? {} const balance = HugeDecimal.from(_balance || 0) diff --git a/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx b/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx index 744f0bfe5..2cb011d60 100644 --- a/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx +++ b/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx @@ -1,15 +1,15 @@ -import { fromBase64, fromUtf8 } from '@cosmjs/encoding' -import { useQueries, useQueryClient } from '@tanstack/react-query' -import cloneDeep from 'lodash.clonedeep' -import { useEffect } from 'react' +import { fromBase64, fromUtf8, toUtf8 } from '@cosmjs/encoding' +import { useQueryClient } from '@tanstack/react-query' import { useFormContext } from 'react-hook-form' import { waitForAll } from 'recoil' import { HugeDecimal } from '@dao-dao/math' import { + contractQueries, genericTokenSelector, tokenQueries, valenceRebalancerExtraQueries, + valenceRebalancerQueries, } from '@dao-dao/state' import { usdPriceSelector } from '@dao-dao/state/recoil/selectors' import { @@ -20,8 +20,6 @@ import { useActionOptions, useCachedLoading, useCachedLoadingWithError, - useInitializedActionForKey, - useUpdatingRef, } from '@dao-dao/stateless' import { AccountType, @@ -30,9 +28,9 @@ import { TokenType, UnifiedCosmosMsg, ValenceAccount, + makeStargateMessage, } from '@dao-dao/types' import { - ActionChainContextType, ActionComponent, ActionContextType, ActionKey, @@ -40,32 +38,34 @@ import { ActionOptions, ProcessedMessage, } from '@dao-dao/types/actions' -import { ExecuteMsg as ValenceAccountExecuteMsg } from '@dao-dao/types/contracts/ValenceAccount' +import { + ExecuteMsg as ValenceAccountExecuteMsg, + InstantiateMsg as ValenceAccountInstantiateMsg, +} from '@dao-dao/types/contracts/ValenceAccount' import { RebalancerData, RebalancerUpdateData, } from '@dao-dao/types/contracts/ValenceRebalancer' +import { MsgInstantiateContract2 } from '@dao-dao/types/protobuf/codegen/cosmwasm/wasm/v1/tx' import { VALENCE_INSTANTIATE2_SALT, VALENCE_SUPPORTED_CHAINS, encodeJsonToBase64, getAccount, getChainAddressForActionOptions, + getDisplayNameForChainId, getSupportedChainConfig, - makeCombineQueryResultsIntoLoadingData, + isDecodedStargateMsg, makeWasmMessage, maybeMakePolytoneExecuteMessages, mustGetSupportedChainConfig, objectMatchesStructure, + tokensEqual, } from '@dao-dao/utils' import { AddressInput } from '../../../../components/AddressInput' -import { - useGenerateInstantiate2, - useQueryLoadingDataWithError, -} from '../../../../hooks' +import { useQueryLoadingDataWithError } from '../../../../hooks' import { useTokenBalances } from '../../../hooks/useTokenBalances' -import { CreateValenceAccountData } from '../CreateValenceAccount/Component' import { ConfigureRebalancerData, ConfigureRebalancerComponent as StatelessConfigureRebalancerComponent, @@ -77,109 +77,14 @@ const Component: ActionComponent = ( ) => { const queryClient = useQueryClient() const options = useActionOptions() + const { watch, setValue } = useFormContext() - const valenceAccount = watch( - (props.fieldNamePrefix + 'valenceAccount') as 'valenceAccount' - ) const chainId = watch((props.fieldNamePrefix + 'chainId') as 'chainId') const selectedTokens = watch((props.fieldNamePrefix + 'tokens') as 'tokens') - - // Get predictable valence account address in case we have to create it. - const generatedValenceAddress = useGenerateInstantiate2({ - chainId, - creator: getChainAddressForActionOptions(options, chainId) || '', - codeId: - (options.chainContext.type === ActionChainContextType.Supported && - options.chainContext.config.codeIds.ValenceAccount) || - -1, - salt: VALENCE_INSTANTIATE2_SALT, - }) - - const existingValenceAccount = getAccount({ - accounts: options.context.accounts, - chainId, - types: [AccountType.Valence], - }) - - // Check if create valence account action already exists. - const existingCreateValenceAccountActionIndex = - props.allActionsWithData.findIndex( - ({ actionKey }) => actionKey === ActionKey.CreateValenceAccount - ) - // Get the data from the Valence creation action if it exists. - const existingCreateValenceAccountActionData = - existingCreateValenceAccountActionIndex > -1 - ? (props.allActionsWithData[existingCreateValenceAccountActionIndex] - ?.data as CreateValenceAccountData) - : undefined - const createValenceAccountAction = useInitializedActionForKey( - ActionKey.CreateValenceAccount - ) - // Can add create valence account if no existing action and defaults loaded. - const canAddCreateValenceAccountAction = - props.isCreating && - !existingValenceAccount && - (existingCreateValenceAccountActionIndex === -1 || - existingCreateValenceAccountActionIndex > props.index) && - !createValenceAccountAction.loading && - !createValenceAccountAction.errored - const addCreateValenceAccountActionIfNeededRef = useUpdatingRef(() => { - if (canAddCreateValenceAccountAction) { - props.addAction?.( - { - actionKey: ActionKey.CreateValenceAccount, - data: cloneDeep(createValenceAccountAction.data.defaults), - }, - props.index - ) - } - }) - - // Set valence account if not set, or add action to create if not found before - // this configure action. - useEffect(() => { - if (!valenceAccount) { - // If existing account found, set it. - if (existingValenceAccount?.type === AccountType.Valence) { - setValue( - (props.fieldNamePrefix + 'valenceAccount') as 'valenceAccount', - existingValenceAccount - ) - } - // Otherwise attempt to use generated one. - else if ( - !generatedValenceAddress.loading && - !generatedValenceAddress.errored - ) { - setValue( - (props.fieldNamePrefix + 'valenceAccount') as 'valenceAccount', - { - type: AccountType.Valence, - chainId, - address: generatedValenceAddress.data, - config: { - admin: '', - rebalancer: null, - }, - } - ) - } - } - - // Attempt to add create valence account action if needed. - if (canAddCreateValenceAccountAction) { - addCreateValenceAccountActionIfNeededRef.current() - } - }, [ - setValue, - valenceAccount, - props.fieldNamePrefix, - existingValenceAccount, - canAddCreateValenceAccountAction, - addCreateValenceAccountActionIfNeededRef, - generatedValenceAddress, - chainId, - ]) + const newValenceAccount = + watch( + (props.fieldNamePrefix + 'newValenceAccount') as 'newValenceAccount' + ) || {} const rebalancer = mustGetSupportedChainConfig(chainId).valence?.rebalancer const whitelists = useQueryLoadingDataWithError( @@ -193,6 +98,17 @@ const Component: ActionComponent = ( : undefined ) ) + const serviceFee = useQueryLoadingDataWithError( + valenceRebalancerExtraQueries.rebalancerRegistrationServiceFee( + queryClient, + rebalancer + ? { + chainId, + address: rebalancer, + } + : undefined + ) + ) const minBalanceDenom = watch( (props.fieldNamePrefix + 'minBalance.denom') as 'minBalance.denom' @@ -208,7 +124,22 @@ const Component: ActionComponent = ( undefined ) - const currentBalances = useTokenBalances({ + const nativeBalances = useTokenBalances({ + filter: TokenType.Native, + // Ensure chosen tokens are loaded. + additionalTokens: props.isCreating + ? undefined + : (newValenceAccount.funds || []).map(({ denom }) => ({ + chainId, + type: TokenType.Native, + denomOrAddress: denom, + })), + // Only fetch balances for base/polytone account on Valence chain. + includeAccountTypes: [AccountType.Base, AccountType.Polytone], + includeChainIds: [chainId], + }) + + const valenceBalances = useTokenBalances({ filter: TokenType.Native, // Ensure chosen tokens are loaded. additionalTokens: selectedTokens.map(({ denom }) => ({ @@ -218,45 +149,21 @@ const Component: ActionComponent = ( })), // Only fetch balances for Valence account. includeAccountTypes: [AccountType.Valence], + includeChainIds: [chainId], }) - // Load tokens used in the create valence account action if it exists. - const initialTokens = useQueries({ - queries: - existingCreateValenceAccountActionData?.funds.map(({ denom }) => - tokenQueries.info(queryClient, { - chainId, - type: TokenType.Native, - denomOrAddress: denom, - }) - ) ?? [], - combine: makeCombineQueryResultsIntoLoadingData({ - transform: (tokens) => - tokens.map( - (token): GenericTokenBalance => ({ - token, - balance: HugeDecimal.fromHumanReadable( - existingCreateValenceAccountActionData?.funds - .find(({ denom }) => denom === token.denomOrAddress) - ?.amount?.toString() || 0, - token.decimals - ).toString(), - }) - ), - }), - }) - - const nativeBalances: LoadingData = - // If creating new Valence account, use initial tokens from that action, - // since there will be no current balances loaded yet. - existingCreateValenceAccountActionData - ? initialTokens - : currentBalances.loading - ? currentBalances + const currentValenceBalances: LoadingData = + // If creating new Valence account, use base native tokens that will be used + // to fund the initial account. + newValenceAccount.creating + ? nativeBalances + : // Otherwise use current Valence account balances. + valenceBalances.loading + ? valenceBalances : { loading: false, - updating: currentBalances.updating, - data: currentBalances.data.filter( + updating: valenceBalances.updating, + data: valenceBalances.data.filter( ({ token }) => token.chainId === chainId ), } @@ -285,6 +192,14 @@ const Component: ActionComponent = ( disabled={!props.isCreating} fieldName={props.fieldNamePrefix + 'chainId'} includeChainIds={VALENCE_SUPPORTED_CHAINS} + onChange={() => { + // Reset new valence account funds when switching chain. + setValue( + (props.fieldNamePrefix + + 'newValenceAccount.funds') as 'newValenceAccount.funds', + [] + ) + }} /> )} @@ -292,7 +207,38 @@ const Component: ActionComponent = ( { + // Subtract service fee from balance for corresponding + // token to ensure that they leave enough for the fee. + // This value is used as the input max. + let balance = + !serviceFee.errored && + serviceFee.data && + tokensEqual(data.token, serviceFee.data.token) + ? HugeDecimal.from(_balance).minus( + serviceFee.data.balance + ) + : HugeDecimal.from(_balance) + if (balance.lt(0)) { + balance = HugeDecimal.zero + } + + return { + ...data, + balance: balance.toFixed(0), + } + } + ), + }, + serviceFee, + currentValenceBalances, baseDenomWhitelistTokens: whitelists.loading || whitelists.errored ? { loading: true } @@ -385,9 +331,16 @@ export class ConfigureRebalancerAction extends ActionBase { - if (!valenceAccount) { - throw new Error('Missing valence account.') + const chainConfig = getSupportedChainConfig(chainId) + if (!chainConfig?.codeIds?.ValenceAccount || !chainConfig?.valence) { + throw new Error(this.options.t('error.unsupportedValenceChain')) } - const rebalancer = mustGetSupportedChainConfig(chainId).valence?.rebalancer - if (!rebalancer) { - throw new Error('Missing rebalancer address.') + const sender = getChainAddressForActionOptions(this.options, chainId) + if (!sender) { + throw new Error( + this.options.t('error.failedToFindChainAccount', { + chain: getDisplayNameForChainId(chainId), + }) + ) } + const { + valence: { rebalancer, servicesManager }, + codeIds: { ValenceAccount }, + } = chainConfig + + const valenceAccountAddress = this.existingValenceAccount + ? this.existingValenceAccount.address + : newValenceAccount.creating + ? // Compute predicted valence account address if we're creating it. + await this.options.queryClient.fetchQuery( + contractQueries.instantiate2Address(this.options.queryClient, { + chainId, + creator: sender, + codeId: ValenceAccount, + salt: VALENCE_INSTANTIATE2_SALT, + }) + ) + : undefined + if (!valenceAccountAddress) { + throw new Error('Missing valence account address.') + } + + const rebalancerConfig = this.existingValenceAccount + ? await this.options.queryClient + .fetchQuery( + valenceRebalancerQueries.getConfig({ + chainId, + contractAddress: rebalancer, + args: { + addr: valenceAccountAddress, + }, + }) + ) + // This will error when no rebalancer is configured. + .catch(() => null) + : undefined + const whitelists = await this.options.queryClient.fetchQuery( valenceRebalancerExtraQueries.whitelistGenericTokens( this.options.queryClient, @@ -456,20 +451,79 @@ export class ConfigureRebalancerAction extends ActionBase + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) + + // Add service fee to funds. + if (serviceFee && serviceFee.balance !== '0') { + const existing = convertedFunds.find( + (f) => f.denom === serviceFee.token.denomOrAddress + ) + if (existing) { + existing.amount = HugeDecimal.from(existing.amount) + .plus(serviceFee.balance) + .toFixed(0) + } else { + convertedFunds.push( + HugeDecimal.from(serviceFee.balance).toCoin( + serviceFee.token.denomOrAddress + ) + ) + } + } + + msgs.push( + makeStargateMessage({ + stargate: { + typeUrl: MsgInstantiateContract2.typeUrl, + value: MsgInstantiateContract2.fromPartial({ + sender, + admin: sender, + codeId: BigInt(ValenceAccount), + label: 'Valence Account', + msg: toUtf8( + JSON.stringify({ + services_manager: servicesManager, + } as ValenceAccountInstantiateMsg) + ), + funds: convertedFunds + // Neutron errors with `invalid coins` if the funds list is not + // alphabetized. + .sort((a, b) => a.denom.localeCompare(b.denom)), + salt: toUtf8(VALENCE_INSTANTIATE2_SALT), + fixMsg: false, + }), + }, + }) + ) + } + + msgs.push( makeWasmMessage({ wasm: { execute: { - contract_addr: valenceAccount.address, + contract_addr: valenceAccountAddress, funds: [], msg: { // If rebalancer already exists, update it. Otherwise, // register it. - [valenceAccount.config.rebalancer - ? 'update_service' - : 'register_to_service']: { + [rebalancerConfig ? 'update_service' : 'register_to_service']: { service_name: 'rebalancer', data: encodeJsonToBase64({ // Common options. @@ -503,7 +557,7 @@ export class ConfigureRebalancerAction extends ActionBase), // Differences between data and update. - ...(valenceAccount.config.rebalancer + ...(rebalancerConfig ? ({ trustee: trustee ? { set: trustee } : 'clear', } as Partial) @@ -517,14 +571,50 @@ export class ConfigureRebalancerAction extends ActionBase { + const isCrossChain = _messages[0].isCrossChain + const messages = isCrossChain ? _messages[0].wrappedMessages : _messages + + const firstMessageIsCreateValenceAccount = + !!getSupportedChainConfig(messages[0].account.chainId)?.codeIds + ?.ValenceAccount && + isDecodedStargateMsg( + messages[0].decodedMessage, + MsgInstantiateContract2 + ) && + fromUtf8(messages[0].decodedMessage.stargate.value.salt) === + VALENCE_INSTANTIATE2_SALT + + // Verify configure rebalancer message immediately following. If cross chain + // message, exactly two should exist since we don't want to match this + // action if the cross-chain execution does anything else. Otherwise, we + // need at least two messages. + const requiredMessageCount = firstMessageIsCreateValenceAccount ? 2 : 1 + if ( + (isCrossChain && messages.length !== requiredMessageCount) || + (!isCrossChain && messages.length < requiredMessageCount) + ) { + return false + } + + const { decodedMessage, account: { chainId }, - }, - ]: ProcessedMessage[]): Promise { + } = messages[firstMessageIsCreateValenceAccount ? 1 : 0] + let serviceName: string | undefined let data: RebalancerData | RebalancerUpdateData | undefined if ( @@ -568,15 +658,27 @@ export class ConfigureRebalancerAction extends ActionBase { + const messages = _messages[0].isCrossChain + ? _messages[0].wrappedMessages + : _messages + + const isCreating = messages.length === 2 + + // Configure rebalancer message. + const { decodedMessage, account: { chainId }, - }, - ]: ProcessedMessage[]): Promise { + } = messages[isCreating ? 1 : 0] + let data: RebalancerData | RebalancerUpdateData | undefined if ( objectMatchesStructure(decodedMessage, { @@ -647,7 +749,35 @@ export class ConfigureRebalancerAction extends ActionBase { + const token = await this.options.queryClient.fetchQuery( + tokenQueries.info(this.options.queryClient, { + chainId, + type: TokenType.Native, + denomOrAddress: denom, + }) + ) + + return { + denom, + amount: HugeDecimal.from(amount).toHumanReadableString( + token.decimals + ), + decimals: token.decimals, + } + }) + ) + return { + newValenceAccount: { + creating: isCreating, + funds: newValenceAccountFunds, + }, chainId, trustee: typeof data.trustee === 'string' diff --git a/packages/stateful/actions/core/actions/CreateValenceAccount/Component.tsx b/packages/stateful/actions/core/actions/CreateValenceAccount/Component.tsx index 14bd8c452..5cd304e10 100644 --- a/packages/stateful/actions/core/actions/CreateValenceAccount/Component.tsx +++ b/packages/stateful/actions/core/actions/CreateValenceAccount/Component.tsx @@ -117,6 +117,12 @@ export const CreateValenceAccountComponent: ActionComponent< ? () => removeCoin(index) : undefined } + overrideInsufficientFundsWarning={(amount, tokenSymbol) => + t('error.insufficientFundsWarningMinusServiceFee', { + amount, + tokenSymbol, + }) + } tokens={nativeBalances} /> ))} diff --git a/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx b/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx index 846a09dec..eca80198c 100644 --- a/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx +++ b/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx @@ -133,15 +133,17 @@ const Component: ActionComponent = (props) => { !serviceFee.errored && serviceFee.data && tokensEqual(data.token, serviceFee.data.token) - ? BigInt(_balance) - BigInt(serviceFee.data.balance) - : BigInt(_balance) - if (balance < 0n) { - balance = 0n + ? HugeDecimal.from(_balance).minus( + serviceFee.data.balance + ) + : HugeDecimal.from(_balance) + if (balance.lt(0)) { + balance = HugeDecimal.zero } return { ...data, - balance: balance.toString(), + balance: balance.toFixed(0), } } ), diff --git a/packages/stateful/actions/core/actions/index.ts b/packages/stateful/actions/core/actions/index.ts index d49357341..699dc1121 100644 --- a/packages/stateful/actions/core/actions/index.ts +++ b/packages/stateful/actions/core/actions/index.ts @@ -14,7 +14,7 @@ export * from './CreateDao' export * from './CreateIca' export * from './CreateNftCollection' export * from './CreateRewardDistribution' -export * from './CreateValenceAccount' +// export * from './CreateValenceAccount' export * from './CrossChainExecute' export * from './Custom' export * from './DaoAdminExec' diff --git a/packages/stateful/actions/core/categories/valence.ts b/packages/stateful/actions/core/categories/valence.ts index bcdf150c7..884de3ee0 100644 --- a/packages/stateful/actions/core/categories/valence.ts +++ b/packages/stateful/actions/core/categories/valence.ts @@ -21,7 +21,7 @@ export const makeValenceActionCategory: ActionCategoryMaker = (options) => ActionKey.WithdrawFromRebalancer, ActionKey.PauseRebalancer, ActionKey.ResumeRebalancer, - ActionKey.CreateValenceAccount, + // ActionKey.CreateValenceAccount, ], } : null diff --git a/packages/stateful/actions/hooks/useTokenBalances.ts b/packages/stateful/actions/hooks/useTokenBalances.ts index f93e3aeb3..3a9d75115 100644 --- a/packages/stateful/actions/hooks/useTokenBalances.ts +++ b/packages/stateful/actions/hooks/useTokenBalances.ts @@ -38,6 +38,10 @@ export type UseTokenBalancesOptions = { * before they can be used in actions, so this defaults to excluding them. */ excludeAccountTypes?: AccountType[] + /** + * Include only these chain IDs. + */ + includeChainIds?: string[] } // Get native and cw20 token unstaked balances for the current context account. @@ -46,6 +50,7 @@ export const useTokenBalances = ({ additionalTokens, includeAccountTypes, excludeAccountTypes = [AccountType.Valence], + includeChainIds, }: UseTokenBalancesOptions = {}): LoadingData< GenericTokenBalanceWithOwner[] > => { @@ -78,6 +83,7 @@ export const useTokenBalances = ({ ignoreStaked: true, includeAccountTypes, excludeAccountTypes, + includeChainIds, }), [], (error) => console.error(error) diff --git a/packages/stateless/components/actions/NativeCoinSelector.tsx b/packages/stateless/components/actions/NativeCoinSelector.tsx index 7bbbe556f..52e569a09 100644 --- a/packages/stateless/components/actions/NativeCoinSelector.tsx +++ b/packages/stateless/components/actions/NativeCoinSelector.tsx @@ -52,6 +52,13 @@ export type NativeCoinSelectorProps = Pick< * Defaults to false. */ noCustomToken?: boolean + /** + * Override insufficient funds warning. + */ + overrideInsufficientFundsWarning?: ( + amount: string, + tokenSymbol: string + ) => string } type NativeCoinForm = { @@ -76,6 +83,7 @@ export const NativeCoinSelector = ({ min, noBalanceWarning = false, noCustomToken = false, + overrideInsufficientFundsWarning, }: NativeCoinSelectorProps) => { const { t } = useTranslation() @@ -106,6 +114,14 @@ export const NativeCoinSelector = ({ ? watchDecimals : selectedToken?.token.decimals || 0 + const insufficientFundsWarning = + overrideInsufficientFundsWarning || + ((amount, tokenSymbol) => + t('error.insufficientFundsWarning', { + amount, + tokenSymbol, + })) + // A warning if the denom was not found in the treasury or the amount is too // high. We don't want to make this an error because often people want to // spend funds that a previous action makes available, so just show a warning. @@ -120,12 +136,12 @@ export const NativeCoinSelector = ({ : !selectedToken ? t('error.unknownDenom', { denom: watchDenom }) : balance.toHumanReadable(decimals).lt(watchAmount) - ? t('error.insufficientFundsWarning', { - amount: balance.toInternationalizedHumanReadableString({ + ? insufficientFundsWarning( + balance.toInternationalizedHumanReadableString({ decimals, }), - tokenSymbol: symbol, - }) + symbol + ) : undefined const minUnit = HugeDecimal.one.toHumanReadableNumber(decimals)