diff --git a/README.md b/README.md index 239c69414..974118dc2 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ If something is misconfigured, check out the docs for | [`dispatch`](./packages/dispatch) | DAO DAO Dispatch. | | [`email`](./packages/email) | Email template and generator tools. | | [`i18n`](./packages/i18n) | Internationalization/translation system. | +| [`math`](./packages/math) | Math utilities. | | [`state`](./packages/state) | State retrieval and management for the DAO DAO UI. | | [`stateful`](./packages/stateful) | Stateful components, hooks, and systems that access and manipulate live data. | | [`stateless`](./packages/stateless) | React components, React hooks, and other stateless rendering utilities which do not access live data. | diff --git a/package.json b/package.json index cab1c5483..6406a0960 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "utils": "yarn workspace @dao-dao/utils", "email": "yarn workspace @dao-dao/email", "dispatch": "yarn workspace @dao-dao/dispatch", + "math": "yarn workspace @dao-dao/math", "publish-all": "find . -name '*.log' -delete && lerna publish --no-private from-package" }, "devDependencies": { diff --git a/packages/i18n/locales/en/translation.json b/packages/i18n/locales/en/translation.json index c037d87e1..81cdbd236 100644 --- a/packages/i18n/locales/en/translation.json +++ b/packages/i18n/locales/en/translation.json @@ -1618,6 +1618,7 @@ "addedToProfile": "Added to profile successfully.", "addedToken": "Added token to Keplr.", "claimedRewards": "Claimed rewards.", + "claimedTokens": "Claimed {{amount}} ${{tokenSymbol}}.", "compensationCycleCompleted_noProposal": "Compensation cycle completed.", "compensationCycleCompleted_withProposal": "Compensation cycle completed. Redirecting to proposal...", "compensationCycleCreated": "Compensation cycle created.", @@ -1649,10 +1650,12 @@ "restaked": "Restaked successfully.", "saved": "Saved successfully.", "staked": "Staked successfully.", + "stakedTokens": "Staked {{amount}} ${{tokenSymbol}}.", "tokenSwapContractInstantiated": "Token swap created successfully.", "transactionExecuted": "Transaction executed successfully.", "unregistered": "Unregistered successfully.", "unstaked": "Unstaked successfully.", + "unstakedTokens": "Unstaked {{amount}} ${{tokenSymbol}}.", "voteCast": "Vote successfully cast.", "withdrewPayment": "Withdrew payment." }, diff --git a/packages/math/.eslintrc.js b/packages/math/.eslintrc.js new file mode 100644 index 000000000..ba31cb99a --- /dev/null +++ b/packages/math/.eslintrc.js @@ -0,0 +1,10 @@ +// @ts-check + +/** @type {import("eslint").Linter.Config} */ +const eslintConfig = { + extends: [require.resolve('@dao-dao/config/eslint')], + ignorePatterns: ['node_modules'], + root: true, +} + +module.exports = eslintConfig diff --git a/packages/math/HugeDecimal.test.ts b/packages/math/HugeDecimal.test.ts new file mode 100644 index 000000000..8b1b29aa9 --- /dev/null +++ b/packages/math/HugeDecimal.test.ts @@ -0,0 +1,161 @@ +import { HugeDecimal } from './HugeDecimal' + +test('HugeDecimal', () => { + expect( + HugeDecimal.fromHumanReadable( + 1234.5678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + }) + ).toBe('1,234.5678') + + expect( + HugeDecimal.fromHumanReadable( + 1234.5678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + minDecimals: 6, + }) + ).toBe('1,234.567800') + + expect( + HugeDecimal.fromHumanReadable( + 1234.5678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + }) + ).toBe('1.23K') + + expect( + HugeDecimal.fromHumanReadable( + 1234.5678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + minDecimals: 0, + }) + ).toBe('1.23K') + + expect( + HugeDecimal.fromHumanReadable( + 1234.5678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + minDecimals: 4, + }) + ).toBe('1.2346K') + + expect( + HugeDecimal.fromHumanReadable( + 1234.5678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + minDecimals: 8, + }) + ).toBe('1.23456780K') + + expect( + HugeDecimal.fromHumanReadable( + 0.001, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + }) + ).toBe('0.001') + + expect( + HugeDecimal.fromHumanReadable(1, 6).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + }) + ).toBe('1') + + expect( + HugeDecimal.from( + '4901849977581594372686' + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: true, + }) + ).toBe('4,901,849,977,581,594.372686') + + expect( + HugeDecimal.from( + '4901849977581594372686' + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: true, + minDecimals: 8, + }) + ).toBe('4,901,849,977,581,594.37268600') + + expect( + HugeDecimal.from( + '4901849977581594372686' + ).toInternationalizedHumanReadableString({ + decimals: 0, + showFullAmount: false, + }) + ).toBe('4,901,849,977.58T') + + expect( + HugeDecimal.from( + // (BigInt(Number.MAX_SAFE_INTEGER) * 10000n).toString() + '90071992547409910000' + ).toInternationalizedHumanReadableString({ + decimals: 0, + showFullAmount: false, + }) + ).toBe('90,071,992.55T') + + expect( + HugeDecimal.from( + // (BigInt(Number.MAX_SAFE_INTEGER) * 10000n).toString() + '90071992547409910000' + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + }) + ).toBe('90.07T') + + expect( + HugeDecimal.from( + // (BigInt(Number.MAX_SAFE_INTEGER) * 10000n).toString() + '90071992547409910000' + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + minDecimals: 6, + }) + ).toBe('90.071993T') + + expect( + HugeDecimal.fromHumanReadable( + 11000027, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + }) + ).toBe('11.00M') + + expect( + HugeDecimal.fromHumanReadable( + 12345678, + 6 + ).toInternationalizedHumanReadableString({ + decimals: 6, + showFullAmount: false, + }) + ).toBe('12.35M') +}) diff --git a/packages/math/HugeDecimal.ts b/packages/math/HugeDecimal.ts new file mode 100644 index 000000000..62487202c --- /dev/null +++ b/packages/math/HugeDecimal.ts @@ -0,0 +1,382 @@ +import { BigNumber } from 'bignumber.js' + +const valueToBigNumber = (n: HugeDecimal.Value): BigNumber => + n instanceof BigNumber + ? n + : n instanceof HugeDecimal + ? n['value'] + : typeof n === 'bigint' + ? new BigNumber(n.toString()) + : typeof n === 'object' && 'amount' in n + ? valueToBigNumber(n.amount) + : new BigNumber(n) + +interface AmountWrapper { + amount: string +} + +interface Coin { + amount: string + denom: string +} + +export namespace HugeDecimal { + export type Value = + | BigNumber + | BigNumber.Value + | bigint + | HugeDecimal + | AmountWrapper +} + +export class HugeDecimal { + private value: BigNumber + + /** + * Returns a new instance of a HugeDecimal object with value `n`, where `n` is + * a numeric value in base 10. + * + * @param n the value + * @returns a HugeDecimal instance + */ + constructor(n: HugeDecimal.Value) { + this.value = valueToBigNumber(n) + } + + /** + * Returns a new instance of a HugeDecimal object with value `n`, where `n` is + * a numeric value in base 10. + * + * @param n the value + * @returns a HugeDecimal instance + */ + static from(n: HugeDecimal.Value) { + if (n instanceof HugeDecimal) { + return n + } + return new HugeDecimal(n) + } + + /** + * Create a HugeDecimal from a value that is in human-readable format, which + * means it has decimals and describes a human-readable token amount. For + * example: `1.000000 $NTRN`. + * + * This will convert the value to its raw integer representation by + * multiplying by 10^decimals and truncating any remaining decimal places. + * + * @param n the value + * @param decimals the number of decimals + * @returns a HugeDecimal instance + */ + static fromHumanReadable(n: HugeDecimal.Value, decimals: number) { + // Multiply by 10^decimals to convert to the integer representation. + return HugeDecimal.from(n).times(BigNumber(10).pow(decimals)).trunc() + } + + /** + * Returns a HugeDecimal whose value is the maximum of the arguments. + * + * @param n values + * @returns a HugeDecimal instance + */ + static max(...n: HugeDecimal.Value[]) { + return new HugeDecimal(BigNumber.max(...n.map(valueToBigNumber))) + } + + /** + * Returns a HugeDecimal whose value is the minimum of the arguments. + * + * @param n values + * @returns a HugeDecimal instance + */ + static min(...n: HugeDecimal.Value[]) { + return new HugeDecimal(BigNumber.min(...n.map(valueToBigNumber))) + } + + /** + * Returns a HugeDecimal instance with value 0. + */ + static get zero() { + return new HugeDecimal(0) + } + + /** + * Returns a HugeDecimal instance with value 1. + */ + static get one() { + return new HugeDecimal(1) + } + + toJSON() { + return this.value.toJSON() + } + + /** + * Returns the value of this HugeDecimal instance as a JavaScript primitive + * number. + * + * Pass `truncateDecimals` to truncate to a specific number of decimal places. + * Leaving it undefined will preserve all existing decimal places. + * + * @param truncateDecimals whether or not to truncate the decimal places + * @returns a number + */ + toNumber(truncateDecimals?: number) { + return truncateDecimals !== undefined + ? Number(this.toFixed(truncateDecimals)) + : this.value.toNumber() + } + + /** + * Returns a string in base-10 in normal notation. + */ + toString() { + return this.value.toString(10) + } + + /** + * Returns a string in normal notation with exactly `decimals` decimal places. + * + * @param decimals decimals + * @returns a string + */ + toFixed(decimals: number) { + return this.value.toFixed(decimals, BigNumber.ROUND_DOWN) + } + + valueOf() { + return this.value.valueOf() + } + + /** + * Returns the integer value of this HugeDecimal with decimals truncated. + */ + trunc() { + return new HugeDecimal(this.value.integerValue(BigNumber.ROUND_DOWN)) + } + + plus(n: HugeDecimal.Value) { + return new HugeDecimal(this.value.plus(valueToBigNumber(n))) + } + + minus(n: HugeDecimal.Value) { + return new HugeDecimal(this.value.minus(valueToBigNumber(n))) + } + + times(n: HugeDecimal.Value) { + return new HugeDecimal(this.value.times(valueToBigNumber(n))) + } + + div(n: HugeDecimal.Value) { + return new HugeDecimal(this.value.div(valueToBigNumber(n))) + } + + /** + * Returns whether or not the value is greater than zero. + */ + isPositive() { + return this.value.gt(0) + } + + isZero() { + return this.value.isZero() + } + + isNaN() { + return this.value.isNaN() + } + + lt(n: HugeDecimal.Value) { + return this.value.lt(valueToBigNumber(n)) + } + + lte(n: HugeDecimal.Value) { + return this.value.lte(valueToBigNumber(n)) + } + + gt(n: HugeDecimal.Value) { + return this.value.gt(valueToBigNumber(n)) + } + + gte(n: HugeDecimal.Value) { + return this.value.gte(valueToBigNumber(n)) + } + + eq(n: HugeDecimal.Value) { + return this.value.eq(valueToBigNumber(n)) + } + + pow(n: HugeDecimal.Value) { + return new HugeDecimal(this.value.pow(valueToBigNumber(n))) + } + + abs() { + return new HugeDecimal(this.value.abs()) + } + + /** + * Returns a HugeDecimal whose value is the value of this HugeDecimal negated, + * i.e. multiplied by -1. + */ + negated() { + return new HugeDecimal(this.value.negated()) + } + + /** + * Returns a HugeDecimal instance with `decimals` decimal places. + * + * @param decimals the number of decimal places + * @returns HugeDecimal instance + */ + toHumanReadable(decimals: number): HugeDecimal { + return new HugeDecimal( + this.div(BigNumber(10).pow(decimals)).toFixed(decimals) + ) + } + + /** + * Returns a human-readable number with `decimals` decimal places. + * + * @param decimals the number of decimal places + * @returns human-readable number + */ + toHumanReadableNumber(decimals: number): number { + return this.toHumanReadable(decimals).toNumber() + } + + /** + * Returns a human-readable string with `decimals` decimal places. + * + * @param decimals the number of decimal places + * @returns human-readable string + */ + toHumanReadableString(decimals: number): string { + return this.toHumanReadable(decimals).toString() + } + + /** + * Returns an internationalized human-readable string with abbreviation of + * large numbers. + * + * @returns an internationalized human-readable string + */ + toInternationalizedHumanReadableString({ + decimals = 0, + showFullAmount = true, + minDecimals = 0, + }: { + /** + * The number of decimals used to make this number human-readable. Defaults + * to 0. + */ + decimals?: number + /** + * Whether or not to show the full amount. Large numbers will be abbreviated + * if this is false. Defaults to true. + */ + showFullAmount?: boolean + /** + * The minimum number of decimal places to show. Defaults to the smallest + * number of non-zero decimal places less than or equal to `decimals`. + */ + minDecimals?: number + } = {}): string { + // Get the decimal separator for the current locale. + const decimalSeparator = (1.1).toLocaleString()[1] + + // Use BigInt for integer part of the number, and add the decimals manually. + // If the number is too large to fit within the size of Number, must use + // this BigInt approach even when not showing the full amount. + if (showFullAmount || this.gte(Number.MAX_SAFE_INTEGER)) { + const human = this.toHumanReadable(decimals) + + const int = human.trunc() + const dec = human.minus(int) + + // Show at least minDecimals, up to the exact number of decimal places in + // the original number, if showing the full amount. If not showing the + // full amount, this number must be very large (based on the conditional + // above), and thus none (0) of the actual decimal places are shown, since + // the large part of the number will be abbreviated (e.g. 1,234.5678 gets + // converted into 1.23K, and the 0.5678 are hidden). + const decimalPlacesToShow = showFullAmount + ? Math.max( + dec.value.toFormat({ decimalSeparator }).split(decimalSeparator)[1] + ?.length ?? 0, + minDecimals + ) + : 0 + + const intStr = BigInt(int.toFixed(0)).toLocaleString( + undefined, + showFullAmount + ? undefined + : { + notation: 'compact', + // Cap minDecimals to 20, which is the maximum allowed. + maximumFractionDigits: Math.min( + Math.max(2, minDecimals), + 20 + ) as 20, + } + ) + const decStr = + decimalPlacesToShow > 0 + ? decimalSeparator + + dec.value + .toFormat(decimalPlacesToShow, { decimalSeparator }) + .split(decimalSeparator)[1] + : '' + + return intStr + decStr + } + // If entire number can fit within the size of Number and not showing the + // full amount, use Number for compact internationalized formatting. + else { + const human = this.toHumanReadableNumber(decimals) + return human.toLocaleString(undefined, { + notation: 'compact', + minimumFractionDigits: minDecimals || (human >= 1000 ? 2 : undefined), + maximumFractionDigits: Math.max( + minDecimals, + human >= 1000 ? 2 : decimals + ), + }) + } + } + + /** + * Returns a Coin object with the amount and denom. + * + * @param denom the denom + * @returns Coin object + */ + toCoin(denom: string): Coin { + return { + amount: this.toFixed(0), + denom, + } + } + + /** + * Returns an array containing a single Coin object with the amount and denom. + * + * @param denom the denom + * @returns array containing a single Coin object + */ + toCoins(denom: string): Coin[] { + return [this.toCoin(denom)] + } + + /** + * Returns the USD value. + * + * @param decimals the number of decimals + * @param usdUnitPrice the USD unit price of the token (with decimals) + * @returns USD value + */ + toUsdValue(decimals: number, usdUnitPrice: BigNumber.Value) { + return this.toHumanReadable(decimals).times(usdUnitPrice).toNumber() + } +} diff --git a/packages/math/LICENSE b/packages/math/LICENSE new file mode 100644 index 000000000..61d94297f --- /dev/null +++ b/packages/math/LICENSE @@ -0,0 +1,32 @@ +The Clear BSD License + +Copyright (c) 2022 DAO DAO Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/math/README.md b/packages/math/README.md new file mode 100644 index 000000000..4bb7de743 --- /dev/null +++ b/packages/math/README.md @@ -0,0 +1,3 @@ +# @dao-dao/math + +Math utilities for DAO DAO. diff --git a/packages/math/index.ts b/packages/math/index.ts new file mode 100644 index 000000000..2336d4580 --- /dev/null +++ b/packages/math/index.ts @@ -0,0 +1 @@ +export * from './HugeDecimal' diff --git a/packages/math/jest.config.js b/packages/math/jest.config.js new file mode 100644 index 000000000..50200e3d7 --- /dev/null +++ b/packages/math/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config') diff --git a/packages/math/package.json b/packages/math/package.json new file mode 100644 index 000000000..da8542580 --- /dev/null +++ b/packages/math/package.json @@ -0,0 +1,20 @@ +{ + "name": "@dao-dao/math", + "version": "2.5.0-rc.3", + "license": "BSD-3-Clause-Clear", + "scripts": { + "format": "eslint . --fix", + "lint": "eslint .", + "test": "jest" + }, + "dependencies": { + "bignumber.js": "^9.1.2" + }, + "devDependencies": { + "@dao-dao/config": "2.5.0-rc.3", + "jest": "^29.1.1", + "typescript": "5.3.3" + }, + "prettier": "@dao-dao/config/prettier", + "gitHead": "d7b959f412c3990123b0e2afb11f32acd9c3764c" +} diff --git a/packages/math/tsconfig.json b/packages/math/tsconfig.json new file mode 100644 index 000000000..351541155 --- /dev/null +++ b/packages/math/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@dao-dao/config/ts/base.json", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/state/package.json b/packages/state/package.json index 55f24c659..8f7060505 100644 --- a/packages/state/package.json +++ b/packages/state/package.json @@ -17,6 +17,7 @@ "@cosmjs/proto-signing": "^0.32.3", "@cosmjs/stargate": "^0.32.3", "@cosmjs/tendermint-rpc": "^0.32.3", + "@dao-dao/math": "2.5.0-rc.3", "@dao-dao/utils": "2.5.0-rc.3", "@tanstack/react-query": "^5.40.0", "graphql": "^16.8.1", diff --git a/packages/state/query/queries/chain.ts b/packages/state/query/queries/chain.ts index 9d8241d10..f6d17cb6e 100644 --- a/packages/state/query/queries/chain.ts +++ b/packages/state/query/queries/chain.ts @@ -3,6 +3,7 @@ import { Coin } from '@cosmjs/stargate' import { QueryClient, queryOptions, skipToken } from '@tanstack/react-query' import uniq from 'lodash.uniq' +import { HugeDecimal } from '@dao-dao/math' import { AllGovParams, ChainId, @@ -1219,11 +1220,11 @@ export const fetchGovProposalVotes = async ( /** * Paginated votes with staked amounts. */ - votes: (Vote & { staked: bigint })[] + votes: (Vote & { staked: HugeDecimal })[] /** * Total votes cast. */ - total: number + total: HugeDecimal }> => { const client = await cosmosProtoRpcClientRouter.connect(chainId) @@ -1252,9 +1253,9 @@ export const fetchGovProposalVotes = async ( return { votes: votes.map((vote, index) => ({ ...vote, - staked: BigInt(stakes[index].amount), + staked: HugeDecimal.from(stakes[index].amount), })), - total: Number(pagination?.total ?? 0), + total: HugeDecimal.from(pagination?.total ?? 0), } } diff --git a/packages/state/query/queries/contracts/CwVesting.extra.ts b/packages/state/query/queries/contracts/CwVesting.extra.ts index 5c5bc6c0d..3c705e842 100644 --- a/packages/state/query/queries/contracts/CwVesting.extra.ts +++ b/packages/state/query/queries/contracts/CwVesting.extra.ts @@ -1,12 +1,12 @@ import { QueryClient, queryOptions } from '@tanstack/react-query' +import { HugeDecimal } from '@dao-dao/math' import { TokenType, VestingInfo, VestingStep, VestingValidatorWithSlashes, } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' import { chainQueries } from '../chain' import { indexerQueries } from '../indexer' @@ -115,7 +115,7 @@ export const fetchVestingPaymentInfo = async ( { slashes: [] as VestingValidatorWithSlashes[], hasUnregisteredSlashes: false, - actualSlashed: 0n, + actualSlashed: HugeDecimal.zero, }, // Promise.all([ // queryClient.fetchQuery( @@ -137,7 +137,7 @@ export const fetchVestingPaymentInfo = async ( // async ([stakeHistory, unbondingDurationSeconds]): Promise<{ // slashes: VestingValidatorWithSlashes[] // hasUnregisteredSlashes: boolean - // actualSlashed: bigint + // actualSlashed: HugeDecimal // }> => { // const uniqueValidators = uniq( // stakeHistory?.stakeEvents.flatMap((event) => @@ -181,10 +181,10 @@ export const fetchVestingPaymentInfo = async ( // (slashed, { slashes }) => // slashed + // slashes.reduce( - // (acc, { amount }) => acc + BigInt(amount), - // BigInt(0) + // (acc, { amount }) => acc.plus(amount), + // HugeDecimal.zero // ), - // BigInt(0) + // HugeDecimal.zero // ) // return { @@ -203,26 +203,24 @@ export const fetchVestingPaymentInfo = async ( ]) const actualStaked = delegationInfo.delegations.reduce( - (acc, { delegated }) => acc + BigInt(delegated.amount), - 0n + (acc, { delegated }) => acc.plus(delegated), + HugeDecimal.zero ) const actualUnstaking = delegationInfo.unbondingDelegations.reduce( - (acc, { balance }) => acc + BigInt(balance.amount), - 0n + (acc, { balance }) => acc.plus(balance), + HugeDecimal.zero ) // If cannot compute the actual slashed amount, then we cannot compute the // stakable amount, so default to 0 to prevent the UI from allowing staking. const stakable = actualSlashed === undefined - ? '0' - : ( - BigInt(total) - - BigInt(vest.claimed) - - BigInt(actualStaked) - - BigInt(actualUnstaking) - - actualSlashed - ).toString() + ? HugeDecimal.zero + : HugeDecimal.from(total) + .minus(vest.claimed) + .minus(actualStaked) + .minus(actualUnstaking) + .minus(actualSlashed) const completed = (vest.status === 'funded' || @@ -238,34 +236,22 @@ export const fetchVestingPaymentInfo = async ( ? [ { timestamp: startTimeMs, - amount: convertMicroDenomToDenomWithDecimals( - vest.vested.constant.y, - token.decimals - ), + amount: HugeDecimal.from(vest.vested.constant.y), }, { timestamp: startTimeMs, - amount: convertMicroDenomToDenomWithDecimals( - vest.vested.constant.y, - token.decimals - ), + amount: HugeDecimal.from(vest.vested.constant.y), }, ] : 'saturating_linear' in vest.vested ? [ { timestamp: startTimeMs + vest.vested.saturating_linear.min_x * 1000, - amount: convertMicroDenomToDenomWithDecimals( - vest.vested.saturating_linear.min_y, - token.decimals - ), + amount: HugeDecimal.from(vest.vested.saturating_linear.min_y), }, { timestamp: startTimeMs + vest.vested.saturating_linear.max_x * 1000, - amount: convertMicroDenomToDenomWithDecimals( - vest.vested.saturating_linear.max_y, - token.decimals - ), + amount: HugeDecimal.from(vest.vested.saturating_linear.max_y), }, ] : vest.vested.piecewise_linear.steps.reduce( @@ -279,10 +265,7 @@ export const fetchVestingPaymentInfo = async ( ...acc, { timestamp: startTimeMs + seconds * 1000, - amount: convertMicroDenomToDenomWithDecimals( - amount, - token.decimals - ), + amount: HugeDecimal.from(amount), }, ] }, @@ -297,9 +280,9 @@ export const fetchVestingPaymentInfo = async ( vest, token, owner, - vested, - distributable, - total, + vested: HugeDecimal.from(vested), + distributable: HugeDecimal.from(distributable), + total: HugeDecimal.from(total), stakable, slashes, hasUnregisteredSlashes, diff --git a/packages/state/query/queries/contracts/DaoRewardsDistributor.extra.ts b/packages/state/query/queries/contracts/DaoRewardsDistributor.extra.ts index 12aaa933e..7feb09201 100644 --- a/packages/state/query/queries/contracts/DaoRewardsDistributor.extra.ts +++ b/packages/state/query/queries/contracts/DaoRewardsDistributor.extra.ts @@ -1,6 +1,7 @@ import { QueryClient, queryOptions } from '@tanstack/react-query' import uniq from 'lodash.uniq' +import { HugeDecimal } from '@dao-dao/math' import { DaoRewardDistribution, GenericTokenBalanceAndValue, @@ -14,7 +15,6 @@ import { PendingRewardsResponse, } from '@dao-dao/types/contracts/DaoRewardsDistributor' import { - convertMicroDenomToDenomWithDecimals, deserializeTokenSource, getRewardDistributorStorageItemKey, serializeTokenSource, @@ -261,9 +261,9 @@ export const fetchPendingDaoRewards = async ( return distributions.map((distribution) => ({ distribution, - rewards: Number( + rewards: HugeDecimal.from( pending_rewards.find((pending) => pending.id === distribution.id) - ?.pending_rewards || '0' + ?.pending_rewards || 0 ), })) } @@ -301,16 +301,14 @@ export const fetchPendingDaoRewards = async ( // Sum all pending rewards for this token. const allPendingRewards = distributions.reduce( (acc, { distribution, rewards }) => - acc + (tokenSourcesEqual(token, distribution.token) ? rewards : 0), - 0 - ) - - const balance = convertMicroDenomToDenomWithDecimals( - allPendingRewards, - token.decimals + acc.plus( + tokenSourcesEqual(token, distribution.token) ? rewards : 0 + ), + HugeDecimal.zero ) - const usdValue = balance * usdPrice + const balance = allPendingRewards.toHumanReadableNumber(token.decimals) + const usdValue = allPendingRewards.toUsdValue(token.decimals, usdPrice) return { token, diff --git a/packages/state/query/queries/contracts/NeutronVotingRegistry.extra.ts b/packages/state/query/queries/contracts/NeutronVotingRegistry.extra.ts index e7b45f98a..8a89d7774 100644 --- a/packages/state/query/queries/contracts/NeutronVotingRegistry.extra.ts +++ b/packages/state/query/queries/contracts/NeutronVotingRegistry.extra.ts @@ -1,5 +1,6 @@ import { QueryClient, queryOptions } from '@tanstack/react-query' +import { HugeDecimal } from '@dao-dao/math' import { VotingVaultWithInfo } from '@dao-dao/types' import { neutronVaultQueries } from './NeutronVault' @@ -38,15 +39,17 @@ export const fetchNeutronVaultsWithInfo = async ( address: vault.address, }) ), - totalPower: ( - await queryClient.fetchQuery( - neutronVaultQueries.totalPowerAtHeight({ - chainId, - contractAddress: vault.address, - args: {}, - }) - ) - ).power, + totalPower: HugeDecimal.from( + ( + await queryClient.fetchQuery( + neutronVaultQueries.totalPowerAtHeight({ + chainId, + contractAddress: vault.address, + args: {}, + }) + ) + ).power + ), }) ) ) diff --git a/packages/state/query/queries/neutron.ts b/packages/state/query/queries/neutron.ts index 9a48944c5..8466f21f9 100644 --- a/packages/state/query/queries/neutron.ts +++ b/packages/state/query/queries/neutron.ts @@ -1,6 +1,7 @@ import { QueryClient, queryOptions } from '@tanstack/react-query' import uniq from 'lodash.uniq' +import { HugeDecimal } from '@dao-dao/math' import { ChainId, GenericTokenBalance, TokenType } from '@dao-dao/types' import { Fee as NeutronFee } from '@dao-dao/types/protobuf/codegen/neutron/feerefunder/fee' import { MAINNET, neutronProtoRpcClientRouter } from '@dao-dao/utils' @@ -43,7 +44,7 @@ export const fetchNeutronIbcTransferFee = async ( token: tokens.find((token) => token.denomOrAddress === denom)!, balance: fees .filter(({ denom: feeDenom }) => feeDenom === denom) - .reduce((acc, { amount }) => acc + BigInt(amount), 0n) + .reduce((acc, { amount }) => acc.plus(amount), HugeDecimal.zero) .toString(), })), } diff --git a/packages/state/recoil/selectors/chain.ts b/packages/state/recoil/selectors/chain.ts index 2ecc0fc11..ebfdae11e 100644 --- a/packages/state/recoil/selectors/chain.ts +++ b/packages/state/recoil/selectors/chain.ts @@ -735,7 +735,7 @@ export const validatorsSelector = selectorFamily>({ return validators .map((validator) => cosmosValidatorToValidator(validator)) - .sort((a, b) => b.tokens - a.tokens) + .sort((a, b) => b.tokens.minus(a.tokens).toNumber()) }, }) diff --git a/packages/state/recoil/selectors/skip.ts b/packages/state/recoil/selectors/skip.ts index 519691041..3a2049995 100644 --- a/packages/state/recoil/selectors/skip.ts +++ b/packages/state/recoil/selectors/skip.ts @@ -1,5 +1,6 @@ import { selectorFamily } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { GenericToken, SkipAsset, @@ -150,7 +151,7 @@ export const skipRouteSelector = selectorFamily< source_asset_denom: sourceDenom, dest_asset_chain_id: toChainId, dest_asset_denom: asset.denom, - amount_in: BigInt(amountIn).toString(), + amount_in: HugeDecimal.from(amountIn).toString(), }), }) ).json() diff --git a/packages/state/recoil/selectors/token.ts b/packages/state/recoil/selectors/token.ts index bbeaf2ed8..e955755f4 100644 --- a/packages/state/recoil/selectors/token.ts +++ b/packages/state/recoil/selectors/token.ts @@ -1,3 +1,4 @@ +import { uniqBy } from 'lodash' import { selectorFamily, waitForAll, @@ -5,6 +6,7 @@ import { waitForAny, } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Account, AmountWithTimestamp, @@ -15,12 +17,12 @@ import { TokenCardLazyInfo, TokenPriceHistoryRange, TokenType, + UnstakingTask, UnstakingTaskStatus, WithChainId, } from '@dao-dao/types' import { MAINNET, - convertMicroDenomToDenomWithDecimals, getChainForChainId, getChainForChainName, getIbcTransferInfoFromChannel, @@ -390,9 +392,9 @@ export const genericTokenUndelegatingBalancesSelector = selectorFamily< } acc.push(existing) } - existing.balance = ( - BigInt(existing.balance) + BigInt(balance) - ).toString() + existing.balance = HugeDecimal.from(existing.balance) + .plus(balance) + .toString() return acc }, [] as GenericTokenBalance[]) @@ -592,7 +594,7 @@ export const tokenCardLazyInfoSelector = selectorFamily< owner: string token: GenericToken // For calculating totalBalance. - unstakedBalance: number + unstakedBalance: string } >({ key: 'tokenCardLazyInfo', @@ -640,13 +642,10 @@ export const tokenCardLazyInfoSelector = selectorFamily< ) const unstakingTasks = unbondingDelegations.map( - ({ balance, finishesAt }) => ({ + ({ balance, finishesAt }): UnstakingTask => ({ token, status: UnstakingTaskStatus.Unstaking, - amount: convertMicroDenomToDenomWithDecimals( - balance.amount, - token.decimals - ), + amount: HugeDecimal.from(balance.amount), date: finishesAt, }) ) @@ -655,29 +654,29 @@ export const tokenCardLazyInfoSelector = selectorFamily< ({ validator, delegated, pendingReward }) => ({ token, validator, - amount: convertMicroDenomToDenomWithDecimals( - delegated.amount, - token.decimals - ), - rewards: convertMicroDenomToDenomWithDecimals( - pendingReward.amount, - token.decimals - ), + amount: HugeDecimal.from(delegated.amount), + rewards: HugeDecimal.from(pendingReward.amount), }) ) - const totalStaked = - stakes.reduce((acc, stake) => acc + stake.amount, 0) ?? 0 - const totalPendingRewards = - stakes?.reduce((acc, stake) => acc + stake.rewards, 0) ?? 0 - const totalUnstaking = - unstakingTasks.reduce( - (acc, task) => - acc + + const totalStaked = stakes.reduce( + (acc, stake) => acc.plus(stake.amount), + HugeDecimal.zero + ) + const totalPendingRewards = stakes.reduce( + (acc, stake) => acc.plus(stake.rewards), + HugeDecimal.zero + ) + const totalUnstaking = unstakingTasks.reduce( + (acc, task) => + acc.plus( // Only include balance of unstaking tasks. - (task.status === UnstakingTaskStatus.Unstaking ? task.amount : 0), - 0 - ) ?? 0 + task.status === UnstakingTaskStatus.Unstaking + ? task.amount + : HugeDecimal.zero + ), + HugeDecimal.zero + ) stakingInfo = { unstakingTasks, @@ -699,40 +698,30 @@ export const tokenCardLazyInfoSelector = selectorFamily< }) ) // Only include DAOs this owner has staked with. - .filter(({ stakedBalance }) => stakedBalance > 0) - .map(({ stakedBalance, ...rest }) => ({ - ...rest, - // Convert to expected denom. - stakedBalance: convertMicroDenomToDenomWithDecimals( - stakedBalance, - token.decimals - ), - })) + .filter(({ stakedBalance }) => stakedBalance.isPositive()) } - const totalBalance = - unstakedBalance + - // Add staked and unstaking balances. - (stakingInfo - ? stakingInfo.totalStaked + stakingInfo.totalUnstaking - : 0) + - // Add balances staked in DAOs, grouped by their - // `stakingContractAddress` so we don't double-count tokens staked with - // the same staking contract if that staking contract is used in - // different DAOs in the list. - Object.values( - daosGoverned?.reduce( - (acc, { stakingContractAddress, stakedBalance = 0 }) => ({ - ...acc, - // If the staking contract address is already in the accumulator, - // overwrite so we don't double-count. All staked balances for the - // same staking contract should be the same, so overwriting should - // do nothing. - [stakingContractAddress]: stakedBalance, - }), - {} as Record - ) || {} - ).reduce((acc, stakedBalance) => acc + stakedBalance, 0) + const totalBalance = HugeDecimal.from(unstakedBalance) + .plus( + // Add staked and unstaking balances. + stakingInfo + ? stakingInfo.totalStaked.plus(stakingInfo.totalUnstaking) + : 0 + ) + .plus( + // Add balances staked in DAOs, unique by their + // `stakingContractAddress` so we don't double-count tokens staked + // with the same staking contract if that staking contract is used in + // different DAOs in the list. + uniqBy( + daosGoverned || [], + ({ stakingContractAddress }) => stakingContractAddress + ).reduce( + (acc, { stakedBalance }) => + acc.plus(stakedBalance || HugeDecimal.zero), + HugeDecimal.zero + ) + ) return { usdUnitPrice, @@ -794,7 +783,7 @@ export const tokenDaosWithStakedBalanceSelector = selectorFamily< { coreAddress: string stakingContractAddress: string - stakedBalance: number + stakedBalance: HugeDecimal }[], WithChainId<{ type: TokenType @@ -854,10 +843,10 @@ export const tokenDaosWithStakedBalanceSelector = selectorFamily< .map(({ coreAddress, stakingContractAddress }, index) => ({ coreAddress, stakingContractAddress, - stakedBalance: Number(daosWalletStakedTokens[index]), + stakedBalance: HugeDecimal.from(daosWalletStakedTokens[index]), })) // Sort descending by staked tokens. - .sort((a, b) => b.stakedBalance - a.stakedBalance) + .sort((a, b) => b.stakedBalance.minus(a.stakedBalance).toNumber()) return daosWithBalances }, diff --git a/packages/state/recoil/selectors/treasury.ts b/packages/state/recoil/selectors/treasury.ts index 4c24afce5..bbd30e868 100644 --- a/packages/state/recoil/selectors/treasury.ts +++ b/packages/state/recoil/selectors/treasury.ts @@ -3,6 +3,7 @@ import { Event, IndexedTx } from '@cosmjs/stargate' import uniq from 'lodash.uniq' import { noWait, selectorFamily, waitForAll, waitForNone } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Account, AccountType, @@ -16,7 +17,6 @@ import { } from '@dao-dao/types' import { COMMUNITY_POOL_ADDRESS_PLACEHOLDER, - convertMicroDenomToDenomWithDecimals, getNativeTokenForChainId, getTokenForChainIdAndDenom, loadableToLoadingData, @@ -103,8 +103,8 @@ export interface TransformedTreasuryTransaction { timestamp: Date | undefined sender: string recipient: string - amount: number - denomLabel: string + amount: HugeDecimal + token: GenericToken outgoing: boolean } @@ -152,11 +152,8 @@ export const transformedTreasuryTransactionsSelector = selectorFamily< timestamp, sender, recipient, - amount: convertMicroDenomToDenomWithDecimals( - coin.amount, - token.decimals - ), - denomLabel: token.symbol, + amount: HugeDecimal.from(coin), + token, outgoing: sender === params.address, } }) @@ -248,11 +245,6 @@ export const treasuryTokenCardInfosForDaoSelector = selectorFamily< balance, isGovernanceToken = false, }): TokenCardInfo | [] => { - const unstakedBalance = convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ) - let hasStakingInfo = false // Staking info only exists for native token. if ( @@ -288,7 +280,7 @@ export const treasuryTokenCardInfosForDaoSelector = selectorFamily< tokenCardLazyInfoSelector({ owner: account.address, token, - unstakedBalance, + unstakedBalance: balance, }) ) ) @@ -297,12 +289,12 @@ export const treasuryTokenCardInfosForDaoSelector = selectorFamily< owner: account, token, isGovernanceToken, - unstakedBalance, + unstakedBalance: HugeDecimal.from(balance), hasStakingInfo, lazyInfo: loadableToLoadingData(lazyInfo, { usdUnitPrice: undefined, stakingInfo: undefined, - totalBalance: unstakedBalance, + totalBalance: HugeDecimal.from(balance), }), } } diff --git a/packages/state/recoil/selectors/wallet.ts b/packages/state/recoil/selectors/wallet.ts index 462f0d874..19dd680f2 100644 --- a/packages/state/recoil/selectors/wallet.ts +++ b/packages/state/recoil/selectors/wallet.ts @@ -7,6 +7,7 @@ import { waitForAny, } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { AccountTxSave, AccountType, @@ -25,7 +26,6 @@ import { INACTIVE_DAO_NAMES, KVPK_API_BASE, ME_SAVED_TX_PREFIX, - convertMicroDenomToDenomWithDecimals, getFallbackImage, getNativeTokenForChainId, loadableToLoadingData, @@ -357,24 +357,19 @@ export const walletTokenCardInfosSelector = selectorFamily< const infos: TokenCardInfo[] = [ ...nativeBalances.flatMap((accountBalances, accountIndex) => accountBalances.map(({ token, balance }) => { - const unstakedBalance = convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ) - // Staking info only exists for native token. const hasStakingInfo = token.denomOrAddress === getNativeTokenForChainId(chainId).denomOrAddress && // Check if anything staked. - Number( + HugeDecimal.from( get( nativeDelegatedBalanceSelector({ address: walletAddress, chainId, }) ).amount - ) > 0 + ).isPositive() const owner = allAccounts[accountIndex] @@ -383,7 +378,7 @@ export const walletTokenCardInfosSelector = selectorFamily< tokenCardLazyInfoSelector({ owner: owner.address, token, - unstakedBalance, + unstakedBalance: balance, }) ) ) @@ -392,12 +387,12 @@ export const walletTokenCardInfosSelector = selectorFamily< owner, token, isGovernanceToken: false, - unstakedBalance, + unstakedBalance: HugeDecimal.from(balance), hasStakingInfo, lazyInfo: loadableToLoadingData(lazyInfo, { usdUnitPrice: undefined, stakingInfo: undefined, - totalBalance: unstakedBalance, + totalBalance: HugeDecimal.from(balance), }), } @@ -410,9 +405,8 @@ export const walletTokenCardInfosSelector = selectorFamily< return [] } - const unstakedBalance = convertMicroDenomToDenomWithDecimals( - cw20Contracts[index].balance || '0', - token.decimals + const unstakedBalance = HugeDecimal.from( + cw20Contracts[index].balance || 0 ) const lazyInfo = get( @@ -420,7 +414,7 @@ export const walletTokenCardInfosSelector = selectorFamily< tokenCardLazyInfoSelector({ owner: walletAddress, token, - unstakedBalance, + unstakedBalance: unstakedBalance.toString(), }) ) ) diff --git a/packages/stateful/actions/core/actions/AuthzGrantRevoke/Component.tsx b/packages/stateful/actions/core/actions/AuthzGrantRevoke/Component.tsx index e0e803000..e6ed46bf6 100644 --- a/packages/stateful/actions/core/actions/AuthzGrantRevoke/Component.tsx +++ b/packages/stateful/actions/core/actions/AuthzGrantRevoke/Component.tsx @@ -11,7 +11,7 @@ import { InputErrorMessage, InputLabel, NativeCoinSelector, - NumberInput, + NumericInput, RadioInput, SegmentedControlsTitle, SelectInput, @@ -69,7 +69,7 @@ export const AuthzGrantRevokeComponent: ActionComponent< const { t } = useTranslation() const { chain_id: chainId, bech32_prefix: bech32Prefix } = useChain() - const { control, register, setValue, watch } = + const { control, register, setValue, getValues, watch } = useFormContext() const { @@ -255,7 +255,7 @@ export const AuthzGrantRevokeComponent: ActionComponent< className="mt-2 self-start" onClick={() => appendCoin({ - amount: 1, + amount: '1', denom: nativeToken.denomOrAddress, decimals: nativeToken.decimals, }) @@ -413,19 +413,21 @@ export const AuthzGrantRevokeComponent: ActionComponent< tooltip={t('form.callsDescription')} /> - + )} @@ -465,7 +467,7 @@ export const AuthzGrantRevokeComponent: ActionComponent< className="mt-2 self-start" onClick={() => appendCoin({ - amount: 1, + amount: '1', denom: nativeToken.denomOrAddress, decimals: nativeToken.decimals, }) diff --git a/packages/stateful/actions/core/actions/AuthzGrantRevoke/index.tsx b/packages/stateful/actions/core/actions/AuthzGrantRevoke/index.tsx index 17af40a01..8a30f4f8d 100644 --- a/packages/stateful/actions/core/actions/AuthzGrantRevoke/index.tsx +++ b/packages/stateful/actions/core/actions/AuthzGrantRevoke/index.tsx @@ -2,6 +2,7 @@ import { fromUtf8, toUtf8 } from '@cosmjs/encoding' import JSON5 from 'json5' import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { tokenQueries } from '@dao-dao/state/query' import { ActionBase, @@ -41,8 +42,6 @@ import { } from '@dao-dao/types/protobuf/codegen/cosmwasm/wasm/v1/authz' import { Any } from '@dao-dao/types/protobuf/codegen/google/protobuf/any' import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, getChainAddressForActionOptions, isDecodedStargateMsg, maybeMakePolytoneExecuteMessages, @@ -162,10 +161,9 @@ export class AuthzGrantRevokeAction extends ActionBase { callsRemaining: BigInt(calls), // MaxFundsLimit // CombinedLimit - amounts: funds.map(({ denom, amount, decimals }) => ({ - amount: convertDenomToMicroDenomStringWithDecimals(amount, decimals), - denom, - })), + amounts: funds.map(({ denom, amount, decimals }) => + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ), }) let authorization: Any | undefined @@ -177,7 +175,7 @@ export class AuthzGrantRevokeAction extends ActionBase { msg: msgTypeUrl, // SendAuthorization spendLimit: funds.map(({ denom, amount, decimals }) => ({ - amount: convertDenomToMicroDenomStringWithDecimals(amount, decimals), + amount: HugeDecimal.fromHumanReadable(amount, decimals).toString(), denom, })), allowList: [], @@ -381,10 +379,8 @@ export class AuthzGrantRevokeAction extends ActionBase { tokens.find((t) => t.denomOrAddress === denom)?.decimals || 0 return { denom, - amount: convertMicroDenomToDenomWithDecimals( - amount, - decimals - ), + amount: + HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, } }) ?? [], @@ -424,10 +420,8 @@ export class AuthzGrantRevokeAction extends ActionBase { tokens.find((t) => t.denomOrAddress === denom)?.decimals || 0 return { denom, - amount: convertMicroDenomToDenomWithDecimals( - amount, - decimals - ), + amount: + HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, } }) ?? [], diff --git a/packages/stateful/actions/core/actions/AuthzGrantRevoke/types.ts b/packages/stateful/actions/core/actions/AuthzGrantRevoke/types.ts index ba76531d8..93052a2dd 100644 --- a/packages/stateful/actions/core/actions/AuthzGrantRevoke/types.ts +++ b/packages/stateful/actions/core/actions/AuthzGrantRevoke/types.ts @@ -33,7 +33,7 @@ export type AuthzGrantRevokeData = { contract: string funds: { denom: string - amount: number + amount: string // Will multiply `amount` by 10^decimals when generating the message. decimals: number }[] diff --git a/packages/stateful/actions/core/actions/BecomeApprover/Component.tsx b/packages/stateful/actions/core/actions/BecomeApprover/Component.tsx index 7da238479..2494b4858 100644 --- a/packages/stateful/actions/core/actions/BecomeApprover/Component.tsx +++ b/packages/stateful/actions/core/actions/BecomeApprover/Component.tsx @@ -2,12 +2,7 @@ import { ComponentType } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { - ErrorPage, - Loader, - RadioInput, - useDaoInfoContext, -} from '@dao-dao/stateless' +import { ErrorPage, Loader, RadioInput, useDao } from '@dao-dao/stateless' import { LoadingDataWithError, StatefulEntityDisplayProps, @@ -34,7 +29,7 @@ export const BecomeApproverComponent: ActionComponent< BecomeApproverOptions > = ({ fieldNamePrefix, isCreating, options: { options, EntityDisplay } }) => { const { t } = useTranslation() - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const { watch, setValue } = useFormContext() const dao = watch((fieldNamePrefix + 'dao') as 'dao') diff --git a/packages/stateful/actions/core/actions/BulkImport/Component.tsx b/packages/stateful/actions/core/actions/BulkImport/Component.tsx index f1a254dd6..449a41abc 100644 --- a/packages/stateful/actions/core/actions/BulkImport/Component.tsx +++ b/packages/stateful/actions/core/actions/BulkImport/Component.tsx @@ -152,16 +152,13 @@ export const BulkImportComponent: ActionComponent = ({ try { // Existence validated above. const action = actionMap[key as keyof typeof actionMap]! + const actionData = merge({}, cloneDeep(action.defaults), data) return { action, // Use the action's defaults as a base, and then merge in the // imported data, overriding any defaults. If data is undefined, // then the action's defaults will be used. - data: merge( - {}, - cloneDeep(action.defaults), - action.transformImportData?.(data) || data - ), + data: action.transformImportData?.(actionData) || actionData, } } catch { return [] diff --git a/packages/stateful/actions/core/actions/CommunityPoolDeposit/Component.tsx b/packages/stateful/actions/core/actions/CommunityPoolDeposit/Component.tsx index 051237699..354f881b3 100644 --- a/packages/stateful/actions/core/actions/CommunityPoolDeposit/Component.tsx +++ b/packages/stateful/actions/core/actions/CommunityPoolDeposit/Component.tsx @@ -1,6 +1,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { InputErrorMessage, TokenAmountDisplay, @@ -8,11 +9,10 @@ import { } from '@dao-dao/stateless' import { GenericTokenBalance, LoadingData } from '@dao-dao/types' import { ActionComponent } from '@dao-dao/types/actions' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' export type CommunityPoolDepositData = { chainId: string - amount: number + amount: string denom: string _error?: string } @@ -26,7 +26,7 @@ export const CommunityPoolDepositComponent: ActionComponent< > = ({ fieldNamePrefix, isCreating, errors, options: { tokens } }) => { const { t } = useTranslation() - const { register, watch, setValue } = + const { register, watch, setValue, getValues } = useFormContext() const spendChainId = watch((fieldNamePrefix + 'chainId') as 'chainId') @@ -40,10 +40,7 @@ export const CommunityPoolDepositComponent: ActionComponent< token.chainId === spendChainId && token.denomOrAddress === spendDenom ) const selectedDecimals = selectedToken?.token.decimals ?? 0 - const selectedBalance = convertMicroDenomToDenomWithDecimals( - selectedToken?.balance ?? 0, - selectedDecimals - ) + const selectedBalance = HugeDecimal.from(selectedToken?.balance ?? 0) // 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 @@ -54,10 +51,10 @@ export const CommunityPoolDepositComponent: ActionComponent< ? undefined : !selectedToken ? t('error.unknownDenom', { denom: spendDenom }) - : spendAmount > selectedBalance + : selectedBalance.toHumanReadable(selectedDecimals).lt(spendAmount) ? t('error.insufficientFundsWarning', { - amount: selectedBalance.toLocaleString(undefined, { - maximumFractionDigits: selectedDecimals, + amount: selectedBalance.toInternationalizedHumanReadableString({ + decimals: selectedDecimals, }), tokenSymbol: symbol, }) @@ -70,11 +67,12 @@ export const CommunityPoolDepositComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'amount') as 'amount', error: errors?.amount, - min: convertMicroDenomToDenomWithDecimals(1, selectedDecimals), - step: convertMicroDenomToDenomWithDecimals(1, selectedDecimals), + min: HugeDecimal.one.toHumanReadableNumber(selectedDecimals), + step: HugeDecimal.one.toHumanReadableNumber(selectedDecimals), }} onSelectToken={({ chainId, denomOrAddress }) => { setValue((fieldNamePrefix + 'chainId') as 'chainId', chainId) @@ -92,11 +90,10 @@ export const CommunityPoolDepositComponent: ActionComponent< description: t('title.balance') + ': ' + - convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + HugeDecimal.from( + balance + ).toInternationalizedHumanReadableString({ + decimals: token.decimals, }), })), } diff --git a/packages/stateful/actions/core/actions/CommunityPoolDeposit/index.tsx b/packages/stateful/actions/core/actions/CommunityPoolDeposit/index.tsx index bc335e768..c9c37c0f8 100644 --- a/packages/stateful/actions/core/actions/CommunityPoolDeposit/index.tsx +++ b/packages/stateful/actions/core/actions/CommunityPoolDeposit/index.tsx @@ -1,6 +1,6 @@ -import { coins } from '@cosmjs/stargate' import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { tokenQueries } from '@dao-dao/state/query' import { ActionBase, DownArrowEmoji } from '@dao-dao/stateless' import { @@ -18,8 +18,6 @@ import { } from '@dao-dao/types/actions' import { MsgFundCommunityPool } from '@dao-dao/types/protobuf/codegen/cosmos/distribution/v1beta1/tx' import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, getChainAddressForActionOptions, isDecodedStargateMsg, maybeMakePolytoneExecuteMessages, @@ -85,7 +83,7 @@ export class CommunityPoolDepositAction extends ActionBase() + const { + control, + watch, + register, + setValue, + getValues, + clearErrors, + setError, + } = useFormContext() const { fields: tokensFields, append: appendToken, @@ -230,10 +237,12 @@ export const ConfigureRebalancerComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + `tokens.${index}.percent`) as `tokens.${number}.percent`, error: errors?.tokens?.[index]?.percent, + numericValue: true, min: 0.01, max: 100, step: 0.01, @@ -332,7 +341,7 @@ export const ConfigureRebalancerComponent: ActionComponent< ? undefined : { denom: tokens[0]?.denom ?? '', - amount: 1, + amount: '1', } ) } @@ -350,16 +359,15 @@ export const ConfigureRebalancerComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'minBalance.amount') as 'minBalance.amount', error: errors?.minBalance?.amount, - min: convertMicroDenomToDenomWithDecimals( - 1, + min: HugeDecimal.one.toHumanReadableNumber( minBalanceToken?.decimals ?? 0 ), - step: convertMicroDenomToDenomWithDecimals( - 1, + step: HugeDecimal.one.toHumanReadableNumber( minBalanceToken?.decimals ?? 0 ), }} @@ -448,12 +456,13 @@ export const ConfigureRebalancerComponent: ActionComponent<
{/* eslint-disable-next-line i18next/no-literal-string */} - {/* eslint-disable-next-line i18next/no-literal-string */} - {/* eslint-disable-next-line i18next/no-literal-string */} - -
)} @@ -634,7 +646,7 @@ export const ConfigureRebalancerComponent: ActionComponent< nativeBalances.data.find( ({ token }) => token.denomOrAddress === denom ) ?? {} - const balance = Number(_balance) || 0 + const balance = HugeDecimal.from(_balance || 0) const price = prices.data.find( ({ token: { denomOrAddress: priceDenom } }) => priceDenom === denom @@ -646,8 +658,7 @@ export const ConfigureRebalancerComponent: ActionComponent< return { symbol: token.symbol, - initialAmount: convertMicroDenomToDenomWithDecimals( - balance, + initialAmount: balance.toHumanReadableNumber( token.decimals ), targetProportion: percent / 100, diff --git a/packages/stateful/actions/core/actions/ConfigureRebalancer/README.md b/packages/stateful/actions/core/actions/ConfigureRebalancer/README.md index 55ddb1d2e..6b401eed8 100644 --- a/packages/stateful/actions/core/actions/ConfigureRebalancer/README.md +++ b/packages/stateful/actions/core/actions/ConfigureRebalancer/README.md @@ -16,7 +16,26 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). ```json { "chainId": "", - ... + "trustee": "", + "baseDenom": "", + "tokens": [ + { + "denom": "", + "percent": + }, + ... + ], + "pid": { + "kp": , + "ki": , + "kd": + }, + "maxLimit": , + "minBalance": { + "denom": "", + "amount": "" + } | undefined, + "targetOverrideStrategy": "" } ``` diff --git a/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx b/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx index c8af2ba7c..dbcfaffd2 100644 --- a/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx +++ b/packages/stateful/actions/core/actions/ConfigureRebalancer/index.tsx @@ -5,6 +5,7 @@ import { useEffect } from 'react' import { useFormContext } from 'react-hook-form' import { waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenSelector, tokenQueries, @@ -47,8 +48,6 @@ import { import { VALENCE_INSTANTIATE2_SALT, VALENCE_SUPPORTED_CHAINS, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, encodeJsonToBase64, getAccount, getChainAddressForActionOptions, @@ -236,12 +235,12 @@ const Component: ActionComponent = ( tokens.map( (token): GenericTokenBalance => ({ token, - balance: convertDenomToMicroDenomStringWithDecimals( + balance: HugeDecimal.fromHumanReadable( existingCreateValenceAccountActionData?.funds .find(({ denom }) => denom === token.denomOrAddress) ?.amount?.toString() || 0, token.decimals - ), + ).toString(), }) ), }), @@ -417,10 +416,9 @@ export class ConfigureRebalancerAction extends ActionBase d.denomOrAddress === denom )?.decimals ?? 0 - ) + ).toString() : undefined, // BPS bps: percent * 100, @@ -682,10 +680,9 @@ export class ConfigureRebalancerAction extends ActionBase() const denomOrAddress = watch( @@ -86,12 +86,7 @@ export const CreateRewardDistributionComponent: ActionComponent< : tokens.data.find((t) => tokensEqual(t.token, token.data)) const decimals = selectedToken?.token.decimals ?? 0 - const minAmount = convertMicroDenomToDenomWithDecimals(1, decimals) - - const selectedBalance = convertMicroDenomToDenomWithDecimals( - selectedToken?.balance ?? 0, - decimals - ) + const selectedBalance = HugeDecimal.from(selectedToken?.balance ?? 0) const warning = !isCreating || tokens.loading || @@ -101,10 +96,11 @@ export const CreateRewardDistributionComponent: ActionComponent< ? undefined : !selectedToken ? t('error.unknownDenom', { denom: denomOrAddress }) - : initialFunds && initialFunds > selectedBalance + : initialFunds && + selectedBalance.toHumanReadable(decimals).lt(initialFunds) ? t('error.insufficientFundsWarning', { - amount: selectedBalance.toLocaleString(undefined, { - maximumFractionDigits: decimals, + amount: selectedBalance.toInternationalizedHumanReadableString({ + decimals, }), tokenSymbol: selectedToken.token.symbol, }) @@ -161,11 +157,10 @@ export const CreateRewardDistributionComponent: ActionComponent< description: t('title.balance') + ': ' + - convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + HugeDecimal.from( + balance + ).toInternationalizedHumanReadableString({ + decimals: token.decimals, }), })), } @@ -198,23 +193,23 @@ export const CreateRewardDistributionComponent: ActionComponent< /> {!immediate && ( -
- +
@@ -222,20 +217,21 @@ export const CreateRewardDistributionComponent: ActionComponent<
-
@@ -269,19 +265,19 @@ export const CreateRewardDistributionComponent: ActionComponent< {t('info.initialRewardsFundsDescription')}

- @@ -299,7 +295,7 @@ export const CreateRewardDistributionComponent: ActionComponent< onClick={() => setValue( (fieldNamePrefix + 'initialFunds') as 'initialFunds', - selectedBalance + selectedBalance.toHumanReadableString(decimals) ) } showFullAmount @@ -307,20 +303,18 @@ export const CreateRewardDistributionComponent: ActionComponent< />
- {selectedBalance > 0 && ( + {selectedBalance.isPositive() && (
{[10, 25, 50, 75, 100].map((percent) => ( setValue( (fieldNamePrefix + 'initialFunds') as 'initialFunds', - amount + amount.toHumanReadableString(decimals) ) } /> diff --git a/packages/stateful/actions/core/actions/CreateRewardDistribution/README.md b/packages/stateful/actions/core/actions/CreateRewardDistribution/README.md index 491e02692..5ac8c5289 100644 --- a/packages/stateful/actions/core/actions/CreateRewardDistribution/README.md +++ b/packages/stateful/actions/core/actions/CreateRewardDistribution/README.md @@ -19,10 +19,10 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "denomOrAddress": "", "immediate": , "rate": { - "amount": , + "amount": "", "unit": "" }, - "initialFunds": , + "initialFunds": "", "openFunding": } ``` diff --git a/packages/stateful/actions/core/actions/CreateRewardDistribution/index.tsx b/packages/stateful/actions/core/actions/CreateRewardDistribution/index.tsx index 74bb48f93..b82e55d83 100644 --- a/packages/stateful/actions/core/actions/CreateRewardDistribution/index.tsx +++ b/packages/stateful/actions/core/actions/CreateRewardDistribution/index.tsx @@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query' import { nanoid } from 'nanoid' import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { contractQueries, tokenQueries } from '@dao-dao/state/query' import { ActionBase, BucketEmoji, useChain } from '@dao-dao/stateless' import { @@ -25,10 +26,8 @@ import { InstantiateMsg, } from '@dao-dao/types/contracts/DaoRewardsDistributor' import { - convertDenomToMicroDenomStringWithDecimals, convertDurationToDurationWithUnits, convertDurationWithUnitsToDuration, - convertMicroDenomToDenomWithDecimals, encodeJsonToBase64, getDaoRewardDistributors, getNativeTokenForChainId, @@ -125,13 +124,13 @@ export class CreateRewardDistributionAction extends ActionBase { if (this.options.context.type !== ActionContextType.Dao) { @@ -169,6 +168,11 @@ export class CreateRewardDistributionAction extends ActionBase appendCoin({ - amount: 1, + amount: '1', denom: nativeToken.denomOrAddress, decimals: nativeToken.decimals, }) @@ -173,10 +171,11 @@ export const CreateValenceAccountComponent: ActionComponent< ? '' : serviceFee.data ? t('format.token', { - amount: convertMicroDenomToDenomWithDecimals( - serviceFee.data.balance, - serviceFee.data.token.decimals - ), + amount: HugeDecimal.from( + serviceFee.data.balance + ).toInternationalizedHumanReadableString({ + decimals: serviceFee.data.token.decimals, + }), symbol: serviceFee.data.token.symbol, }) : '', diff --git a/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx b/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx index 77b152478..4cf57fbb3 100644 --- a/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx +++ b/packages/stateful/actions/core/actions/CreateValenceAccount/index.tsx @@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query' import { useEffect } from 'react' import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { tokenQueries, valenceRebalancerExtraQueries, @@ -32,8 +33,6 @@ import { MsgInstantiateContract2 } from '@dao-dao/types/protobuf/codegen/cosmwas import { VALENCE_INSTANTIATE2_SALT, VALENCE_SUPPORTED_CHAINS, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, getChainAddressForActionOptions, getDisplayNameForChainId, getSupportedChainConfig, @@ -164,7 +163,7 @@ export class CreateValenceAccountAction extends ActionBase ({ - denom, - amount: convertDenomToMicroDenomStringWithDecimals(amount, decimals), - })) + const convertedFunds = funds.map(({ denom, amount, decimals }) => + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) // Add service fee to funds. if (serviceFee && serviceFee.amount !== '0') { const existing = convertedFunds.find((f) => f.denom === serviceFee.denom) if (existing) { - existing.amount = ( - BigInt(existing.amount) + BigInt(serviceFee.amount) - ).toString() + existing.amount = HugeDecimal.from(existing.amount) + .plus(serviceFee.amount) + .toString() } else { convertedFunds.push({ denom: serviceFee.denom, @@ -280,8 +278,7 @@ export class CreateValenceAccountAction extends ActionBase { proposalDeposit: { enabled: !!depositInfoWithToken.depositInfo, amount: depositInfoWithToken.depositInfo - ? convertMicroDenomToDenomWithDecimals( - depositInfoWithToken.depositInfo.amount, - depositInfoWithToken.token.decimals - ) - : 10, + ? HugeDecimal.from( + depositInfoWithToken.depositInfo.amount + ).toHumanReadableString(depositInfoWithToken.token.decimals) + : '10', type: depositInfoWithToken.depositInfo && 'cw20' in depositInfoWithToken.depositInfo.denom diff --git a/packages/stateful/actions/core/actions/Execute/Component.tsx b/packages/stateful/actions/core/actions/Execute/Component.tsx index 6186c2b03..66ad878da 100644 --- a/packages/stateful/actions/core/actions/Execute/Component.tsx +++ b/packages/stateful/actions/core/actions/Execute/Component.tsx @@ -3,6 +3,7 @@ import JSON5 from 'json5' import { useFieldArray, useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { AccountSelector, AddressInput, @@ -19,7 +20,6 @@ import { import { GenericTokenBalance, LoadingData, TokenType } from '@dao-dao/types' import { ActionComponent, ActionContextType } from '@dao-dao/types/actions' import { - convertMicroDenomToDenomWithDecimals, getNativeTokenForChainId, makeValidateAddress, makeWasmMessage, @@ -34,7 +34,7 @@ export type ExecuteData = { message: string funds: { denom: string - amount: number + amount: string // Will multiply `amount` by 10^decimals when generating the message. decimals: number }[] @@ -58,7 +58,7 @@ export const ExecuteComponent: ActionComponent = ({ const { context } = useActionOptions() const { chain_id: chainId, bech32_prefix: bech32Prefix } = useChain() - const { register, control, watch, setValue } = useFormContext() + const { register, control, watch, setValue, getValues } = useFormContext() const { fields: coins, append: appendCoin, @@ -192,15 +192,14 @@ export const ExecuteComponent: ActionComponent = ({ amount={{ watch, setValue, + getValues, register, fieldName: fieldNamePrefix + 'funds.0.amount', error: errors?.funds?.[0]?.amount, - min: convertMicroDenomToDenomWithDecimals( - 1, + min: HugeDecimal.one.toHumanReadableNumber( selectedCw20?.token.decimals ?? 0 ), - step: convertMicroDenomToDenomWithDecimals( - 1, + step: HugeDecimal.one.toHumanReadableNumber( selectedCw20?.token.decimals ?? 0 ), }} diff --git a/packages/stateful/actions/core/actions/Execute/index.tsx b/packages/stateful/actions/core/actions/Execute/index.tsx index b45147bbe..c7e97eccd 100644 --- a/packages/stateful/actions/core/actions/Execute/index.tsx +++ b/packages/stateful/actions/core/actions/Execute/index.tsx @@ -3,6 +3,7 @@ import JSON5 from 'json5' import { useEffect } from 'react' import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { tokenQueries } from '@dao-dao/state/query' import { ActionBase, @@ -23,8 +24,6 @@ import { import { MsgExecuteContract as SecretMsgExecuteContract } from '@dao-dao/types/protobuf/codegen/secret/compute/v1beta1/msg' import { bech32DataToAddress, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, decodeJsonFromBase64, encodeJsonToBase64, getAccountAddress, @@ -188,10 +187,10 @@ export class ExecuteAction extends ActionBase { contractAddress: funds[0].denom, msg: { send: { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( funds[0].amount, funds[0].decimals - ), + ).toString(), [isSecret ? 'recipient' : 'contract']: address, msg: encodeJsonToBase64(msg), ...(isSecret && { @@ -207,13 +206,9 @@ export class ExecuteAction extends ActionBase { contractAddress: address, msg, funds: funds - .map(({ denom, amount, decimals }) => ({ - denom, - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - decimals - ), - })) + .map(({ denom, amount, decimals }) => + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) // Neutron errors with `invalid coins` if the funds list is not // alphabetized. .sort((a, b) => a.denom.localeCompare(b.denom)), @@ -364,16 +359,16 @@ export class ExecuteAction extends ActionBase { ? [ { denom: decodedMessage.wasm.execute.contract_addr, - amount: convertMicroDenomToDenomWithDecimals( - executeMsg.send.amount, - cw20TokenDecimals - ), + amount: HugeDecimal.from( + executeMsg.send.amount + ).toHumanReadableString(cw20TokenDecimals), decimals: cw20TokenDecimals, }, ] : fundsTokens.map(({ denom, amount, decimals }) => ({ denom, - amount: convertMicroDenomToDenomWithDecimals(amount, decimals), + amount: + HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, })), cw20: isCw20, @@ -402,16 +397,16 @@ export class ExecuteAction extends ActionBase { chainId, decodedMessage.stargate.value.contract ), - amount: convertMicroDenomToDenomWithDecimals( - executeMsg.send.amount, - cw20TokenDecimals - ), + amount: HugeDecimal.from( + executeMsg.send.amount + ).toHumanReadableString(cw20TokenDecimals), decimals: cw20TokenDecimals, }, ] : fundsTokens.map(({ denom, amount, decimals }) => ({ denom, - amount: convertMicroDenomToDenomWithDecimals(amount, decimals), + amount: + HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, })), cw20: isCw20, diff --git a/packages/stateful/actions/core/actions/FundRewardDistribution/Component.tsx b/packages/stateful/actions/core/actions/FundRewardDistribution/Component.tsx index af4e11807..69feb779e 100644 --- a/packages/stateful/actions/core/actions/FundRewardDistribution/Component.tsx +++ b/packages/stateful/actions/core/actions/FundRewardDistribution/Component.tsx @@ -3,12 +3,13 @@ import clsx from 'clsx' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { FilterableItemPopup, InputErrorMessage, InputLabel, InputThemedText, - NumberInput, + NumericInput, PercentButton, StatusCard, TokenAmountDisplay, @@ -20,7 +21,6 @@ import { } from '@dao-dao/types' import { ActionComponent } from '@dao-dao/types/actions' import { - convertMicroDenomToDenomWithDecimals, getFallbackImage, getHumanReadableRewardDistributionLabel, toAccessibleImageUrl, @@ -32,7 +32,7 @@ import { export type FundRewardDistributionData = { address: string id: number - amount: number + amount: string } export type FundRewardDistributionOptions = { @@ -55,7 +55,7 @@ export const FundRewardDistributionComponent: ActionComponent< options: { distributions, tokens }, }) => { const { t } = useTranslation() - const { register, setValue, watch } = + const { register, setValue, watch, getValues } = useFormContext() const address = watch((fieldNamePrefix + 'address') as 'address') @@ -68,28 +68,29 @@ export const FundRewardDistributionComponent: ActionComponent< const selectedBalance = selectedDistribution && !tokens.loading - ? convertMicroDenomToDenomWithDecimals( + ? HugeDecimal.from( tokens.data.find((t) => tokensEqual(t.token, selectedDistribution.token) - )?.balance || 0, - selectedDistribution.token.decimals + )?.balance || 0 ) - : 0 + : HugeDecimal.zero const warning = !isCreating || tokens.loading || tokens.updating || !selectedDistribution ? undefined - : amount && amount > selectedBalance + : amount && + selectedBalance + .toHumanReadable(selectedDistribution.token.decimals) + .lt(amount) ? t('error.insufficientFundsWarning', { - amount: selectedBalance.toLocaleString(undefined, { - maximumFractionDigits: selectedDistribution.token.decimals, + amount: selectedBalance.toInternationalizedHumanReadableString({ + decimals: selectedDistribution.token.decimals, }), tokenSymbol: selectedDistribution.token.symbol, }) : undefined - const minAmount = convertMicroDenomToDenomWithDecimals( - 1, + const minAmount = HugeDecimal.one.toHumanReadableNumber( selectedDistribution?.token.decimals ?? 0 ) @@ -159,17 +160,19 @@ export const FundRewardDistributionComponent: ActionComponent<
- @@ -187,7 +190,9 @@ export const FundRewardDistributionComponent: ActionComponent< onClick={() => setValue( (fieldNamePrefix + 'amount') as 'amount', - selectedBalance + selectedBalance.toHumanReadableString( + selectedDistribution.token.decimals + ) ) } showFullAmount @@ -195,20 +200,23 @@ export const FundRewardDistributionComponent: ActionComponent< />
- {selectedBalance > 0 && ( + {selectedBalance.isPositive() && (
{[10, 25, 50, 75, 100].map((percent) => ( setValue( (fieldNamePrefix + 'amount') as 'amount', - amount + amount.toHumanReadableString( + selectedDistribution.token.decimals + ) ) } /> diff --git a/packages/stateful/actions/core/actions/FundRewardDistribution/README.md b/packages/stateful/actions/core/actions/FundRewardDistribution/README.md index ab54cf1dc..df51ecc47 100644 --- a/packages/stateful/actions/core/actions/FundRewardDistribution/README.md +++ b/packages/stateful/actions/core/actions/FundRewardDistribution/README.md @@ -17,6 +17,6 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). { "address": "
", "id": , - "amount": + "amount": "" } ``` diff --git a/packages/stateful/actions/core/actions/FundRewardDistribution/index.tsx b/packages/stateful/actions/core/actions/FundRewardDistribution/index.tsx index 3d525f46b..522d1feb8 100644 --- a/packages/stateful/actions/core/actions/FundRewardDistribution/index.tsx +++ b/packages/stateful/actions/core/actions/FundRewardDistribution/index.tsx @@ -1,5 +1,6 @@ import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { daoRewardsDistributorExtraQueries } from '@dao-dao/state/query' import { ActionBase, @@ -22,8 +23,6 @@ import { ProcessedMessage, } from '@dao-dao/types/actions' import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, encodeJsonToBase64, getDaoRewardDistributors, makeExecuteSmartContractMessage, @@ -134,7 +133,7 @@ export class FundRewardDistributionAction extends ActionBase + depositTokens: LoadingDataWithError TokenAmountDisplay: ComponentType GovProposalActionDisplay: ComponentType } @@ -34,7 +32,7 @@ export type GovernanceDepositData = { chainId: string proposalId: string deposit: { - amount: number + amount: string denom: string }[] } @@ -55,13 +53,14 @@ export const GovernanceDepositComponent: ActionComponent< data, }) => { const { t } = useTranslation() - const { setValue, register, watch } = useFormContext() + const { setValue, register, getValues, watch } = + useFormContext() const proposalId = watch((fieldNamePrefix + 'proposalId') as 'proposalId') const proposalSelected = proposals.find((p) => p.id.toString() === proposalId) const selectedDepositToken = - depositTokens.loading || !data.deposit.length + depositTokens.loading || depositTokens.errored || !data.deposit.length ? undefined : depositTokens.data.find( ({ denomOrAddress }) => denomOrAddress === data.deposit[0].denom @@ -101,19 +100,17 @@ export const GovernanceDepositComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'deposit.0.amount') as 'deposit.0.amount', error: errors?.deposit?.[0]?.amount, - min: convertMicroDenomToDenomWithDecimals( - 1, + min: HugeDecimal.one.toHumanReadableNumber( selectedDepositToken?.decimals ?? 0 ), - step: convertMicroDenomToDenomWithDecimals( - 1, + step: HugeDecimal.one.toHumanReadableNumber( selectedDepositToken?.decimals ?? 0 ), - convertMicroDenom: true, }} onSelectToken={({ denomOrAddress }) => setValue( @@ -123,7 +120,9 @@ export const GovernanceDepositComponent: ActionComponent< } readOnly={!isCreating} selectedToken={selectedDepositToken} - tokens={depositTokens} + tokens={ + depositTokens.errored ? { loading: false, data: [] } : depositTokens + } />
diff --git a/packages/stateful/actions/core/actions/GovernanceDeposit/README.md b/packages/stateful/actions/core/actions/GovernanceDeposit/README.md index 013f85e82..c7130ec5e 100644 --- a/packages/stateful/actions/core/actions/GovernanceDeposit/README.md +++ b/packages/stateful/actions/core/actions/GovernanceDeposit/README.md @@ -19,7 +19,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "deposit": [ { "denom": "", - "amount": + "amount": "" } ] } diff --git a/packages/stateful/actions/core/actions/GovernanceDeposit/index.tsx b/packages/stateful/actions/core/actions/GovernanceDeposit/index.tsx index 297c82a72..3a2ba58d7 100644 --- a/packages/stateful/actions/core/actions/GovernanceDeposit/index.tsx +++ b/packages/stateful/actions/core/actions/GovernanceDeposit/index.tsx @@ -2,16 +2,15 @@ import { Coin } from '@cosmjs/stargate' import { useQueryClient } from '@tanstack/react-query' import { useEffect } from 'react' import { useFormContext } from 'react-hook-form' -import { waitForAll } from 'recoil' -import { chainQueries, genericTokenSelector } from '@dao-dao/state' +import { HugeDecimal } from '@dao-dao/math' +import { chainQueries, tokenQueries } from '@dao-dao/state' import { ActionBase, BankEmoji, ChainProvider, DaoSupportedChainPickerInput, useActionOptions, - useCachedLoading, useChain, } from '@dao-dao/stateless' import { @@ -37,7 +36,7 @@ import { import { GovProposalActionDisplay } from '../../../../components' import { TokenAmountDisplay } from '../../../../components/TokenAmountDisplay' -import { useQueryLoadingDataWithError } from '../../../../hooks' +import { useQueryLoadingDataWithError, useQueryTokens } from '../../../../hooks' import { GovActionsProvider } from '../../../providers/gov' import { GovernanceDepositData, @@ -129,8 +128,26 @@ const InnerComponent: ActionComponent = ( : undefined ) + const depositTokens = useQueryTokens( + context.params.minDeposit.map(({ denom }) => ({ + chainId, + type: TokenType.Native, + denomOrAddress: denom, + })) + ) + + const minDeposit = context.params.minDeposit[0] + const minDepositToken = + depositTokens.loading || depositTokens.errored + ? undefined + : depositTokens.data.find((t) => t.denomOrAddress === minDeposit.denom) + // On proposal change, update deposit to remaining needed. useEffect(() => { + if (!minDepositToken) { + return + } + const proposalSelected = proposalId && !proposalOptions.loading && @@ -140,24 +157,31 @@ const InnerComponent: ActionComponent = ( return } - const minDeposit = context.params.minDeposit[0] - const missingDeposit = - BigInt(minDeposit.amount) - - BigInt( - proposalSelected.proposal.totalDeposit.find( - ({ denom }) => minDeposit.denom === denom - )?.amount ?? 0 - ) + const missingDeposit = HugeDecimal.from(minDeposit.amount).minus( + proposalSelected.proposal.totalDeposit.find( + ({ denom }) => denom === minDeposit.denom + )?.amount ?? 0 + ) - if (missingDeposit > 0) { + if (missingDeposit.isPositive()) { setValue((fieldNamePrefix + 'deposit') as 'deposit', [ { denom: minDeposit.denom, - amount: Number(missingDeposit), + amount: missingDeposit.toHumanReadableString( + minDepositToken.decimals + ), }, ]) } - }, [proposalId, proposalOptions, context.params, setValue, fieldNamePrefix]) + }, [ + proposalId, + proposalOptions, + context.params, + setValue, + fieldNamePrefix, + minDepositToken, + minDeposit, + ]) // Select first proposal once loaded if nothing selected. useEffect(() => { @@ -175,19 +199,6 @@ const InnerComponent: ActionComponent = ( } }, [isCreating, proposalOptions, proposalId, setValue, fieldNamePrefix]) - const depositTokens = useCachedLoading( - waitForAll( - context.params.minDeposit.map(({ denom }) => - genericTokenSelector({ - type: TokenType.Native, - denomOrAddress: denom, - chainId, - }) - ) - ), - [] - ) - return ( { } } - encode({ + async encode({ chainId, proposalId, deposit, - }: GovernanceDepositData): UnifiedCosmosMsg[] { + }: GovernanceDepositData): Promise { const depositor = getChainAddressForActionOptions(this.options, chainId) if (!depositor) { throw new Error('Depositor address not found for chain.') } + const amount = await Promise.all( + deposit.map(async ({ denom, amount }) => { + const { decimals } = await this.options.queryClient.fetchQuery( + tokenQueries.info(this.options.queryClient, { + chainId, + type: TokenType.Native, + denomOrAddress: denom, + }) + ) + + return HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + }) + ) + return maybeMakePolytoneExecuteMessages( this.options.chain.chain_id, chainId, makeStargateMessage({ stargate: { typeUrl: MsgDeposit.typeUrl, - value: { + value: MsgDeposit.fromPartial({ proposalId: BigInt(proposalId || '0'), depositor, - amount: deposit.map(({ denom, amount }) => ({ - denom, - amount: BigInt(amount).toString(), - })), - } as MsgDeposit, + amount, + }), }, }) ) @@ -270,21 +292,35 @@ export class GovernanceDepositAction extends ActionBase { }) } - decode([ + async decode([ { decodedMessage, account: { chainId }, }, - ]: ProcessedMessage[]): GovernanceDepositData { + ]: ProcessedMessage[]): Promise { + const deposit = await Promise.all( + (decodedMessage.stargate.value.amount as Coin[]).map( + async ({ denom, amount }) => { + const { decimals } = await this.options.queryClient.fetchQuery( + tokenQueries.info(this.options.queryClient, { + chainId, + type: TokenType.Native, + denomOrAddress: denom, + }) + ) + + return { + denom, + amount: HugeDecimal.fromHumanReadable(amount, decimals).toString(), + } + } + ) + ) + return { chainId, proposalId: decodedMessage.stargate.value.proposalId.toString(), - deposit: (decodedMessage.stargate.value.amount as Coin[]).map( - ({ denom, amount }) => ({ - denom, - amount: Number(amount), - }) - ), + deposit, } } } diff --git a/packages/stateful/actions/core/actions/GovernanceProposal/Component.stories.tsx b/packages/stateful/actions/core/actions/GovernanceProposal/Component.stories.tsx index e2f86c195..2bd1852f5 100644 --- a/packages/stateful/actions/core/actions/GovernanceProposal/Component.stories.tsx +++ b/packages/stateful/actions/core/actions/GovernanceProposal/Component.stories.tsx @@ -38,16 +38,18 @@ Default.args = { metadata: '', deposit: [ { - amount: 100, + amount: '100', denom: 'ujunox', + decimals: 6, }, ], legacy: { typeUrl: SoftwareUpgradeProposal.typeUrl, spends: [ { - amount: 1, + amount: '1', denom: 'ujunox', + decimals: 6, }, ], spendRecipient: 'junoRecipient', @@ -73,7 +75,7 @@ Default.args = { isCreating: true, errors: {}, options: { - minDeposits: { loading: false, data: [] }, + minDeposits: { loading: false, errored: false, data: [] }, communityPoolBalances: { loading: false, data: [] }, encodeContext: { type: ActionContextType.Wallet, diff --git a/packages/stateful/actions/core/actions/GovernanceProposal/Component.tsx b/packages/stateful/actions/core/actions/GovernanceProposal/Component.tsx index 1fe523a82..7670de517 100644 --- a/packages/stateful/actions/core/actions/GovernanceProposal/Component.tsx +++ b/packages/stateful/actions/core/actions/GovernanceProposal/Component.tsx @@ -5,6 +5,7 @@ import { ComponentType, useEffect } from 'react' import { useFieldArray, useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, CodeMirrorInput, @@ -21,7 +22,7 @@ import { TokenInput, useActionOptions, useChainContext, - useDaoInfoContextIfAvailable, + useDaoIfAvailable, } from '@dao-dao/stateless' import { AddressInputProps, @@ -35,6 +36,7 @@ import { GovProposalActionDisplayProps, GovernanceProposalActionData, LoadingData, + LoadingDataWithError, StatefulTokenAmountDisplayProps, } from '@dao-dao/types' import { ActionComponent, ActionContextType } from '@dao-dao/types/actions' @@ -43,7 +45,6 @@ import { Cosmos_govv1beta1Content_FromAmino } from '@dao-dao/types/protobuf/code import { ParameterChangeProposal } from '@dao-dao/types/protobuf/codegen/cosmos/params/v1beta1/params' import { SoftwareUpgradeProposal } from '@dao-dao/types/protobuf/codegen/cosmos/upgrade/v1beta1/upgrade' import { - convertMicroDenomToDenomWithDecimals, getChainAssets, govProposalActionDataToDecodedContent, makeValidateAddress, @@ -53,7 +54,7 @@ import { } from '@dao-dao/utils' export type GovernanceProposalOptions = { - minDeposits: LoadingData< + minDeposits: LoadingDataWithError< (GenericTokenBalance & { min: string })[] @@ -83,8 +84,15 @@ export const GovernanceProposalComponent: ActionComponent< } = props const { t } = useTranslation() - const { register, setValue, watch, control, setError, clearErrors } = - useFormContext() + const { + register, + setValue, + getValues, + watch, + control, + setError, + clearErrors, + } = useFormContext() const { context } = useActionOptions() // Type-check, this action should not be used in a non-gov action context. @@ -102,7 +110,7 @@ export const GovernanceProposalComponent: ActionComponent< // Whether or not this action is being used directly on a governance page (as // opposed to in a DAO proposal). const onGovernancePage = - useDaoInfoContextIfAvailable()?.coreVersion === ContractVersion.Gov + useDaoIfAvailable()?.coreVersion === ContractVersion.Gov const { chainId, @@ -110,16 +118,20 @@ export const GovernanceProposalComponent: ActionComponent< nativeToken, } = useChainContext() - const selectedMinDepositToken = minDeposits.loading - ? undefined - : minDeposits.data.find( - ({ token }) => token.denomOrAddress === data.deposit[0].denom - ) - const depositMin = - convertMicroDenomToDenomWithDecimals( - selectedMinDepositToken?.min ?? 0, - selectedMinDepositToken?.token.decimals ?? 0 - ) * context.params.minInitialDepositRatio + const selectedMinDepositToken = + minDeposits.loading || minDeposits.errored + ? undefined + : minDeposits.data.find( + ({ token }) => token.denomOrAddress === data.deposit[0].denom + ) + const minDepositTokenDecimals = selectedMinDepositToken?.token.decimals ?? 0 + const depositVotingMin = HugeDecimal.from(selectedMinDepositToken?.min ?? 0) + const depositSubmitMin = depositVotingMin.times( + context.params.minInitialDepositRatio + ) + const depositTokenBalance = HugeDecimal.from( + selectedMinDepositToken?.balance ?? 0 + ) const { fields: spendFields, @@ -172,6 +184,10 @@ export const GovernanceProposalComponent: ActionComponent< const parsedUpgradePlan = JSON5.parse(upgradePlan) const parsedCustom = JSON5.parse(custom) + const spendAmount = spends.map(({ amount, denom, decimals }) => + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) + const content = typeUrl === GOVERNANCE_PROPOSAL_TYPE_CUSTOM ? Cosmos_govv1beta1Content_FromAmino({ @@ -190,10 +206,7 @@ export const GovernanceProposalComponent: ActionComponent< title, description, // CommunityPoolSpendProposal - amount: spends.map(({ amount, denom }) => ({ - denom, - amount: BigInt(amount).toString(), - })), + amount: spendAmount, recipient: spendRecipient, // ParameterChangeProposal changes: JSON5.parse(parameterChanges), @@ -367,17 +380,14 @@ export const GovernanceProposalComponent: ActionComponent<

{t('info.govDepositDescription', { - amount: convertMicroDenomToDenomWithDecimals( - selectedMinDepositToken?.min ?? 0, - selectedMinDepositToken?.token.decimals ?? 0 - ).toLocaleString(undefined, { - maximumFractionDigits: - selectedMinDepositToken?.token.decimals ?? 0, - }), - minAmount: depositMin.toLocaleString(undefined, { - maximumFractionDigits: - selectedMinDepositToken?.token.decimals ?? 0, - }), + amount: + depositVotingMin.toInternationalizedHumanReadableString({ + decimals: minDepositTokenDecimals, + }), + minAmount: + depositSubmitMin.toInternationalizedHumanReadableString({ + decimals: minDepositTokenDecimals, + }), symbol: selectedMinDepositToken?.token.symbol ?? t('info.tokens'), })} @@ -389,43 +399,53 @@ export const GovernanceProposalComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'deposit.0.amount') as 'deposit.0.amount', error: errors?.deposit?.[0]?.amount, - min: depositMin, - step: convertMicroDenomToDenomWithDecimals( - 1, - selectedMinDepositToken?.token.decimals ?? 0 + min: depositSubmitMin.toHumanReadableNumber( + minDepositTokenDecimals + ), + step: HugeDecimal.one.toHumanReadableNumber( + minDepositTokenDecimals ), - convertMicroDenom: true, // Validate that balance is sufficient to pay the deposit. validations: [ (value) => isNaN(value) || !selectedMinDepositToken || - Number(selectedMinDepositToken.balance) >= value || + depositTokenBalance.gte(value) || t('error.insufficientBalance', { - amount: convertMicroDenomToDenomWithDecimals( - selectedMinDepositToken.balance, - selectedMinDepositToken.token.decimals - ), + amount: + depositTokenBalance.toInternationalizedHumanReadableString( + { + decimals: minDepositTokenDecimals, + } + ), tokenSymbol: selectedMinDepositToken.token.symbol, }), ], }} - onSelectToken={({ denomOrAddress }) => + onSelectToken={({ denomOrAddress, decimals }) => { setValue( (fieldNamePrefix + 'deposit.0.denom') as 'deposit.0.denom', denomOrAddress ) - } + setValue( + (fieldNamePrefix + + 'deposit.0.decimals') as 'deposit.0.decimals', + decimals + ) + }} readOnly={!isCreating} selectedToken={selectedMinDepositToken?.token} tokens={ minDeposits.loading ? { loading: true } + : minDeposits.errored + ? { loading: false, data: [] } : { loading: false, data: minDeposits.data.map(({ token }) => token), @@ -559,28 +579,34 @@ export const GovernanceProposalComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + `legacy.spends.${index}.amount`) as `legacy.spends.${number}.amount`, error: errors?.legacy?.spends?.[index]?.amount, - min: convertMicroDenomToDenomWithDecimals( - 1, + min: HugeDecimal.one.toHumanReadableNumber( selectedToken.decimals ), - step: convertMicroDenomToDenomWithDecimals( - 1, + step: HugeDecimal.one.toHumanReadableNumber( selectedToken.decimals ), - convertMicroDenom: true, }} - onSelectToken={({ denomOrAddress }) => + onSelectToken={({ + denomOrAddress, + decimals, + }) => { setValue( (fieldNamePrefix + `legacy.spends.${index}.denom`) as `legacy.spends.${number}.denom`, denomOrAddress ) - } + setValue( + (fieldNamePrefix + + `legacy.spends.${index}.decimals`) as `legacy.spends.${number}.decimals`, + decimals + ) + }} selectedToken={selectedToken} tokens={{ loading: false, @@ -603,7 +629,7 @@ export const GovernanceProposalComponent: ActionComponent< className="self-start" onClick={() => appendSpend({ - amount: 1, + amount: '1', denom: nativeToken?.denomOrAddress || '', }) } @@ -703,9 +729,9 @@ export const GovernanceProposalComponent: ActionComponent< GovProposalActionDisplay={GovProposalActionDisplay} TokenAmountDisplay={TokenAmountDisplay} content={govProposalActionDataToDecodedContent(data)} - deposit={data.deposit.map(({ denom, amount }) => ({ + deposit={data.deposit.map(({ denom, amount, decimals }) => ({ denom, - amount: isNaN(amount) ? '0' : BigInt(amount).toString(), + amount: HugeDecimal.fromHumanReadable(amount, decimals).toString(), }))} /> )} diff --git a/packages/stateful/actions/core/actions/GovernanceProposal/README.md b/packages/stateful/actions/core/actions/GovernanceProposal/README.md index 1656d584a..2b6087376 100644 --- a/packages/stateful/actions/core/actions/GovernanceProposal/README.md +++ b/packages/stateful/actions/core/actions/GovernanceProposal/README.md @@ -21,13 +21,15 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "deposit": [ { "denom": "", - "amount": + "amount": "", + "decimals": } ], "spends": [ { "denom": "", - "amount": + "amount": "", + "decimals": }, ... ], diff --git a/packages/stateful/actions/core/actions/GovernanceProposal/index.tsx b/packages/stateful/actions/core/actions/GovernanceProposal/index.tsx index 4512de17d..098371de0 100644 --- a/packages/stateful/actions/core/actions/GovernanceProposal/index.tsx +++ b/packages/stateful/actions/core/actions/GovernanceProposal/index.tsx @@ -3,11 +3,12 @@ import { MutableRefObject, useEffect, useRef } from 'react' import { useFormContext } from 'react-hook-form' import { waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { chainQueries, communityPoolBalancesSelector, genericTokenBalanceSelector, - genericTokenSelector, + tokenQueries, } from '@dao-dao/state' import { ActionBase, @@ -16,6 +17,7 @@ import { RaisedHandEmoji, useActionOptions, useCachedLoading, + useCachedLoadingWithError, } from '@dao-dao/stateless' import { AccountType, @@ -138,8 +140,42 @@ const InnerComponent = ({ ? context.params.expeditedMinDeposit : context.params.minDeposit + // Get tokens and balances for all deposit tokens. + const minDeposits = useCachedLoadingWithError( + address + ? waitForAll( + minDepositParams.map(({ denom }) => + genericTokenBalanceSelector({ + address, + type: TokenType.Native, + denomOrAddress: denom, + chainId, + }) + ) + ) + : undefined, + (data) => + data.map(({ token, balance }, index) => ({ + token, + balance, + // Min deposit required. + min: minDepositParams[index].amount, + })) + ) + + const minDepositToken = + minDeposits.loading || minDeposits.errored || !minDepositParams.length + ? undefined + : minDeposits.data.find( + ({ token }) => token.denomOrAddress === minDepositParams[0].denom + ) + // On chain or min deposit change, reset deposit, except first load. useEffect(() => { + if (!minDepositToken) { + return + } + if (!firstDepositSet.current) { firstDepositSet.current = true return @@ -148,7 +184,10 @@ const InnerComponent = ({ setValue((props.fieldNamePrefix + 'deposit') as 'deposit', [ { denom: minDepositParams[0].denom, - amount: Number(minDepositParams[0].amount), + amount: HugeDecimal.from( + minDepositParams[0].amount + ).toHumanReadableString(minDepositToken.token.decimals), + decimals: minDepositToken.token.decimals, }, ]) }, [ @@ -157,6 +196,7 @@ const InnerComponent = ({ props.fieldNamePrefix, minDepositParams, firstDepositSet, + minDepositToken, ]) // Update version in data. @@ -169,56 +209,11 @@ const InnerComponent = ({ ) }, [setValue, props.fieldNamePrefix, context.params.supportsV1]) - // Get token info for all deposit tokens. - const minDepositTokens = useCachedLoading( - waitForAll( - minDepositParams.map(({ denom }) => - genericTokenSelector({ - type: TokenType.Native, - denomOrAddress: denom, - chainId, - }) - ) - ), - [] - ) - - // Get address balances for all deposit tokens when wallet connected. - const minDepositBalances = useCachedLoading( - address - ? waitForAll( - minDepositParams.map(({ denom }) => - genericTokenBalanceSelector({ - address, - type: TokenType.Native, - denomOrAddress: denom, - chainId, - }) - ) - ) - : undefined, - [] - ) - return ( ({ - token, - // Wallet's balance or 0 if not loaded. - balance: - (!minDepositBalances.loading && - minDepositBalances.data[index]?.balance) || - '0', - // Min deposit required. - min: minDepositParams[index].amount, - })), - }, + minDeposits, communityPoolBalances, encodeContext, TokenAmountDisplay, @@ -317,6 +312,14 @@ export class GovernanceProposalAction extends ActionBase + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) + ) + const msg = supportsV1 ? makeStargateMessage({ stargate: { typeUrl: MsgSubmitProposalV1.typeUrl, - value: { + value: MsgSubmitProposalV1.fromPartial({ messages: useV1LegacyContent ? [ MsgExecLegacyContent.toProtoMsg({ @@ -399,10 +412,7 @@ export class GovernanceProposalAction extends ActionBase cwMsgToProtobuf(chainId, msg, govModuleAddress) ), - initialDeposit: deposit.map(({ amount, denom }) => ({ - amount: BigInt(amount).toString(), - denom, - })), + initialDeposit, proposer, title, summary: description, @@ -410,20 +420,17 @@ export class GovernanceProposalAction extends ActionBase ({ - amount: BigInt(amount).toString(), - denom, - })), + initialDeposit, proposer, - } as MsgSubmitProposalV1Beta1, + }), }, }) @@ -464,12 +471,12 @@ export class GovernanceProposalAction extends ActionBase { + ]: ProcessedMessage[]): Promise> { if (decodedMessage.stargate.typeUrl === MsgSubmitProposalV1Beta1.typeUrl) { const proposal = decodedMessage.stargate.value as MsgSubmitProposalV1Beta1 const typeUrl = proposal.content?.typeUrl @@ -485,27 +492,59 @@ export class GovernanceProposalAction extends ActionBase { + const { decimals } = await this.options.queryClient.fetchQuery( + tokenQueries.info(this.options.queryClient, { + chainId, + type: TokenType.Native, + denomOrAddress: denom, + }) + ) + + return { + denom, + amount: HugeDecimal.from(amount).toHumanReadableString(decimals), + decimals, + } + }) + ) + + const spends = + proposal.content.typeUrl === CommunityPoolSpendProposal.typeUrl + ? await Promise.all( + (proposal.content.amount as Coin[]).map( + async ({ denom, amount }) => { + const { decimals } = + await this.options.queryClient.fetchQuery( + tokenQueries.info(this.options.queryClient, { + chainId, + type: TokenType.Native, + denomOrAddress: denom, + }) + ) + + return { + denom, + amount: + HugeDecimal.from(amount).toHumanReadableString(decimals), + decimals, + } + } + ) + ) + : [] + return { chainId, version: GovProposalVersion.V1_BETA_1, title: proposal.content.title, description: proposal.content.description, metadata: '', - deposit: proposal.initialDeposit.map(({ amount, ...coin }) => ({ - ...coin, - amount: Number(amount), - })), + deposit, legacy: { typeUrl, - spends: - proposal.content.typeUrl === CommunityPoolSpendProposal.typeUrl - ? (proposal.content.amount as Coin[]).map( - ({ amount, denom }) => ({ - amount: Number(amount), - denom, - }) - ) - : [], + spends, spendRecipient: proposal.content.typeUrl === CommunityPoolSpendProposal.typeUrl ? proposal.content.recipient @@ -531,16 +570,31 @@ export class GovernanceProposalAction extends ActionBase { + const { decimals } = await this.options.queryClient.fetchQuery( + tokenQueries.info(this.options.queryClient, { + chainId, + type: TokenType.Native, + denomOrAddress: denom, + }) + ) + + return { + denom, + amount: HugeDecimal.from(amount).toHumanReadableString(decimals), + decimals, + } + }) + ) + return { chainId, version: GovProposalVersion.V1, title: proposal.title, description: proposal.summary, metadata: proposal.metadata, - deposit: proposal.initialDeposit.map(({ amount, ...coin }) => ({ - ...coin, - amount: Number(amount), - })), + deposit, msgs: decodedMessages, expedited: proposal.expedited || false, } diff --git a/packages/stateful/actions/core/actions/Instantiate/Component.tsx b/packages/stateful/actions/core/actions/Instantiate/Component.tsx index 3ecfffdce..6c2acca88 100644 --- a/packages/stateful/actions/core/actions/Instantiate/Component.tsx +++ b/packages/stateful/actions/core/actions/Instantiate/Component.tsx @@ -12,7 +12,7 @@ import { InputErrorMessage, InputLabel, NativeCoinSelector, - NumberInput, + NumericInput, TextInput, useActionOptions, useChain, @@ -41,7 +41,7 @@ export type InstantiateData = { message: string funds: { denom: string - amount: number + amount: string // Will multiply `amount` by 10^decimals when generating the message. decimals: number }[] @@ -132,10 +132,12 @@ export const InstantiateComponent: ActionComponent = ({

- { const msg = JSON5.parse(message) const convertedFunds = funds - .map(({ denom, amount, decimals }) => ({ - denom, - amount: convertDenomToMicroDenomStringWithDecimals(amount, decimals), - })) + .map(({ denom, amount, decimals }) => + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) // Neutron errors with `invalid coins` if the funds list is not // alphabetized. .sort((a, b) => a.denom.localeCompare(b.denom)) @@ -429,7 +427,7 @@ export class InstantiateAction extends ActionBase { message: JSON.stringify(decodedMessage.wasm.instantiate.msg, null, 2), funds: fundsTokens.map(({ denom, amount, decimals }) => ({ denom, - amount: convertMicroDenomToDenomWithDecimals(amount, decimals), + amount: HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, })), _polytone: polytone @@ -457,7 +455,7 @@ export class InstantiateAction extends ActionBase { ), funds: fundsTokens.map(({ denom, amount, decimals }) => ({ denom, - amount: convertMicroDenomToDenomWithDecimals(amount, decimals), + amount: HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, })), _polytone: polytone diff --git a/packages/stateful/actions/core/actions/Instantiate2/Component.tsx b/packages/stateful/actions/core/actions/Instantiate2/Component.tsx index 2106184a4..01f0ace18 100644 --- a/packages/stateful/actions/core/actions/Instantiate2/Component.tsx +++ b/packages/stateful/actions/core/actions/Instantiate2/Component.tsx @@ -13,7 +13,7 @@ import { InputErrorMessage, InputLabel, NativeCoinSelector, - NumberInput, + NumericInput, TextInput, useActionOptions, useChain, @@ -43,7 +43,7 @@ export type Instantiate2Data = { salt: string funds: { denom: string - amount: number + amount: string // Will multiply `amount` by 10^decimals when generating the message. decimals: number }[] @@ -118,10 +118,12 @@ export const Instantiate2Component: ActionComponent = ({
- { const msg = JSON5.parse(message) const convertedFunds = funds - .map(({ denom, amount, decimals }) => ({ - denom, - amount: convertDenomToMicroDenomStringWithDecimals(amount, decimals), - })) + .map(({ denom, amount, decimals }) => + HugeDecimal.fromHumanReadable(amount, decimals).toCoin(denom) + ) // Neutron errors with `invalid coins` if the funds list is not // alphabetized. .sort((a, b) => a.denom.localeCompare(b.denom)) @@ -315,7 +313,7 @@ export class Instantiate2Action extends ActionBase { salt: data.salt, funds: fundsTokens.map(({ denom, amount, decimals }) => ({ denom, - amount: convertMicroDenomToDenomWithDecimals(amount, decimals), + amount: HugeDecimal.from(amount).toHumanReadableString(decimals), decimals, })), } diff --git a/packages/stateful/actions/core/actions/ManageStaking/Component.stories.tsx b/packages/stateful/actions/core/actions/ManageStaking/Component.stories.tsx index 8541d9b23..fb696620e 100644 --- a/packages/stateful/actions/core/actions/ManageStaking/Component.stories.tsx +++ b/packages/stateful/actions/core/actions/ManageStaking/Component.stories.tsx @@ -1,5 +1,6 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' +import { HugeDecimal } from '@dao-dao/math' import { token } from '@dao-dao/stateless/components/token/TokenCard.stories' import { CHAIN_ID } from '@dao-dao/storybook' import { makeReactHookFormDecorator } from '@dao-dao/storybook/decorators' @@ -19,7 +20,7 @@ export default { type: StakingActionType.Delegate, validator: '', toValidator: '', - amount: 1, + amount: '1', withdrawAddress: '', }), ], @@ -33,7 +34,7 @@ const stakes: TokenStake[] = [ { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: 'sparkIBC', moniker: 'Spark IBC', @@ -41,14 +42,14 @@ const stakes: TokenStake[] = [ details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 5, + tokens: HugeDecimal.fromHumanReadable(5, token.decimals), }, - rewards: 1.23, + rewards: HugeDecimal.fromHumanReadable(1.23, token.decimals), }, { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: 'elsehow', moniker: 'elsehow', @@ -56,14 +57,14 @@ const stakes: TokenStake[] = [ details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 6.2, + tokens: HugeDecimal.fromHumanReadable(6, token.decimals), }, - rewards: 4.56, + rewards: HugeDecimal.fromHumanReadable(4.56, token.decimals), }, { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: 'cosmostation', moniker: 'Cosmostation', @@ -71,9 +72,9 @@ const stakes: TokenStake[] = [ details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 7, + tokens: HugeDecimal.fromHumanReadable(7, token.decimals), }, - rewards: 7.89, + rewards: HugeDecimal.fromHumanReadable(7.89, token.decimals), }, ] @@ -94,7 +95,7 @@ Default.args = { details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 9, + tokens: HugeDecimal.fromHumanReadable(9, token.decimals), }, ], executed: false, diff --git a/packages/stateful/actions/core/actions/ManageStaking/Component.tsx b/packages/stateful/actions/core/actions/ManageStaking/Component.tsx index 8ecba5944..9f331c526 100644 --- a/packages/stateful/actions/core/actions/ManageStaking/Component.tsx +++ b/packages/stateful/actions/core/actions/ManageStaking/Component.tsx @@ -4,10 +4,11 @@ import { ComponentType, useCallback, useEffect } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { InputErrorMessage, InputLabel, - NumberInput, + NumericInput, SelectInput, TokenAmountDisplay, ValidatorPicker, @@ -17,7 +18,6 @@ import { AddressInputProps, TokenStake, Validator } from '@dao-dao/types' import { ActionComponent } from '@dao-dao/types/actions' import { StakingActionType, - convertMicroDenomToDenomWithDecimals, isValidValidatorAddress, makeValidateAddress, makeValidateValidatorAddress, @@ -54,17 +54,17 @@ export const getStakeActions = ( }, ] -export interface ManageStakingOptions { +export type ManageStakingOptions = { nativeBalance: string stakes: TokenStake[] validators: Validator[] executed: boolean - claimedRewards?: number + claimedRewards?: HugeDecimal nativeUnstakingDurationSeconds: number AddressInput: ComponentType> } -export interface ManageStakingData { +export type ManageStakingData = { chainId: string type: StakingActionType validator: string @@ -72,7 +72,7 @@ export interface ManageStakingData { toValidator: string // For use when setting withdraw address. withdrawAddress: string - amount: number + amount: string _error?: string } @@ -96,7 +96,7 @@ export const ManageStakingComponent: ActionComponent< }) => { const { t } = useTranslation() - const { register, watch, setError, clearErrors, setValue } = + const { register, watch, setError, clearErrors, setValue, getValues } = useFormContext() const stakeActions = getStakeActions(t) @@ -122,32 +122,26 @@ export const ManageStakingComponent: ActionComponent< } // Metadata for the given denom. - const minAmount = convertMicroDenomToDenomWithDecimals( - 1, - nativeToken.decimals - ) + const minAmount = HugeDecimal.one.toHumanReadableNumber(nativeToken.decimals) // Get how much is staked and pending for the selected validator. const sourceValidatorStaked = (isValidValidatorAddress(validator, bech32Prefix) && stakes.find(({ validator: { address } }) => address === validator) ?.amount) || - 0 + HugeDecimal.zero const sourceValidatorPendingRewards = (isValidValidatorAddress(validator, bech32Prefix) && stakes.find(({ validator: { address } }) => address === validator) ?.rewards) || - 0 + HugeDecimal.zero // If staking, maxAmount is denom treasury balance. Otherwise (for // undelegating and redelegating), maxAmount is the staked amount for the // source validator. const maxAmount = type === StakingActionType.Delegate - ? convertMicroDenomToDenomWithDecimals( - nativeBalance, - nativeToken.decimals - ) + ? HugeDecimal.from(nativeBalance) : sourceValidatorStaked // Manually validate based on context. @@ -179,18 +173,18 @@ export const ManageStakingComponent: ActionComponent< return true } - const humanReadableAmount = maxAmount.toLocaleString(undefined, { - maximumFractionDigits: 6, - }) - // Logic for undelegating. if (type === StakingActionType.Undelegate) { return ( - Number(amount) <= sourceValidatorStaked || - (sourceValidatorStaked === 0 + sourceValidatorStaked + .toHumanReadable(nativeToken.decimals) + .gte(amount) || + (sourceValidatorStaked.isZero() ? t('error.nothingStaked') : t('error.stakeInsufficient', { - amount: humanReadableAmount, + amount: maxAmount.toInternationalizedHumanReadableString({ + decimals: nativeToken.decimals, + }), tokenSymbol: nativeToken.symbol, })) ) @@ -208,11 +202,15 @@ export const ManageStakingComponent: ActionComponent< } return ( - Number(amount) <= sourceValidatorStaked || - (sourceValidatorStaked === 0 + sourceValidatorStaked + .toHumanReadable(nativeToken.decimals) + .gte(amount) || + (sourceValidatorStaked.isZero() ? t('error.nothingStaked') : t('error.stakeInsufficient', { - amount: humanReadableAmount, + amount: maxAmount.toInternationalizedHumanReadableString({ + decimals: nativeToken.decimals, + }), tokenSymbol: nativeToken.symbol, })) ) @@ -227,6 +225,7 @@ export const ManageStakingComponent: ActionComponent< t, amount, nativeToken.symbol, + nativeToken.decimals, sourceValidatorStaked, toValidator, ]) @@ -253,10 +252,12 @@ export const ManageStakingComponent: ActionComponent< // 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. const delegateWarning = - isCreating && amount > maxAmount && type === StakingActionType.Delegate + isCreating && + maxAmount.toHumanReadable(nativeToken.decimals).lt(amount) && + type === StakingActionType.Delegate ? t('error.insufficientFundsWarning', { - amount: maxAmount.toLocaleString(undefined, { - maximumFractionDigits: nativeToken.decimals, + amount: maxAmount.toInternationalizedHumanReadableString({ + decimals: nativeToken.decimals, }), tokenSymbol: nativeToken.symbol, }) @@ -339,18 +340,18 @@ export const ManageStakingComponent: ActionComponent< {/* If not withdrawing reward or updating withdraw address, show amount input. */} {type !== StakingActionType.WithdrawDelegatorReward && type !== StakingActionType.SetWithdrawAddress && ( - )}
@@ -376,7 +377,7 @@ export const ManageStakingComponent: ActionComponent< // If claiming rewards, show pending rewards if not executed, and // claimed rewards if executed. (executed && !!claimedRewards) || - (!executed && sourceValidatorPendingRewards > 0)) && + (!executed && sourceValidatorPendingRewards.isPositive())) && // Only show balance if creating. isCreating && (
@@ -396,12 +397,21 @@ export const ManageStakingComponent: ActionComponent< amount={ type === StakingActionType.WithdrawDelegatorReward ? executed - ? claimedRewards ?? 0 + ? claimedRewards ?? HugeDecimal.zero : sourceValidatorPendingRewards : maxAmount } decimals={nativeToken.decimals} iconUrl={nativeToken.imageUrl} + onClick={ + type !== StakingActionType.WithdrawDelegatorReward + ? () => + setValue( + (fieldNamePrefix + 'amount') as 'amount', + maxAmount.toHumanReadableString(nativeToken.decimals) + ) + : undefined + } showFullAmount symbol={nativeToken.symbol} /> diff --git a/packages/stateful/actions/core/actions/ManageStaking/README.md b/packages/stateful/actions/core/actions/ManageStaking/README.md index 94b72e928..5ef7d32e0 100644 --- a/packages/stateful/actions/core/actions/ManageStaking/README.md +++ b/packages/stateful/actions/core/actions/ManageStaking/README.md @@ -21,7 +21,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "toValidator": "", // Only used when setting withdraw address. "withdrawAddress": "", - "amount": + "amount": "" } ``` diff --git a/packages/stateful/actions/core/actions/ManageStaking/index.tsx b/packages/stateful/actions/core/actions/ManageStaking/index.tsx index 5e89cf293..ecddc2a46 100644 --- a/packages/stateful/actions/core/actions/ManageStaking/index.tsx +++ b/packages/stateful/actions/core/actions/ManageStaking/index.tsx @@ -1,8 +1,9 @@ -import { coin, parseCoins } from '@cosmjs/amino' +import { parseCoins } from '@cosmjs/amino' import { useQueryClient } from '@tanstack/react-query' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { chainQueries, nativeUnstakingDurationSecondsSelector, @@ -46,8 +47,6 @@ import { } from '@dao-dao/types/protobuf/codegen/cosmos/staking/v1beta1/tx' import { StakingActionType, - convertDenomToMicroDenomWithDecimals, - convertMicroDenomToDenomWithDecimals, getChainAddressForActionOptions, getNativeTokenForChainId, isDecodedStargateMsg, @@ -93,16 +92,13 @@ const InnerComponent: ActionComponent = (props) => { ? { loading: true } : { loading: false, - data: coin( - BigInt( - balances.data.find( - ({ token: { chainId, denomOrAddress } }) => - chainId === nativeToken.chainId && - denomOrAddress === nativeToken.denomOrAddress - )?.balance ?? 0 - ).toString(), - nativeToken.denomOrAddress - ), + data: HugeDecimal.from( + balances.data.find( + ({ token: { chainId, denomOrAddress } }) => + chainId === nativeToken.chainId && + denomOrAddress === nativeToken.denomOrAddress + )?.balance ?? 0 + ).toCoin(nativeToken.denomOrAddress), } const address = getChainAddressForActionOptions(options, chainId) @@ -139,7 +135,7 @@ const InnerComponent: ActionComponent = (props) => { // rewards. If in wallet context, will be undefined. const executedTxLoadable = useExecutedProposalTxLoadable() - let claimedRewards: number | undefined + let claimedRewards: HugeDecimal | undefined if ( executedTxLoadable.state === 'hasValue' && executedTxLoadable.contents && @@ -194,10 +190,7 @@ const InnerComponent: ActionComponent = (props) => { )[0] if (coin) { - claimedRewards = convertMicroDenomToDenomWithDecimals( - coin.amount ?? 0, - nativeToken.decimals - ) + claimedRewards = HugeDecimal.from(coin) } } } @@ -226,14 +219,8 @@ const InnerComponent: ActionComponent = (props) => { ({ validator, delegated, pendingReward }) => ({ token: nativeToken, validator, - amount: convertMicroDenomToDenomWithDecimals( - delegated.amount, - nativeToken.decimals - ), - rewards: convertMicroDenomToDenomWithDecimals( - pendingReward.amount, - nativeToken.decimals - ), + amount: HugeDecimal.from(delegated.amount), + rewards: HugeDecimal.from(pendingReward.amount), }) ), validators: loadingValidators.loading ? [] : loadingValidators.data, @@ -326,7 +313,7 @@ export class ManageStakingAction extends ActionBase { // Default to first validator if exists. validator: firstValidator, toValidator: '', - amount: 1, + amount: '1', withdrawAddress: this.options.address, } } @@ -344,14 +331,10 @@ export class ManageStakingAction extends ActionBase { chainId ) const nativeToken = getNativeTokenForChainId(chainId) - const microAmount = convertDenomToMicroDenomWithDecimals( + const amount = HugeDecimal.fromHumanReadable( macroAmount, nativeToken.decimals - ) - const amount = coin( - BigInt(microAmount).toString(), - nativeToken.denomOrAddress - ) + ).toCoin(nativeToken.denomOrAddress) let msg: UnifiedCosmosMsg switch (type) { @@ -517,7 +500,7 @@ export class ManageStakingAction extends ActionBase { decodedMessage.distribution.withdraw_delegator_reward.validator, // Default values, not needed for displaying this type of message. toValidator: '', - amount: 1, + amount: '1', withdrawAddress: '', } } else if ( @@ -530,7 +513,7 @@ export class ManageStakingAction extends ActionBase { decodedMessage.distribution.set_withdraw_address.address, validator: '', toValidator: '', - amount: 1, + amount: '1', } } } else if ('staking' in decodedMessage) { @@ -555,8 +538,7 @@ export class ManageStakingAction extends ActionBase { action.type === StakingActionType.Redelegate ? data.dst_validator : '', - amount: convertMicroDenomToDenomWithDecimals( - data.amount.amount, + amount: HugeDecimal.from(data.amount.amount).toHumanReadableString( nativeToken.decimals ), withdrawAddress: '', diff --git a/packages/stateful/actions/core/actions/ManageSubDaoPause/Component.tsx b/packages/stateful/actions/core/actions/ManageSubDaoPause/Component.tsx index 5bece8211..d2e038d0c 100644 --- a/packages/stateful/actions/core/actions/ManageSubDaoPause/Component.tsx +++ b/packages/stateful/actions/core/actions/ManageSubDaoPause/Component.tsx @@ -7,7 +7,7 @@ import { InputErrorMessage, InputLabel, Loader, - NumberInput, + NumericInput, SegmentedControlsTitle, useActionOptions, } from '@dao-dao/stateless' @@ -35,7 +35,8 @@ export const ManageSubDaoPauseComponent: ActionComponent< options: { neutronSubdaos, EntityDisplay }, }) => { const { t } = useTranslation() - const { register, watch, setValue } = useFormContext() + const { register, watch, setValue, getValues } = + useFormContext() const { address: daoAddress } = useActionOptions() const address = watch((fieldNamePrefix + 'address') as 'address') @@ -112,23 +113,25 @@ export const ManageSubDaoPauseComponent: ActionComponent<
- diff --git a/packages/stateful/actions/core/actions/ManageVesting/BeginVesting.tsx b/packages/stateful/actions/core/actions/ManageVesting/BeginVesting.tsx index 4f98191ac..0bb5cb531 100644 --- a/packages/stateful/actions/core/actions/ManageVesting/BeginVesting.tsx +++ b/packages/stateful/actions/core/actions/ManageVesting/BeginVesting.tsx @@ -11,6 +11,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, ChainProvider, @@ -20,7 +21,7 @@ import { IconButton, InputErrorMessage, InputLabel, - NumberInput, + NumericInput, RadioInput, RadioInputOption, SelectInput, @@ -51,7 +52,6 @@ import { } from '@dao-dao/types' import { convertDurationWithUnitsToSeconds, - convertMicroDenomToDenomWithDecimals, formatDateTimeTz, getChainAddressForActionOptions, getChainForChainId, @@ -65,7 +65,7 @@ import { export type BeginVestingData = { chainId: string - amount: number + amount: string type: TokenType denomOrAddress: string recipient: string @@ -133,8 +133,15 @@ export const BeginVesting: ActionComponent = ({ throw new Error('Unsupported chain context') } - const { control, register, watch, setValue, setError, clearErrors } = - useFormContext() + const { + control, + register, + watch, + setValue, + getValues, + setError, + clearErrors, + } = useFormContext() const { fields: stepFields, append: appendStep, @@ -201,13 +208,16 @@ export const BeginVesting: ActionComponent = ({ const lastMs = index === 0 ? startDate.getTime() : acc[acc.length - 1].timestamp - const lastAmount = index === 0 ? 0 : acc[acc.length - 1].amount + const lastAmount = + index === 0 ? HugeDecimal.zero : acc[acc.length - 1].amount return [ ...acc, { timestamp: lastMs + delayMs, - amount: lastAmount + (percent / 100) * watchAmount, + amount: lastAmount.plus( + HugeDecimal.from(watchAmount).times(percent).div(100) + ), }, ] }, [] as VestingStep[]) @@ -236,11 +246,7 @@ export const BeginVesting: ActionComponent = ({ ({ token: { denomOrAddress } }) => denomOrAddress === watchDenomOrAddress ) const selectedDecimals = selectedToken?.token.decimals ?? 0 - const selectedMicroBalance = selectedToken?.balance ?? 0 - const selectedBalance = convertMicroDenomToDenomWithDecimals( - selectedMicroBalance, - selectedDecimals - ) + const selectedBalance = HugeDecimal.from(selectedToken?.balance ?? 0) const selectedSymbol = selectedToken?.token?.symbol ?? t('info.tokens') const configureVestingPaymentAction = useInitializedActionForKey( @@ -287,16 +293,17 @@ export const BeginVesting: ActionComponent = ({ // A warning if 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. - const insufficientFundsWarning = - watchAmount > selectedBalance - ? t('error.insufficientFundsWarning', { - amount: selectedBalance.toLocaleString(undefined, { - maximumFractionDigits: selectedDecimals, - }), - tokenSymbol: - selectedToken?.token.symbol ?? t('info.token').toLocaleUpperCase(), - }) - : undefined + const insufficientFundsWarning = selectedBalance + .toHumanReadable(selectedDecimals) + .lt(watchAmount) + ? t('error.insufficientFundsWarning', { + amount: selectedBalance.toInternationalizedHumanReadableString({ + decimals: selectedDecimals, + }), + tokenSymbol: + selectedToken?.token.symbol ?? t('info.token').toLocaleUpperCase(), + }) + : undefined return ( @@ -366,11 +373,12 @@ export const BeginVesting: ActionComponent = ({ amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'amount') as 'amount', error: errors?.amount, - min: convertMicroDenomToDenomWithDecimals(1, selectedDecimals), - step: convertMicroDenomToDenomWithDecimals(1, selectedDecimals), + min: HugeDecimal.one.toHumanReadableNumber(selectedDecimals), + step: HugeDecimal.one.toHumanReadableNumber(selectedDecimals), }} onSelectToken={({ chainId, type, denomOrAddress }) => { setValue((fieldNamePrefix + 'chainId') as 'chainId', chainId) @@ -394,11 +402,10 @@ export const BeginVesting: ActionComponent = ({ description: t('title.balance') + ': ' + - convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + HugeDecimal.from( + balance + ).toInternationalizedHumanReadableString({ + decimals: token.decimals, }), })), }} @@ -603,22 +610,23 @@ export const BeginVesting: ActionComponent = ({
- @@ -637,14 +645,16 @@ export const BeginVesting: ActionComponent = ({
- = ({ count: steps[index].delay.value, }).toLocaleLowerCase() } - validation={[validatePositive, validateRequired]} - watch={watch} + validation={[validateRequired, validatePositive]} /> {isCreating && ( diff --git a/packages/stateful/actions/core/actions/ManageVesting/CancelVesting.tsx b/packages/stateful/actions/core/actions/ManageVesting/CancelVesting.tsx index 8c8486833..30e1ba6fc 100644 --- a/packages/stateful/actions/core/actions/ManageVesting/CancelVesting.tsx +++ b/packages/stateful/actions/core/actions/ManageVesting/CancelVesting.tsx @@ -19,7 +19,6 @@ import { VestingInfo, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, formatDateTimeTz, getChainAddressForActionOptions, } from '@dao-dao/utils' @@ -123,10 +122,7 @@ export const CancelVesting: ActionComponent = ({

", "begin": { - "amount": , + "chainId": "", + "amount": "", + "type": "", "denomOrAddress": "", "recipient": "", "title": "", // Optional. "description": "<DESCRIPTION>", "startDate": "<START DATE>", - "duration": { - "value": <DURATION VALUE>, - "units": "<seconds | minutes | hours | days | weeks | months | years>" - } + "ownerMode": "<none | me | other | many>", + "otherOwner": "<ADDRESS>", + "manyOwners": [ + { "address": "<ADDRESS>" }, + ... + ], + "manyOwnersCw1WhitelistContract": "<MANY OWNER CW1-WHITELIST ADDRESS>", + "steps": [ + { + "percent": <PERCENT>, + "delay": { + "value": <DURATION VALUE>, + "units": "<seconds | minutes | hours | days | weeks | months | years>" + } + }, + ... + ] }, "cancel": { + "chainId": "<CHAIN ID>", "address": "<VESTING PAYMENT ADDRESS>" }, "registerSlash": { + "chainId": "<CHAIN ID>", "address": "<VESTING PAYMENT ADDRESS>", "valiator": "<VALIDATOR ADDRESS>", "time": "<TIME OF SLASH>", diff --git a/packages/stateful/actions/core/actions/ManageVesting/RegisterSlash.tsx b/packages/stateful/actions/core/actions/ManageVesting/RegisterSlash.tsx index ab22a0283..efc85ea80 100644 --- a/packages/stateful/actions/core/actions/ManageVesting/RegisterSlash.tsx +++ b/packages/stateful/actions/core/actions/ManageVesting/RegisterSlash.tsx @@ -2,6 +2,7 @@ import { ComponentType } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { ButtonLink, ChainProvider, @@ -23,7 +24,6 @@ import { VestingValidatorSlash, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, getChainAddressForActionOptions, getNativeTokenForChainId, } from '@dao-dao/utils' @@ -241,10 +241,7 @@ const RenderVest = ({ validatorOperatorAddress, <TokenAmountDisplay key={`${index}-token`} - amount={convertMicroDenomToDenomWithDecimals( - slash.unregisteredAmount, - nativeToken.decimals - )} + amount={HugeDecimal.from(slash.unregisteredAmount)} decimals={nativeToken.decimals} iconUrl={nativeToken.imageUrl} symbol={nativeToken.symbol} @@ -267,11 +264,7 @@ const RenderVest = ({ </p> <TokenAmountDisplay - key="token" - amount={convertMicroDenomToDenomWithDecimals( - data.amount, - nativeToken.decimals - )} + amount={HugeDecimal.from(data.amount)} decimals={nativeToken.decimals} iconUrl={nativeToken.imageUrl} symbol={nativeToken.symbol} diff --git a/packages/stateful/actions/core/actions/ManageVesting/index.tsx b/packages/stateful/actions/core/actions/ManageVesting/index.tsx index 741332e41..238cb8064 100644 --- a/packages/stateful/actions/core/actions/ManageVesting/index.tsx +++ b/packages/stateful/actions/core/actions/ManageVesting/index.tsx @@ -1,9 +1,9 @@ -import { coins } from '@cosmjs/amino' import { useQueries, useQueryClient } from '@tanstack/react-query' import { ComponentType, useEffect } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { chainQueries, cw1WhitelistExtraQueries, @@ -46,9 +46,7 @@ import { import { InstantiateMsg as VestingInstantiateMsg } from '@dao-dao/types/contracts/CwVesting' import { chainIsIndexed, - convertDenomToMicroDenomWithDecimals, convertDurationWithUnitsToSeconds, - convertMicroDenomToDenomWithDecimals, convertSecondsToDurationWithUnits, decodeJsonFromBase64, encodeJsonToBase64, @@ -502,7 +500,7 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { mode: this.widgetData ? 'begin' : 'cancel', begin: { chainId: this.options.chain.chain_id, - amount: 1, + amount: '1', type: TokenType.Native, denomOrAddress: getNativeTokenForChainId(this.options.chain.chain_id) .denomOrAddress, @@ -597,10 +595,7 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { : null, ]) - const total = convertDenomToMicroDenomWithDecimals( - begin.amount, - token.decimals - ) + const total = HugeDecimal.fromHumanReadable(begin.amount, token.decimals) const vestingDurationSeconds = begin.steps.reduce( (acc, { delay }) => acc + convertDurationWithUnitsToSeconds(delay), @@ -664,14 +659,13 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { ...acc, [ lastSeconds + delaySeconds, - BigInt( + HugeDecimal.from( // For the last step, use total to avoid rounding // issues. index === begin.steps.length - 1 ? total - : Math.round( - Number(lastAmount) + - (percent / 100) * Number(total) + : HugeDecimal.from(lastAmount).plus( + total.times(percent / 100) ) ).toString(), ], @@ -687,7 +681,7 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { ).toString() : '', title: begin.title, - total: BigInt(total).toString(), + total: total.toString(), unbonding_duration_seconds: token.type === TokenType.Native && token.denomOrAddress === @@ -710,7 +704,7 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { msg: { instantiate_native_payroll_contract: msg, } as ExecuteMsg, - funds: coins(total, token.denomOrAddress), + funds: total.toCoins(token.denomOrAddress), }) } else if (token.type === TokenType.Cw20) { // Execute CW20 send message. @@ -720,7 +714,7 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { sender: vestingSource.owner, msg: { send: { - amount: BigInt(total).toString(), + amount: total.toString(), contract: vestingSource.factory, msg: encodeJsonToBase64({ instantiate_payroll_contract: msg, @@ -958,8 +952,7 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { ).toISOString() : '', title: instantiateMsg.title, - amount: convertMicroDenomToDenomWithDecimals( - instantiateMsg.total, + amount: HugeDecimal.from(instantiateMsg.total).toHumanReadableString( token.decimals ), ownerMode, @@ -1014,13 +1007,11 @@ export class ManageVestingAction extends ActionBase<ManageVestingData> { return [ ...acc, { - percent: Number( - ( - ((Number(amount) - Number(pastAmount)) / - Number(instantiateMsg!.total)) * - 100 - ).toFixed(2) - ), + percent: HugeDecimal.from(amount) + .minus(pastAmount) + .div(instantiateMsg!.total) + .times(100) + .toNumber(2), delay: convertSecondsToDurationWithUnits( seconds - pastTimestamp ), diff --git a/packages/stateful/actions/core/actions/Migrate/Component.tsx b/packages/stateful/actions/core/actions/Migrate/Component.tsx index 58235ed38..125187e0c 100644 --- a/packages/stateful/actions/core/actions/Migrate/Component.tsx +++ b/packages/stateful/actions/core/actions/Migrate/Component.tsx @@ -6,7 +6,7 @@ import { CodeMirrorInput, InputErrorMessage, InputLabel, - NumberInput, + NumericInput, StatusCard, useActionOptions, useChain, @@ -72,13 +72,16 @@ export const MigrateContractComponent: ActionComponent<MigrateOptions> = ({ </div> <div className="flex flex-col gap-1"> <InputLabel name={t('form.codeId')} /> - <NumberInput + <NumericInput containerClassName="xs:h-full" disabled={!isCreating} error={errors?.codeId} fieldName={fieldNamePrefix + 'codeId'} + min={1} + numericValue register={register} sizing="fill" + step={1} validation={[validateRequired, validatePositive]} /> <InputErrorMessage error={errors?.codeId} /> diff --git a/packages/stateful/actions/core/actions/Spend/Component.stories.tsx b/packages/stateful/actions/core/actions/Spend/Component.stories.tsx index 96ef861b7..817461a13 100644 --- a/packages/stateful/actions/core/actions/Spend/Component.stories.tsx +++ b/packages/stateful/actions/core/actions/Spend/Component.stories.tsx @@ -21,7 +21,7 @@ export default { toChainId: CHAIN_ID, from: '', to: '', - amount: 1, + amount: '1', cw20: false, denom: getNativeTokenForChainId(CHAIN_ID).denomOrAddress, }), diff --git a/packages/stateful/actions/core/actions/Spend/Component.tsx b/packages/stateful/actions/core/actions/Spend/Component.tsx index 77e3bd86e..8bfba43a7 100644 --- a/packages/stateful/actions/core/actions/Spend/Component.tsx +++ b/packages/stateful/actions/core/actions/Spend/Component.tsx @@ -1,9 +1,9 @@ import { ArrowRightAltRounded } from '@mui/icons-material' -import clsx from 'clsx' import { ComponentType, RefAttributes, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { AccountSelector, Button, @@ -16,7 +16,7 @@ import { InputLabel, InputThemedText, Loader, - NumberInput, + NumericInput, PercentButton, SelectInput, StatusCard, @@ -46,7 +46,6 @@ import { } from '@dao-dao/types/actions' import { Params as NobleTariffParams } from '@dao-dao/types/protobuf/codegen/tariff/params' import { - convertMicroDenomToDenomWithDecimals, formatDateTimeTz, formatPercentOf100, getAccountAddress, @@ -74,7 +73,7 @@ export type SpendData = { */ from: string to: string - amount: number + amount: string denom: string /** * Whether or not `denom` is a CW20 token address. CW20 tokens cannot be sent @@ -121,7 +120,7 @@ export type SpendOptions = { // If this is an IBC transfer, this is the path of chains. ibcPath: LoadingDataWithError<string[]> // If this is an IBC transfer, show the expected receive amount. - ibcAmountOut: LoadingDataWithError<number | undefined> + ibcAmountOut: LoadingDataWithError<HugeDecimal | undefined> // If this is an IBC transfer and a multi-TX route exists that unwinds the // tokens correctly but doesn't use PFM, this is the better path. betterNonPfmIbcPath: LoadingData<string[] | undefined> @@ -169,7 +168,7 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ chain: { chain_id: mainChainId }, } = useActionOptions() - const { register, watch, setValue } = useFormContext<SpendData>() + const { register, watch, setValue, getValues } = useFormContext<SpendData>() const spendChainId = watch((fieldNamePrefix + 'fromChainId') as 'fromChainId') const spendAmount = watch((fieldNamePrefix + 'amount') as 'amount') @@ -268,15 +267,13 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ token.denomOrAddress === spendDenom && (token.type === TokenType.Cw20) === isCw20 ) - const balance = convertMicroDenomToDenomWithDecimals( - selectedToken?.balance ?? 0, - selectedToken?.token.decimals ?? 0 - ) const decimals = loadedCustomToken ? token.data.decimals : selectedToken?.token.decimals || 0 + const balance = HugeDecimal.from(selectedToken?.balance ?? 0) + // 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. @@ -286,10 +283,10 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ ? undefined : !selectedToken ? t('error.unknownDenom', { denom: spendDenom }) - : spendAmount > balance + : balance.toHumanReadable(decimals).lt(spendAmount) ? t('error.insufficientFundsWarning', { - amount: balance.toLocaleString(undefined, { - maximumFractionDigits: decimals, + amount: balance.toInternationalizedHumanReadableString({ + decimals, }), tokenSymbol: symbol, }) @@ -333,11 +330,12 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'amount') as 'amount', error: errors?.amount, - min: convertMicroDenomToDenomWithDecimals(1, decimals), - step: convertMicroDenomToDenomWithDecimals(1, decimals), + min: HugeDecimal.one.toHumanReadableNumber(decimals), + step: HugeDecimal.one.toHumanReadableNumber(decimals), // For custom token, show unit if loaded successfully. unit: loadedCustomToken ? token.data.symbol : undefined, unitIconUrl: loadedCustomToken @@ -345,7 +343,7 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ : undefined, unitClassName: '!text-text-primary', }} - containerClassName={clsx('grow !max-w-full')} + containerClassName="grow !max-w-full" onCustomTokenChange={(custom) => { setValue((fieldNamePrefix + 'denom') as 'denom', custom) // If denom entered is a valid contract address, it's most @@ -417,11 +415,10 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ description: t('title.balance') + ': ' + - convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + HugeDecimal.from( + balance + ).toInternationalizedHumanReadableString({ + decimals: token.decimals, }), })), } @@ -481,27 +478,31 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ decimals={selectedToken.token.decimals} iconUrl={selectedToken.token.imageUrl} onClick={() => - setValue((fieldNamePrefix + 'amount') as 'amount', balance) + setValue( + (fieldNamePrefix + 'amount') as 'amount', + balance.toHumanReadableString(decimals) + ) } showFullAmount symbol={selectedToken.token.symbol} /> </div> - {balance > 0 && ( + {balance.isPositive() && ( <div className="grid grid-cols-5 gap-1"> {[10, 25, 50, 75, 100].map((percent) => ( <PercentButton key={percent} - amount={spendAmount} - decimals={selectedToken.token.decimals} - label={`${percent}%`} + amount={HugeDecimal.fromHumanReadable( + spendAmount, + decimals + )} loadingMax={{ loading: false, data: balance }} - percent={percent / 100} + percent={percent} setAmount={(amount) => setValue( (fieldNamePrefix + 'amount') as 'amount', - amount + amount.toHumanReadableString(decimals) ) } /> @@ -565,7 +566,7 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ ) : ( <div className="flex flex-row gap-3 items-center"> <TokenAmountDisplay - amount={spendAmount} + amount={HugeDecimal.fromHumanReadable(spendAmount, decimals)} decimals={decimals} iconClassName="!h-6 !w-6" iconUrl={ @@ -669,11 +670,10 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ fee: neutronTransferFee.data .map(({ token, balance }) => t('format.token', { - amount: convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + amount: HugeDecimal.from( + balance + ).toInternationalizedHumanReadableString({ + decimals: token.decimals, }), symbol: token.symbol, }) @@ -795,14 +795,16 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ {isCreating ? ( <> <div className="flex flex-row gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.ibcTimeout?.value} fieldName={ (fieldNamePrefix + 'ibcTimeout.value') as 'ibcTimeout.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="md" @@ -814,8 +816,7 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ count: ibcTimeout?.value, }).toLocaleLowerCase() } - validation={[validatePositive, validateRequired]} - watch={watch} + validation={[validateRequired, validatePositive]} /> {isCreating && ( @@ -857,9 +858,12 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ {selectedToken && !ibcAmountOut.loading && + !ibcAmountOut.updating && !ibcAmountOut.errored && ibcAmountOut.data && - ibcAmountOut.data !== spendAmount && ( + !ibcAmountOut.data + .toHumanReadable(selectedToken.token.decimals) + .eq(spendAmount) && ( <div className="flex flex-col gap-2 mt-1"> <InputLabel name={t('form.amountReceived')} /> @@ -880,25 +884,27 @@ export const SpendComponent: ActionComponent<SpendOptions> = ({ type NobleTariffProps = { token: GenericToken - amount: number + amount: string params: NobleTariffParams } const NobleTariff = ({ token: { symbol, decimals }, - amount, + amount: _amount, params: { transferFeeBps, transferFeeMax }, }: NobleTariffProps) => { const { t } = useTranslation() + const amount = HugeDecimal.fromHumanReadable(_amount, decimals) + const feeDecimal = Number(transferFeeBps) / 1e4 - const maxFee = convertMicroDenomToDenomWithDecimals(transferFeeMax, decimals) + const maxFee = HugeDecimal.from(transferFeeMax) const fee = - amount && !isNaN(amount) - ? Math.min(Number((amount * feeDecimal).toFixed(decimals)), maxFee) - : 0 + _amount && !amount.isNaN() + ? HugeDecimal.min(amount.times(feeDecimal), maxFee) + : HugeDecimal.zero - if (fee === 0) { + if (fee.isZero()) { return null } @@ -907,14 +913,14 @@ const NobleTariff = ({ {t('info.nobleTariffApplied', { feePercent: formatPercentOf100(feeDecimal * 100), tokenSymbol: symbol, - maxFee: maxFee.toLocaleString(undefined, { - maximumFractionDigits: decimals, + maxFee: maxFee.toInternationalizedHumanReadableString({ + decimals, }), - fee: fee.toLocaleString(undefined, { - maximumFractionDigits: decimals, + fee: fee.toInternationalizedHumanReadableString({ + decimals, }), - output: (amount - fee).toLocaleString(undefined, { - maximumFractionDigits: decimals, + output: amount.minus(fee).toInternationalizedHumanReadableString({ + decimals, }), })} </p> diff --git a/packages/stateful/actions/core/actions/Spend/README.md b/packages/stateful/actions/core/actions/Spend/README.md index 6a4d693ce..1e2e40c48 100644 --- a/packages/stateful/actions/core/actions/Spend/README.md +++ b/packages/stateful/actions/core/actions/Spend/README.md @@ -19,7 +19,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "toChainId": "<CHAIN ID>", "from": "<FROM ADDRESS>", "to": "<RECIPIENT ADDRESS>", - "amount": <AMOUNT>, + "amount": "<AMOUNT>", "denom": "<DENOM>", "cw20": "<true | false>", "ibcTimeout": { @@ -29,12 +29,6 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). } ``` -You must set `decimals` correctly for the specified `amount` of funds. The final -message gets generated with `amount * 10^(decimals)` microunits of the specified -`denom`. For example: `amount = 5`, `decimals = 6`, and `denom = "untrn"` => -`5000000untrn` or `5 * 10^6 untrn`, where `untrn` is the microdenom/true -denomination of `NTRN`. - If used in a DAO, `fromChainId` and `from` determine which account has the tokens being sent, which can be the native chain or any supported Polytone or ICA chain. `toChainId` is unrelated, and it determines if the tokens are sent to diff --git a/packages/stateful/actions/core/actions/Spend/index.tsx b/packages/stateful/actions/core/actions/Spend/index.tsx index 4d142e262..8b9b6eb15 100644 --- a/packages/stateful/actions/core/actions/Spend/index.tsx +++ b/packages/stateful/actions/core/actions/Spend/index.tsx @@ -1,9 +1,9 @@ -import { coin, coins } from '@cosmjs/amino' import { useQueryClient } from '@tanstack/react-query' import { ComponentType, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import { constSelector, useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { neutronQueries, nobleQueries, @@ -49,9 +49,7 @@ import { MsgTransfer } from '@dao-dao/types/protobuf/codegen/ibc/applications/tr import { MsgTransfer as NeutronMsgTransfer } from '@dao-dao/types/protobuf/codegen/neutron/transfer/v1/tx' import { MAINNET, - convertDenomToMicroDenomStringWithDecimals, convertDurationWithUnitsToSeconds, - convertMicroDenomToDenomWithDecimals, decodeMessage, getAccountAddress, getChainForChainId, @@ -254,10 +252,10 @@ const StatefulSpendComponent: ComponentType< const amountIn = selectedToken && amount - ? convertDenomToMicroDenomStringWithDecimals( + ? HugeDecimal.fromHumanReadable( amount, selectedToken.token.decimals - ) + ).toString() : undefined // Get Skip route for IBC transfer. @@ -395,7 +393,7 @@ const StatefulSpendComponent: ComponentType< errored: false, } // Get the amount out from an IBC path. - const ibcAmountOut: LoadingDataWithError<number | undefined> = isIbc + const ibcAmountOut: LoadingDataWithError<HugeDecimal | undefined> = isIbc ? skipRoute.loading || !selectedToken ? { loading: true, @@ -411,10 +409,7 @@ const StatefulSpendComponent: ComponentType< loading: false, errored: false, updating: skipRoute.updating, - data: convertMicroDenomToDenomWithDecimals( - skipRoute.data.amount_out, - selectedToken.token.decimals - ), + data: HugeDecimal.from(skipRoute.data.amount_out), } : { loading: false, @@ -561,7 +556,7 @@ export class SpendAction extends ActionBase<SpendData> { toChainId: this.options.chain.chain_id, from: this.options.address, to: '', - amount: 1, + amount: '1', denom: nativeToken?.denomOrAddress || '', cw20: nativeToken?.type === TokenType.Cw20, ibcTimeout: { @@ -593,7 +588,7 @@ export class SpendAction extends ActionBase<SpendData> { type: cw20 ? TokenType.Cw20 : TokenType.Native, }) ) - const amount = convertDenomToMicroDenomStringWithDecimals(_amount, decimals) + const amount = HugeDecimal.fromHumanReadable(_amount, decimals) // Gov module community pool spend. if (this.options.context.type === ActionContextType.Gov) { @@ -603,7 +598,7 @@ export class SpendAction extends ActionBase<SpendData> { value: MsgCommunityPoolSpend.fromPartial({ authority: this.options.address, recipient: to, - amount: coins(amount, denom), + amount: amount.toCoins(denom), }), }, }) @@ -673,7 +668,7 @@ export class SpendAction extends ActionBase<SpendData> { value: { sourcePort: 'transfer', sourceChannel, - token: coin(amount, denom), + token: amount.toCoin(denom), sender: from, receiver: to, timeoutTimestamp, @@ -743,7 +738,7 @@ export class SpendAction extends ActionBase<SpendData> { } } else if (!cw20) { msg = { - bank: makeBankMessage(amount, to, denom), + bank: makeBankMessage(amount.toString(), to, denom), } } else { msg = makeWasmMessage({ @@ -754,7 +749,7 @@ export class SpendAction extends ActionBase<SpendData> { msg: { transfer: { recipient: to, - amount, + amount: amount.toString(), }, }, }, @@ -1004,17 +999,16 @@ export class SpendAction extends ActionBase<SpendData> { toChainId, from, to, - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.stargate.value.token.amount, - token.decimals - ), + amount: HugeDecimal.from( + decodedMessage.stargate.value.token.amount + ).toHumanReadableString(token.decimals), denom: token.denomOrAddress, // Should always be false. cw20: token.type === TokenType.Cw20, // Nanoseconds to milliseconds. _absoluteIbcTimeout: Number( - decodedMessage.stargate.value.timeoutTimestamp / BigInt(1e6) + BigInt(decodedMessage.stargate.value.timeoutTimestamp) / BigInt(1e6) ), _ibcData: { @@ -1028,10 +1022,9 @@ export class SpendAction extends ActionBase<SpendData> { toChainId: chainId, from, to: decodedMessage.bank.send.to_address, - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.bank.send.amount[0].amount, - token.decimals - ), + amount: HugeDecimal.from( + decodedMessage.bank.send.amount[0].amount + ).toHumanReadableString(token.decimals), denom: token.denomOrAddress, cw20: false, } @@ -1041,10 +1034,9 @@ export class SpendAction extends ActionBase<SpendData> { toChainId: chainId, from, to: decodedMessage.wasm.execute.msg.transfer.recipient, - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.wasm.execute.msg.transfer.amount, - token.decimals - ), + amount: HugeDecimal.from( + decodedMessage.wasm.execute.msg.transfer.amount + ).toHumanReadableString(token.decimals), denom: decodedMessage.wasm.execute.contract_addr, cw20: true, } @@ -1056,8 +1048,8 @@ export class SpendAction extends ActionBase<SpendData> { transformImportData(data: any): SpendData { return { ...data, - // Ensure amount is a number. - amount: Number(data.amount), + // Ensure amount is a string. + amount: HugeDecimal.from(data.amount).toString(), } } } diff --git a/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx b/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx index c830dcc93..3e59ec2de 100644 --- a/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx +++ b/packages/stateful/actions/core/actions/UpdateInfo/Component.tsx @@ -10,7 +10,7 @@ import { TextAreaInput, TextInput, useActionOptions, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { ChainId, ContractVersion } from '@dao-dao/types' import { ActionComponent, ActionContextType } from '@dao-dao/types/actions' @@ -29,7 +29,7 @@ export const UpdateInfoComponent: ActionComponent< undefined, UpdateInfoData > = ({ fieldNamePrefix, errors, isCreating, data }) => { - const { name } = useDaoInfoContext() + const { name } = useDao() const { address, context } = useActionOptions() const { t } = useTranslation() const { register, watch, setValue } = useFormContext() diff --git a/packages/stateful/actions/core/actions/UpdateRewardDistribution/Component.tsx b/packages/stateful/actions/core/actions/UpdateRewardDistribution/Component.tsx index d3e3ace63..7a9f1da12 100644 --- a/packages/stateful/actions/core/actions/UpdateRewardDistribution/Component.tsx +++ b/packages/stateful/actions/core/actions/UpdateRewardDistribution/Component.tsx @@ -2,13 +2,14 @@ import { ArrowDropDown } from '@mui/icons-material' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { FilterableItemPopup, InputErrorMessage, InputLabel, InputThemedText, MarkdownRenderer, - NumberInput, + NumericInput, SegmentedControls, SelectInput, StatusCard, @@ -22,7 +23,6 @@ import { import { ActionComponent } from '@dao-dao/types/actions' import { convertDurationToDurationWithUnits, - convertMicroDenomToDenomWithDecimals, getFallbackImage, getHumanReadableRewardDistributionLabel, toAccessibleImageUrl, @@ -35,7 +35,7 @@ export type UpdateRewardDistributionData = { id: number immediate: boolean rate: { - amount: number + amount: string duration: DurationWithUnits } openFunding?: boolean | null @@ -52,7 +52,7 @@ export const UpdateRewardDistributionComponent: ActionComponent< UpdateRewardDistributionOptions > = ({ fieldNamePrefix, errors, isCreating, options: { distributions } }) => { const { t } = useTranslation() - const { register, setValue, watch } = + const { register, setValue, getValues, watch } = useFormContext<UpdateRewardDistributionData>() const address = watch((fieldNamePrefix + 'address') as 'address') @@ -67,10 +67,7 @@ export const UpdateRewardDistributionComponent: ActionComponent< (distribution) => distribution.address === address && distribution.id === id ) - const minAmount = convertMicroDenomToDenomWithDecimals( - 1, - selectedDistribution?.token.decimals ?? 0 - ) + const decimals = selectedDistribution?.token.decimals ?? 0 const selectedDistributionDisplay = selectedDistribution && ( <> @@ -115,10 +112,9 @@ export const UpdateRewardDistributionComponent: ActionComponent< if ('linear' in active_epoch.emission_rate) { setValue( (fieldNamePrefix + 'rate.amount') as 'rate.amount', - convertMicroDenomToDenomWithDecimals( - active_epoch.emission_rate.linear.amount, - token.decimals - ) + HugeDecimal.from( + active_epoch.emission_rate.linear.amount + ).toHumanReadableString(token.decimals) ) setValue( (fieldNamePrefix + 'rate.duration') as 'rate.duration', @@ -192,22 +188,22 @@ export const UpdateRewardDistributionComponent: ActionComponent< {!immediate && ( <div className="flex flex-wrap flex-row gap-x-4 gap-y-2 px-4 py-3 bg-background-tertiary rounded-md max-w-prose"> - <NumberInput + <NumericInput containerClassName="grow" disabled={!isCreating} error={errors?.rate?.amount} fieldName={(fieldNamePrefix + 'rate.amount') as 'rate.amount'} - min={minAmount} + getValues={getValues} + min={HugeDecimal.one.toHumanReadableNumber(decimals)} register={register} setValue={setValue} - step={minAmount} + step={HugeDecimal.one.toHumanReadableNumber(decimals)} unit={ selectedDistribution?.token ? '$' + selectedDistribution.token.symbol : t('info.tokens') } validation={[validateRequired, validatePositive]} - watch={watch} /> <div className="flex flex-row grow gap-4 justify-between items-center"> @@ -215,20 +211,21 @@ export const UpdateRewardDistributionComponent: ActionComponent< <div className="flex grow flex-row gap-2"> <div className="flex flex-col gap-1 grow"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.rate?.duration?.value} fieldName={ (fieldNamePrefix + 'rate.duration.value') as 'rate.duration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="none" step={1} validation={[validatePositive, validateRequired]} - watch={watch} /> <InputErrorMessage error={errors?.rate?.duration?.value} diff --git a/packages/stateful/actions/core/actions/UpdateRewardDistribution/README.md b/packages/stateful/actions/core/actions/UpdateRewardDistribution/README.md index 4ba51b97f..6aa9055ad 100644 --- a/packages/stateful/actions/core/actions/UpdateRewardDistribution/README.md +++ b/packages/stateful/actions/core/actions/UpdateRewardDistribution/README.md @@ -19,7 +19,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). "id": <ID>, "immediate": <true | false>, "rate": { - "amount": <AMOUNT>, + "amount": "<AMOUNT>", "unit": "<seconds | minutes | hours | days | weeks | months | years | blocks>" }, "openFunding": <true | false> diff --git a/packages/stateful/actions/core/actions/UpdateRewardDistribution/index.tsx b/packages/stateful/actions/core/actions/UpdateRewardDistribution/index.tsx index b9e4b5e5d..822f20a9e 100644 --- a/packages/stateful/actions/core/actions/UpdateRewardDistribution/index.tsx +++ b/packages/stateful/actions/core/actions/UpdateRewardDistribution/index.tsx @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { daoRewardsDistributorExtraQueries } from '@dao-dao/state/query' import { ActionBase, ConstructionEmoji } from '@dao-dao/stateless' import { @@ -15,10 +16,8 @@ import { ProcessedMessage, } from '@dao-dao/types/actions' import { - convertDenomToMicroDenomStringWithDecimals, convertDurationToDurationWithUnits, convertDurationWithUnitsToDuration, - convertMicroDenomToDenomWithDecimals, getDaoRewardDistributors, makeExecuteSmartContractMessage, objectMatchesStructure, @@ -99,11 +98,10 @@ export class UpdateRewardDistributionAction extends ActionBase<UpdateRewardDistr emissionRate && !('immediate' in emissionRate) && !('paused' in emissionRate) - ? convertMicroDenomToDenomWithDecimals( - emissionRate.linear.amount, - this.distributions[0].token.decimals - ) - : 1, + ? HugeDecimal.from( + emissionRate.linear.amount + ).toHumanReadableString(this.distributions[0].token.decimals) + : '1', duration: emissionRate && !('immediate' in emissionRate) && @@ -145,10 +143,10 @@ export class UpdateRewardDistributionAction extends ActionBase<UpdateRewardDistr } : { linear: { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( rate.amount, distribution.token.decimals - ), + ).toString(), duration: convertDurationWithUnitsToDuration(rate.duration), continuous: false, }, @@ -204,13 +202,11 @@ export class UpdateRewardDistributionAction extends ActionBase<UpdateRewardDistr id: updateMsg.id, immediate: 'immediate' in updateMsg.emission_rate, rate: { - amount: + amount: HugeDecimal.from( 'linear' in updateMsg.emission_rate - ? convertMicroDenomToDenomWithDecimals( - updateMsg.emission_rate.linear.amount, - distribution.token.decimals - ) - : 1, + ? updateMsg.emission_rate.linear.amount + : 1 + ).toHumanReadableString(distribution.token.decimals), duration: 'linear' in updateMsg.emission_rate ? convertDurationToDurationWithUnits( diff --git a/packages/stateful/actions/core/actions/WithdrawRewardDistribution/Component.tsx b/packages/stateful/actions/core/actions/WithdrawRewardDistribution/Component.tsx index ce08bf177..55ed50e17 100644 --- a/packages/stateful/actions/core/actions/WithdrawRewardDistribution/Component.tsx +++ b/packages/stateful/actions/core/actions/WithdrawRewardDistribution/Component.tsx @@ -13,7 +13,6 @@ import { import { DaoRewardDistributionWithRemaining } from '@dao-dao/types' import { ActionComponent } from '@dao-dao/types/actions' import { - convertMicroDenomToDenomWithDecimals, getFallbackImage, getHumanReadableRewardDistributionLabel, toAccessibleImageUrl, @@ -83,10 +82,7 @@ export const WithdrawRewardDistributionComponent: ActionComponent< label: getHumanReadableRewardDistributionLabel(t, distribution), description: ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - distribution.remaining, - distribution.token.decimals - )} + amount={distribution.remaining} className="text-text-interactive-valid" decimals={distribution.token.decimals} suffix={' ' + t('info.remaining')} @@ -120,12 +116,12 @@ export const WithdrawRewardDistributionComponent: ActionComponent< {selectedDistribution && ( <p className="text-text-interactive-valid"> {t('info.tokensWillBeWithdrawn', { - amount: convertMicroDenomToDenomWithDecimals( - selectedDistribution.remaining, - selectedDistribution.token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: selectedDistribution.token.decimals, - }), + amount: + selectedDistribution.remaining.toInternationalizedHumanReadableString( + { + decimals: selectedDistribution.token.decimals, + } + ), tokenSymbol: selectedDistribution.token.symbol, })} </p> diff --git a/packages/stateful/actions/core/actions/WithdrawRewardDistribution/index.tsx b/packages/stateful/actions/core/actions/WithdrawRewardDistribution/index.tsx index f07288f5d..419ecf3a1 100644 --- a/packages/stateful/actions/core/actions/WithdrawRewardDistribution/index.tsx +++ b/packages/stateful/actions/core/actions/WithdrawRewardDistribution/index.tsx @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { daoRewardsDistributorExtraQueries, daoRewardsDistributorQueries, @@ -89,7 +90,7 @@ export class WithdrawRewardDistributionAction extends ActionBase<WithdrawRewardD distribution ): Promise<DaoRewardDistributionWithRemaining> => ({ ...distribution, - remaining: Number( + remaining: HugeDecimal.from( await this.options.queryClient.fetchQuery( daoRewardsDistributorQueries.undistributedRewards({ chainId: this.options.chain.chain_id, diff --git a/packages/stateful/actions/core/actions/token_swap/PerformTokenSwap/index.tsx b/packages/stateful/actions/core/actions/token_swap/PerformTokenSwap/index.tsx index b071cda97..e5f5d0b82 100644 --- a/packages/stateful/actions/core/actions/token_swap/PerformTokenSwap/index.tsx +++ b/packages/stateful/actions/core/actions/token_swap/PerformTokenSwap/index.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { contractQueries } from '@dao-dao/state/query' import { ActionBase, @@ -22,7 +23,6 @@ import { } from '@dao-dao/types/actions' import { ContractName, - convertDenomToMicroDenomStringWithDecimals, decodeJsonFromBase64, encodeJsonToBase64, makeWasmMessage, @@ -154,10 +154,10 @@ export class PerformTokenSwapAction extends ActionBase<PerformTokenSwapData> { } // Convert amount to micro amount. - const amount = convertDenomToMicroDenomStringWithDecimals( + const amount = HugeDecimal.fromHumanReadable( selfParty.amount, selfParty.decimals - ) + ).toString() return selfParty.type === 'cw20' ? makeWasmMessage({ diff --git a/packages/stateful/actions/core/actions/token_swap/stateful/ChooseExistingTokenSwap.tsx b/packages/stateful/actions/core/actions/token_swap/stateful/ChooseExistingTokenSwap.tsx index 5dc19f560..ea47a8617 100644 --- a/packages/stateful/actions/core/actions/token_swap/stateful/ChooseExistingTokenSwap.tsx +++ b/packages/stateful/actions/core/actions/token_swap/stateful/ChooseExistingTokenSwap.tsx @@ -3,17 +3,14 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useRecoilCallback } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { CwTokenSwapSelectors, genericTokenSelector, } from '@dao-dao/state/recoil' import { useActionOptions } from '@dao-dao/stateless' import { ActionComponent, TokenType } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - objectMatchesStructure, - processError, -} from '@dao-dao/utils' +import { objectMatchesStructure, processError } from '@dao-dao/utils' import { ChooseExistingTokenSwap as StatelessChooseExistingTokenSwap } from '../stateless/ChooseExistingTokenSwap' @@ -151,12 +148,11 @@ export const ChooseExistingTokenSwap: ActionComponent< setValue(props.fieldNamePrefix + 'selfParty', { type: 'cw20' in selfParty.promise ? 'cw20' : 'native', denomOrAddress: selfPartyTokenInfo.denomOrAddress, - amount: convertMicroDenomToDenomWithDecimals( + amount: HugeDecimal.from( 'cw20' in selfParty.promise ? selfParty.promise.cw20.amount - : selfParty.promise.native.amount, - selfPartyTokenInfo.decimals - ), + : selfParty.promise.native.amount + ).toHumanReadableString(selfPartyTokenInfo.decimals), decimals: selfPartyTokenInfo.decimals, }) diff --git a/packages/stateful/actions/core/actions/token_swap/stateful/InstantiateTokenSwap.tsx b/packages/stateful/actions/core/actions/token_swap/stateful/InstantiateTokenSwap.tsx index 0c8791754..79238457f 100644 --- a/packages/stateful/actions/core/actions/token_swap/stateful/InstantiateTokenSwap.tsx +++ b/packages/stateful/actions/core/actions/token_swap/stateful/InstantiateTokenSwap.tsx @@ -4,6 +4,7 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenBalancesSelector } from '@dao-dao/state' import { DaoDaoCoreSelectors } from '@dao-dao/state/recoil' import { Loader, useActionOptions, useCachedLoading } from '@dao-dao/stateless' @@ -15,8 +16,6 @@ import { } from '@dao-dao/types' import { InstantiateMsg } from '@dao-dao/types/contracts/CwTokenSwap' import { - convertDenomToMicroDenomStringWithDecimals, - convertDenomToMicroDenomWithDecimals, getNativeTokenForChainId, instantiateSmartContract, isValidBech32Address, @@ -78,19 +77,19 @@ export const InstantiateTokenSwap: ActionComponent< ? { cw20: { contract_addr: selfParty.denomOrAddress, - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( selfParty.amount, selfParty.decimals - ), + ).toString(), }, } : { native: { denom: selfParty.denomOrAddress, - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( selfParty.amount, selfParty.decimals - ), + ).toString(), }, }, }, @@ -101,7 +100,7 @@ export const InstantiateTokenSwap: ActionComponent< ? { cw20: { contract_addr: counterparty.denomOrAddress, - amount: convertDenomToMicroDenomWithDecimals( + amount: HugeDecimal.fromHumanReadable( counterparty.amount, counterparty.decimals ).toString(), @@ -110,7 +109,7 @@ export const InstantiateTokenSwap: ActionComponent< : { native: { denom: counterparty.denomOrAddress, - amount: convertDenomToMicroDenomWithDecimals( + amount: HugeDecimal.fromHumanReadable( counterparty.amount, counterparty.decimals ).toString(), @@ -208,7 +207,7 @@ const InnerInstantiateTokenSwap: ActionComponent< denomOrAddress: selfPartyDefaultCw20 ? selfPartyDefaultCw20.token.denomOrAddress : nativeToken.denomOrAddress, - amount: 0, + amount: '0', decimals: selfPartyDefaultCw20 ? selfPartyDefaultCw20.token.decimals : nativeToken.decimals, @@ -219,7 +218,7 @@ const InnerInstantiateTokenSwap: ActionComponent< address: '', type: 'native', denomOrAddress: nativeToken.denomOrAddress, - amount: 0, + amount: '0', decimals: nativeToken.decimals, }, }) diff --git a/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.stories.tsx b/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.stories.tsx index 6909969c7..fce8cf172 100644 --- a/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.stories.tsx +++ b/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.stories.tsx @@ -24,13 +24,13 @@ export default { selfParty: { type: TokenType.Cw20, denomOrAddress: 'cw20_1', - amount: 0, + amount: '0', decimals: 6, }, counterparty: { ...getNativeTokenForChainId(CHAIN_ID), address: '', - amount: 0, + amount: '0', }, }), makeDaoProvidersDecorator(makeDaoInfo()), diff --git a/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.tsx b/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.tsx index 4eebde82b..47b650b4b 100644 --- a/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.tsx +++ b/packages/stateful/actions/core/actions/token_swap/stateless/InstantiateTokenSwap.tsx @@ -1,6 +1,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, InputErrorMessage, @@ -10,7 +11,6 @@ import { } from '@dao-dao/stateless' import { ActionComponent } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, isValidBech32Address, makeValidateAddress, validateRequired, @@ -37,7 +37,7 @@ export const InstantiateTokenSwap: ActionComponent< const { chain: { bech32_prefix: bech32Prefix }, } = useActionOptions() - const { register, watch, setValue, trigger } = useFormContext() + const { register, watch, setValue, getValues, trigger } = useFormContext() const selfParty = watch(fieldNamePrefix + 'selfParty') const counterparty = watch(fieldNamePrefix + 'counterparty') @@ -46,11 +46,7 @@ export const InstantiateTokenSwap: ActionComponent< ({ token }) => selfParty.denomOrAddress === token.denomOrAddress ) const selfDecimals = selfToken?.token.decimals ?? 0 - const selfMin = convertMicroDenomToDenomWithDecimals(1, selfDecimals) - const selfMax = convertMicroDenomToDenomWithDecimals( - selfToken?.balance ?? 0, - selfDecimals - ) + const selfMax = HugeDecimal.from(selfToken?.balance ?? 0) const selfSymbol = selfToken?.token.symbol ?? t('info.tokens') const counterpartyToken = counterpartyTokenBalances.loading @@ -59,14 +55,7 @@ export const InstantiateTokenSwap: ActionComponent< ({ token }) => counterparty.denomOrAddress === token.denomOrAddress ) const counterpartyDecimals = counterpartyToken?.token.decimals ?? 0 - const counterpartyMin = convertMicroDenomToDenomWithDecimals( - 1, - counterpartyDecimals - ) - const counterpartyMax = convertMicroDenomToDenomWithDecimals( - counterpartyToken?.balance ?? 0, - counterpartyDecimals - ) + const counterpartyMax = HugeDecimal.from(counterpartyToken?.balance ?? 0) const counterpartySymbol = counterpartyToken?.token.symbol ?? t('info.tokens') const counterpartyAddressValid = @@ -113,11 +102,12 @@ export const InstantiateTokenSwap: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: fieldNamePrefix + 'counterparty.amount', error: errors?.counterparty?.amount, - min: counterpartyMin, - step: counterpartyMin, + min: HugeDecimal.one.toHumanReadableNumber(counterpartyDecimals), + step: HugeDecimal.one.toHumanReadableNumber(counterpartyDecimals), }} disabled={!counterpartyAddressValid} onSelectToken={({ type, denomOrAddress, decimals }) => { @@ -147,11 +137,13 @@ export const InstantiateTokenSwap: ActionComponent< /> {/* Warn if counterparty does not have the requested amount. */} - {counterparty.amount > counterpartyMax && ( + {counterpartyMax + .toHumanReadable(counterpartyDecimals) + .lt(counterparty.amount) && ( <p className="caption-text text-text-interactive-warning-body"> {t('error.counterpartyBalanceInsufficient', { - amount: counterpartyMax.toLocaleString(undefined, { - maximumFractionDigits: counterpartyDecimals, + amount: counterpartyMax.toInternationalizedHumanReadableString({ + decimals: counterpartyDecimals, }), tokenSymbol: counterpartySymbol, })} @@ -175,18 +167,19 @@ export const InstantiateTokenSwap: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: fieldNamePrefix + 'selfParty.amount', error: errors?.selfParty?.amount, - min: selfMin, - step: selfMin, - max: selfMax, + min: HugeDecimal.one.toHumanReadableNumber(selfDecimals), + step: HugeDecimal.one.toHumanReadableNumber(selfDecimals), + max: selfMax.toHumanReadableString(selfDecimals), validations: [ (value) => - value <= selfMax || + selfMax.toHumanReadable(selfDecimals).gte(value) || t('error.treasuryInsufficient', { - amount: selfMax.toLocaleString(undefined, { - maximumFractionDigits: selfDecimals, + amount: selfMax.toInternationalizedHumanReadableString({ + decimals: selfDecimals, }), tokenSymbol: selfSymbol, }), diff --git a/packages/stateful/actions/core/actions/token_swap/types.ts b/packages/stateful/actions/core/actions/token_swap/types.ts index 2e4610bb0..483d9c446 100644 --- a/packages/stateful/actions/core/actions/token_swap/types.ts +++ b/packages/stateful/actions/core/actions/token_swap/types.ts @@ -12,7 +12,7 @@ export interface Counterparty { address: string type: TokenType denomOrAddress: string - amount: number + amount: string decimals: number } diff --git a/packages/stateful/actions/providers/dao.tsx b/packages/stateful/actions/providers/dao.tsx index 9b1c4dd36..5ff7d61fb 100644 --- a/packages/stateful/actions/providers/dao.tsx +++ b/packages/stateful/actions/providers/dao.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { processMessage } from '@dao-dao/state' import { ActionsContext, - useDaoContext, + useDao, useSupportedChainContext, } from '@dao-dao/stateless' import { @@ -33,7 +33,7 @@ import { export const DaoActionsProvider = ({ children }: ActionsProviderProps) => { const { t } = useTranslation() const chainContext = useSupportedChainContext() - const { dao } = useDaoContext() + const dao = useDao() const queryClient = useQueryClient() // Get the action category makers for a DAO from its various sources: diff --git a/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts b/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts index e6a73425c..c390430bb 100644 --- a/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts +++ b/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.secret.ts @@ -176,7 +176,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -200,7 +200,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -308,7 +308,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< proposalId: number }): FetchQueryOptions<ProposalResponse> { return secretDaoProposalMultipleQueries.proposal({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { proposalId, @@ -334,7 +334,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< // If no voter nor permit, return query in loading state. const permit = voter && this.dao.getExistingPermit(voter) return secretDaoProposalMultipleQueries.getVote({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, // Force type-cast since the query won't be enabled until this is set. // This allows us to pass an undefined `voter` argument in order to @@ -373,21 +373,21 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< getProposalCountQuery(): FetchQueryOptions<number> { return secretDaoProposalMultipleQueries.proposalCount({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } getDaoAddressQuery(): FetchQueryOptions<string> { return secretDaoProposalMultipleQueries.dao({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } getConfigQuery(): FetchQueryOptions<Config> { return secretDaoProposalMultipleQueries.config({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } @@ -398,7 +398,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< 'secretMultipleChoiceProposalModule', 'depositInfo', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -407,7 +407,7 @@ export class SecretMultipleChoiceProposalModule extends ProposalModuleBase< const { deposit_info: depositInfo } = await this.queryClient.fetchQuery( secretDaoPreProposeMultipleQueries.config({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.prePropose.address, }) ) diff --git a/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.ts b/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.ts index 22a1745d0..6322cebb6 100644 --- a/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.ts +++ b/packages/stateful/clients/proposal-module/MultipleChoiceProposalModule.ts @@ -234,7 +234,7 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -258,7 +258,7 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -345,7 +345,7 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< proposalId: number }): FetchQueryOptions<ProposalResponse> { return daoProposalMultipleQueries.proposal(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { proposalId, @@ -367,7 +367,7 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< voter?: string }): FetchQueryOptions<VoteResponse> { return daoProposalMultipleQueries.getVote(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { proposalId, @@ -394,21 +394,21 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< getProposalCountQuery(): FetchQueryOptions<number> { return daoProposalMultipleQueries.proposalCount(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.info.address, }) } getDaoAddressQuery(): FetchQueryOptions<string> { return daoProposalMultipleQueries.dao(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } getConfigQuery(): FetchQueryOptions<Config> { return daoProposalMultipleQueries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } @@ -419,7 +419,7 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< 'multipleChoiceProposalModule', 'depositInfo', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -428,7 +428,7 @@ export class MultipleChoiceProposalModule extends ProposalModuleBase< const { deposit_info: depositInfo } = await this.queryClient.fetchQuery( daoPreProposeMultipleQueries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.prePropose.address, }) ) diff --git a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts index 3377c551c..f76f7796f 100644 --- a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts +++ b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.secret.ts @@ -202,7 +202,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< isPreProposeApprovalProposal ? Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.prePropose.address, 'id' @@ -210,7 +210,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< ) : Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -234,7 +234,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -339,7 +339,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< proposalId: number }): FetchQueryOptions<ProposalResponse> { return secretDaoProposalSingleQueries.proposal({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { proposalId, @@ -362,7 +362,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< }): FetchQueryOptions<VoteResponse> { const permit = voter && this.dao.getExistingPermit(voter) return secretDaoProposalSingleQueries.getVote({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.info.address, // Force type-cast since the query won't be enabled until this is set. // This allows us to pass an undefined `voter` argument in order to @@ -401,14 +401,14 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< getProposalCountQuery(): FetchQueryOptions<number> { return secretDaoProposalSingleQueries.proposalCount({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } getConfigQuery(): FetchQueryOptions<Config> { return secretDaoProposalSingleQueries.config({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } @@ -419,7 +419,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< 'secretSingleChoiceProposalModule', 'depositInfo', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -428,7 +428,7 @@ export class SecretSingleChoiceProposalModule extends ProposalModuleBase< const { deposit_info: depositInfo } = await this.queryClient.fetchQuery( secretDaoPreProposeSingleQueries.config({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.prePropose.address, }) ) diff --git a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts index 382e3e258..326bedc74 100644 --- a/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts +++ b/packages/stateful/clients/proposal-module/SingleChoiceProposalModule.ts @@ -212,7 +212,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -249,7 +249,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< isPreProposeApprovalProposal ? Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.prePropose.address, 'id' @@ -257,7 +257,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< ) : Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -281,7 +281,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< proposalNumber = Number( findWasmAttributeValue( - this.dao.chainId, + this.chainId, events, this.address, 'proposal_id' @@ -388,7 +388,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< proposalId: number }): FetchQueryOptions<ProposalResponse> { return daoProposalSingleV2Queries.proposal(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { proposalId, @@ -415,7 +415,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< : daoProposalSingleV2Queries.getVote return query(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { proposalId, @@ -447,14 +447,14 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< : daoProposalSingleV2Queries.proposalCount return query(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } getConfigQuery(): FetchQueryOptions<Config> { return daoProposalSingleV2Queries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) } @@ -465,7 +465,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< 'singleChoiceProposalModule', 'depositInfo', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -474,7 +474,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< const { deposit_info: depositInfo } = await this.queryClient.fetchQuery( daoPreProposeSingleQueries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.prePropose.address, }) ) @@ -488,7 +488,7 @@ export class SingleChoiceProposalModule extends ProposalModuleBase< const { deposit_info: depositInfo } = await this.queryClient.fetchQuery( cwProposalSingleV1Queries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) diff --git a/packages/stateful/clients/proposal-module/base.ts b/packages/stateful/clients/proposal-module/base.ts index 022b564f7..2a49c0a7c 100644 --- a/packages/stateful/clients/proposal-module/base.ts +++ b/packages/stateful/clients/proposal-module/base.ts @@ -42,6 +42,13 @@ export abstract class ProposalModuleBase< public readonly info: ProposalModuleInfo ) {} + /** + * Chain ID of the proposal module. + */ + get chainId(): string { + return this.dao.chainId + } + /** * Contract address. */ diff --git a/packages/stateful/clients/voting-module/Cw20StakedVotingModule.ts b/packages/stateful/clients/voting-module/Cw20StakedVotingModule.ts index de7e6bf20..4afda1002 100644 --- a/packages/stateful/clients/voting-module/Cw20StakedVotingModule.ts +++ b/packages/stateful/clients/voting-module/Cw20StakedVotingModule.ts @@ -160,7 +160,7 @@ export class Cw20StakedVotingModule extends VotingModuleBase<CwDao> { } return daoVotingCw20StakedQueries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -173,7 +173,7 @@ export class Cw20StakedVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingCw20StakedQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -187,21 +187,21 @@ export class Cw20StakedVotingModule extends VotingModuleBase<CwDao> { 'cw20StakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], queryFn: async () => { const governanceTokenAddress = await this.queryClient.fetchQuery( daoVotingCw20StakedQueries.tokenContract(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const token = await this.queryClient.fetchQuery( tokenQueries.info(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Cw20, denomOrAddress: governanceTokenAddress, }) @@ -215,7 +215,7 @@ export class Cw20StakedVotingModule extends VotingModuleBase<CwDao> { async getHookCaller(): Promise<string> { return this.queryClient.fetchQuery( daoVotingCw20StakedQueries.stakingContract(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) @@ -225,7 +225,7 @@ export class Cw20StakedVotingModule extends VotingModuleBase<CwDao> { return ( await this.queryClient.fetchQuery( cw20StakeQueries.getHooks(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: await this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/Cw4VotingModule.secret.ts b/packages/stateful/clients/voting-module/Cw4VotingModule.secret.ts index 6196e013a..f994a2e72 100644 --- a/packages/stateful/clients/voting-module/Cw4VotingModule.secret.ts +++ b/packages/stateful/clients/voting-module/Cw4VotingModule.secret.ts @@ -94,7 +94,7 @@ export class SecretCw4VotingModule extends VotingModuleBase<SecretCwDao> { } return secretDaoVotingCw4Queries.votingPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { auth: { permit }, @@ -130,7 +130,7 @@ export class SecretCw4VotingModule extends VotingModuleBase<SecretCwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return secretDaoVotingCw4Queries.totalPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -142,7 +142,7 @@ export class SecretCw4VotingModule extends VotingModuleBase<SecretCwDao> { return ( await this.queryClient.fetchQuery( secretDaoVotingCw4Queries.groupContract({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) @@ -153,7 +153,7 @@ export class SecretCw4VotingModule extends VotingModuleBase<SecretCwDao> { return ( await this.queryClient.fetchQuery( secretCw4GroupQueries.hooks({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: await this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/Cw4VotingModule.ts b/packages/stateful/clients/voting-module/Cw4VotingModule.ts index f4ae2aa92..0687162cc 100644 --- a/packages/stateful/clients/voting-module/Cw4VotingModule.ts +++ b/packages/stateful/clients/voting-module/Cw4VotingModule.ts @@ -80,7 +80,7 @@ export class Cw4VotingModule extends VotingModuleBase<CwDao> { } return daoVotingCw4Queries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -93,7 +93,7 @@ export class Cw4VotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingCw4Queries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -104,7 +104,7 @@ export class Cw4VotingModule extends VotingModuleBase<CwDao> { async getHookCaller(): Promise<string> { return this.queryClient.fetchQuery( daoVotingCw4Queries.groupContract(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) @@ -114,7 +114,7 @@ export class Cw4VotingModule extends VotingModuleBase<CwDao> { return ( await this.queryClient.fetchQuery( cw4GroupQueries.hooks(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: await this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/Cw721StakedVotingModule.ts b/packages/stateful/clients/voting-module/Cw721StakedVotingModule.ts index 0321e1c3f..658c214e6 100644 --- a/packages/stateful/clients/voting-module/Cw721StakedVotingModule.ts +++ b/packages/stateful/clients/voting-module/Cw721StakedVotingModule.ts @@ -151,7 +151,7 @@ export class Cw721StakedVotingModule extends VotingModuleBase<CwDao> { } return daoVotingCw721StakedQueries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -164,7 +164,7 @@ export class Cw721StakedVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingCw721StakedQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -178,7 +178,7 @@ export class Cw721StakedVotingModule extends VotingModuleBase<CwDao> { 'cw721StakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -186,26 +186,26 @@ export class Cw721StakedVotingModule extends VotingModuleBase<CwDao> { const { nft_address: collectionAddress } = await this.queryClient.fetchQuery( daoVotingCw721StakedQueries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const contractInfo = await this.queryClient.fetchQuery( cw721BaseQueries.contractInfo({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: collectionAddress, }) ) return { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Cw721, denomOrAddress: collectionAddress, symbol: contractInfo.symbol, decimals: 0, source: { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Cw721, denomOrAddress: collectionAddress, }, @@ -222,7 +222,7 @@ export class Cw721StakedVotingModule extends VotingModuleBase<CwDao> { return ( await this.queryClient.fetchQuery( daoVotingCw721StakedQueries.hooks(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/FallbackVotingModule.ts b/packages/stateful/clients/voting-module/FallbackVotingModule.ts index 3752b1bea..dab29f0e2 100644 --- a/packages/stateful/clients/voting-module/FallbackVotingModule.ts +++ b/packages/stateful/clients/voting-module/FallbackVotingModule.ts @@ -34,7 +34,7 @@ export class FallbackVotingModule extends VotingModuleBase<CwDao> { } return daoDaoCoreQueries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.dao.coreAddress, args: { address, @@ -47,7 +47,7 @@ export class FallbackVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoDaoCoreQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.dao.coreAddress, args: { height, diff --git a/packages/stateful/clients/voting-module/NativeStakedVotingModule.ts b/packages/stateful/clients/voting-module/NativeStakedVotingModule.ts index ea2281458..064ad9ae4 100644 --- a/packages/stateful/clients/voting-module/NativeStakedVotingModule.ts +++ b/packages/stateful/clients/voting-module/NativeStakedVotingModule.ts @@ -41,7 +41,7 @@ export class NativeStakedVotingModule extends VotingModuleBase<CwDao> { } return daoVotingNativeStakedQueries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -54,7 +54,7 @@ export class NativeStakedVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingNativeStakedQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -68,21 +68,21 @@ export class NativeStakedVotingModule extends VotingModuleBase<CwDao> { 'nativeStakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], queryFn: async () => { const { denom } = await this.queryClient.fetchQuery( daoVotingNativeStakedQueries.getConfig(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const token = await this.queryClient.fetchQuery( tokenQueries.info(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Native, denomOrAddress: denom, }) diff --git a/packages/stateful/clients/voting-module/NeutronVotingRegistryVotingModule.ts b/packages/stateful/clients/voting-module/NeutronVotingRegistryVotingModule.ts index 525fa819b..7a144e003 100644 --- a/packages/stateful/clients/voting-module/NeutronVotingRegistryVotingModule.ts +++ b/packages/stateful/clients/voting-module/NeutronVotingRegistryVotingModule.ts @@ -36,7 +36,7 @@ export class NeutronVotingRegistryVotingModule extends VotingModuleBase<CwDao> { } return neutronVotingRegistryQueries.votingPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -49,7 +49,7 @@ export class NeutronVotingRegistryVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return neutronVotingRegistryQueries.totalPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, diff --git a/packages/stateful/clients/voting-module/OnftStakedVotingModule.ts b/packages/stateful/clients/voting-module/OnftStakedVotingModule.ts index 59bf89d85..b9bfdd5ec 100644 --- a/packages/stateful/clients/voting-module/OnftStakedVotingModule.ts +++ b/packages/stateful/clients/voting-module/OnftStakedVotingModule.ts @@ -79,7 +79,7 @@ export class OnftStakedVotingModule extends VotingModuleBase<CwDao> { } return daoVotingOnftStakedQueries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -92,7 +92,7 @@ export class OnftStakedVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingOnftStakedQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -106,34 +106,34 @@ export class OnftStakedVotingModule extends VotingModuleBase<CwDao> { 'onftStakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], queryFn: async () => { const { onft_collection_id } = await this.queryClient.fetchQuery( daoVotingOnftStakedQueries.config(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const { symbol, previewUri } = await this.queryClient.fetchQuery( omniflixQueries.onftCollectionInfo({ - chainId: this.dao.chainId, + chainId: this.chainId, id: onft_collection_id, }) ) return { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Onft, denomOrAddress: onft_collection_id, symbol, decimals: 0, imageUrl: previewUri, source: { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Onft, denomOrAddress: onft_collection_id, }, @@ -150,7 +150,7 @@ export class OnftStakedVotingModule extends VotingModuleBase<CwDao> { return ( await this.queryClient.fetchQuery( daoVotingOnftStakedQueries.hooks(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/SgCommunityNftVotingModule.ts b/packages/stateful/clients/voting-module/SgCommunityNftVotingModule.ts index 682ee3588..21397e568 100644 --- a/packages/stateful/clients/voting-module/SgCommunityNftVotingModule.ts +++ b/packages/stateful/clients/voting-module/SgCommunityNftVotingModule.ts @@ -38,7 +38,7 @@ export class SgCommunityNftVotingModule extends VotingModuleBase<CwDao> { return daoVotingSgCommunityNftQueries.votingPowerAtHeight( this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -52,7 +52,7 @@ export class SgCommunityNftVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingSgCommunityNftQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -68,7 +68,7 @@ export class SgCommunityNftVotingModule extends VotingModuleBase<CwDao> { return ( await this.queryClient.fetchQuery( daoVotingSgCommunityNftQueries.hooks(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/Snip20StakedVotingModule.secret.ts b/packages/stateful/clients/voting-module/Snip20StakedVotingModule.secret.ts index 660ca8411..cf83d37ab 100644 --- a/packages/stateful/clients/voting-module/Snip20StakedVotingModule.secret.ts +++ b/packages/stateful/clients/voting-module/Snip20StakedVotingModule.secret.ts @@ -174,7 +174,7 @@ export class SecretSnip20StakedVotingModule extends VotingModuleBase<SecretCwDao } return secretDaoVotingSnip20StakedQueries.votingPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { auth: { permit }, @@ -210,7 +210,7 @@ export class SecretSnip20StakedVotingModule extends VotingModuleBase<SecretCwDao height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return secretDaoVotingSnip20StakedQueries.totalPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -224,7 +224,7 @@ export class SecretSnip20StakedVotingModule extends VotingModuleBase<SecretCwDao 'snip20StakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -232,14 +232,14 @@ export class SecretSnip20StakedVotingModule extends VotingModuleBase<SecretCwDao const { addr: governanceTokenAddress } = await this.queryClient.fetchQuery( secretDaoVotingSnip20StakedQueries.tokenContract({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const token = await this.queryClient.fetchQuery( tokenQueries.info(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Cw20, denomOrAddress: governanceTokenAddress, }) @@ -254,7 +254,7 @@ export class SecretSnip20StakedVotingModule extends VotingModuleBase<SecretCwDao return ( await this.queryClient.fetchQuery( secretDaoVotingSnip20StakedQueries.stakingContract({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) @@ -265,7 +265,7 @@ export class SecretSnip20StakedVotingModule extends VotingModuleBase<SecretCwDao return ( await this.queryClient.fetchQuery( snip20StakeQueries.getHooks({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: await this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/Snip721StakedVotingModule.secret.ts b/packages/stateful/clients/voting-module/Snip721StakedVotingModule.secret.ts index cd7721bc1..1baac459a 100644 --- a/packages/stateful/clients/voting-module/Snip721StakedVotingModule.secret.ts +++ b/packages/stateful/clients/voting-module/Snip721StakedVotingModule.secret.ts @@ -88,7 +88,7 @@ export class SecretSnip721StakedVotingModule extends VotingModuleBase<SecretCwDa } return secretDaoVotingSnip721StakedQueries.votingPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { auth: { permit }, @@ -124,7 +124,7 @@ export class SecretSnip721StakedVotingModule extends VotingModuleBase<SecretCwDa height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return secretDaoVotingSnip721StakedQueries.totalPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -138,7 +138,7 @@ export class SecretSnip721StakedVotingModule extends VotingModuleBase<SecretCwDa 'snip721StakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], @@ -146,26 +146,26 @@ export class SecretSnip721StakedVotingModule extends VotingModuleBase<SecretCwDa const { nft_address: collectionAddress } = await this.queryClient.fetchQuery( secretDaoVotingSnip721StakedQueries.config({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const contractInfo = await this.queryClient.fetchQuery( cw721BaseQueries.contractInfo({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: collectionAddress, }) ) return { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Cw721, denomOrAddress: collectionAddress, symbol: contractInfo.symbol, decimals: 0, source: { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Cw721, denomOrAddress: collectionAddress, }, @@ -182,7 +182,7 @@ export class SecretSnip721StakedVotingModule extends VotingModuleBase<SecretCwDa return ( await this.queryClient.fetchQuery( secretDaoVotingSnip721StakedQueries.hooks({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/TokenStakedVotingModule.secret.ts b/packages/stateful/clients/voting-module/TokenStakedVotingModule.secret.ts index 17d3d8c9d..3de76a1fe 100644 --- a/packages/stateful/clients/voting-module/TokenStakedVotingModule.secret.ts +++ b/packages/stateful/clients/voting-module/TokenStakedVotingModule.secret.ts @@ -87,7 +87,7 @@ export class SecretTokenStakedVotingModule extends VotingModuleBase<SecretCwDao> } return secretDaoVotingTokenStakedQueries.votingPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { auth: { permit }, @@ -123,7 +123,7 @@ export class SecretTokenStakedVotingModule extends VotingModuleBase<SecretCwDao> height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return secretDaoVotingTokenStakedQueries.totalPowerAtHeight({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -137,21 +137,21 @@ export class SecretTokenStakedVotingModule extends VotingModuleBase<SecretCwDao> 'secretTokenStakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], queryFn: async () => { const { denom } = await this.queryClient.fetchQuery( secretDaoVotingTokenStakedQueries.denom({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const token = await this.queryClient.fetchQuery( tokenQueries.info(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Native, denomOrAddress: denom, }) @@ -170,7 +170,7 @@ export class SecretTokenStakedVotingModule extends VotingModuleBase<SecretCwDao> return ( await this.queryClient.fetchQuery( secretDaoVotingTokenStakedQueries.getHooks({ - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/TokenStakedVotingModule.ts b/packages/stateful/clients/voting-module/TokenStakedVotingModule.ts index c03d839b6..d5d59ec80 100644 --- a/packages/stateful/clients/voting-module/TokenStakedVotingModule.ts +++ b/packages/stateful/clients/voting-module/TokenStakedVotingModule.ts @@ -144,7 +144,7 @@ export class TokenStakedVotingModule extends VotingModuleBase<CwDao> { } return daoVotingTokenStakedQueries.votingPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { address, @@ -157,7 +157,7 @@ export class TokenStakedVotingModule extends VotingModuleBase<CwDao> { height?: number ): FetchQueryOptions<TotalPowerAtHeightResponse> { return daoVotingTokenStakedQueries.totalPowerAtHeight(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, args: { height, @@ -171,21 +171,21 @@ export class TokenStakedVotingModule extends VotingModuleBase<CwDao> { 'tokenStakedVotingModule', 'governanceToken', { - chainId: this.dao.chainId, + chainId: this.chainId, address: this.address, }, ], queryFn: async () => { const { denom } = await this.queryClient.fetchQuery( daoVotingTokenStakedQueries.denom(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.address, }) ) const token = await this.queryClient.fetchQuery( tokenQueries.info(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, type: TokenType.Native, denomOrAddress: denom, }) @@ -204,7 +204,7 @@ export class TokenStakedVotingModule extends VotingModuleBase<CwDao> { return ( await this.queryClient.fetchQuery( daoVotingTokenStakedQueries.getHooks(this.queryClient, { - chainId: this.dao.chainId, + chainId: this.chainId, contractAddress: this.getHookCaller(), }) ) diff --git a/packages/stateful/clients/voting-module/base.ts b/packages/stateful/clients/voting-module/base.ts index c0328fdc1..82fb114fc 100644 --- a/packages/stateful/clients/voting-module/base.ts +++ b/packages/stateful/clients/voting-module/base.ts @@ -42,6 +42,13 @@ export abstract class VotingModuleBase<Dao extends IDaoBase = IDaoBase> return this.info.contract } + /** + * Chain ID of the voting module. + */ + get chainId(): string { + return this.dao.chainId + } + /** * Query options to fetch the voting power for a given address. Optionally * specify a block height. If undefined, the latest block height will be used. diff --git a/packages/stateful/command/contexts/generic/dao.tsx b/packages/stateful/command/contexts/generic/dao.tsx index 292b68b54..2ac4e3897 100644 --- a/packages/stateful/command/contexts/generic/dao.tsx +++ b/packages/stateful/command/contexts/generic/dao.tsx @@ -12,7 +12,7 @@ import { useRecoilState } from 'recoil' import useDeepCompareEffect from 'use-deep-compare-effect' import { navigatingToHrefAtom } from '@dao-dao/state' -import { useDaoInfoContext, useDaoNavHelpers } from '@dao-dao/stateless' +import { useDao, useDaoNavHelpers } from '@dao-dao/stateless' import { AccountType, ContractVersion, Feature } from '@dao-dao/types' import { CommandModalContextMaker, @@ -48,7 +48,9 @@ export const makeGenericDaoContext: CommandModalContextMaker<{ const useSections = () => { const { t } = useTranslation() const { getDaoPath, getDaoProposalPath, router } = useDaoNavHelpers() - const { accounts, supportedFeatures } = useDaoInfoContext() + const { + info: { accounts, supportedFeatures }, + } = useDao() const loadingTabs = useLoadingTabs() const { isFollowing, setFollowing, setUnfollowing, updatingFollowing } = diff --git a/packages/stateful/components/ProposalLine.tsx b/packages/stateful/components/ProposalLine.tsx index dc553b56e..9a12a0e46 100644 --- a/packages/stateful/components/ProposalLine.tsx +++ b/packages/stateful/components/ProposalLine.tsx @@ -1,10 +1,6 @@ import { useTranslation } from 'react-i18next' -import { - LineLoader, - StatusCard, - useDaoContextIfAvailable, -} from '@dao-dao/stateless' +import { LineLoader, StatusCard, useDaoIfAvailable } from '@dao-dao/stateless' import { StatefulProposalLineProps } from '@dao-dao/types' import { @@ -20,7 +16,7 @@ export const ProposalLine = ({ coreAddress, ...props }: StatefulProposalLineProps) => { - const existingDao = useDaoContextIfAvailable()?.dao + const existingDao = useDaoIfAvailable() const content = ( <ProposalModuleAdapterProvider proposalId={props.proposalId}> diff --git a/packages/stateful/components/ProposalList.tsx b/packages/stateful/components/ProposalList.tsx index 319c71d3f..bf5414d3d 100644 --- a/packages/stateful/components/ProposalList.tsx +++ b/packages/stateful/components/ProposalList.tsx @@ -11,7 +11,7 @@ import { ProposalList as StatelessProposalList, useAppContext, useCachedLoadingWithError, - useDaoContext, + useDao, useDaoNavHelpers, useLoadingPromise, useUpdatingRef, @@ -66,7 +66,7 @@ export const ProposalList = ({ ...props }: StatefulProposalListProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { mode } = useAppContext() const { isMember = false } = useMembership() @@ -294,7 +294,7 @@ export const ProposalList = ({ newProposalInfos = newProposalInfos.filter( (info) => !( - info.proposalModule.dao.chainId === ChainId.NeutronMainnet && + info.proposalModule.chainId === ChainId.NeutronMainnet && info.proposalModule.dao.coreAddress === NEUTRON_GOVERNANCE_DAO && (info.id === 'A47' || info.id === 'A48') diff --git a/packages/stateful/components/SelfRelayExecuteModal.tsx b/packages/stateful/components/SelfRelayExecuteModal.tsx index 5770b0030..1edeb801d 100644 --- a/packages/stateful/components/SelfRelayExecuteModal.tsx +++ b/packages/stateful/components/SelfRelayExecuteModal.tsx @@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next' import { useRecoilCallback, waitForAll } from 'recoil' import { useDeepCompareMemoize } from 'use-deep-compare-effect' +import { HugeDecimal } from '@dao-dao/math' import { tokenQueries } from '@dao-dao/state/query' import { genericTokenBalanceSelector, @@ -60,7 +61,6 @@ import { SendAuthorization } from '@dao-dao/types/protobuf/codegen/cosmos/bank/v import { toTimestamp } from '@dao-dao/types/protobuf/codegen/helpers' import { CHAIN_GAS_MULTIPLIER, - convertMicroDenomToDenomWithDecimals, getChainForChainId, getDisplayNameForChainId, getFallbackImage, @@ -156,7 +156,7 @@ export const SelfRelayExecuteModal = ({ ) // Amount funded once funding is complete. const [fundedAmount, setFundedAmount] = useState< - Record<string, number | undefined> + Record<string, HugeDecimal | undefined> >({}) const [executeTx, setExecuteTx] = useState<Pick<IndexedTx, 'events' | 'height'>>() @@ -166,7 +166,7 @@ export const SelfRelayExecuteModal = ({ }>() // Amount refunded once refunding is complete. const [refundedAmount, setRefundedAmount] = useState< - Record<string, number | undefined> + Record<string, HugeDecimal | undefined> >({}) // If relay fails and user decides to refund and cancel, this will be set to @@ -457,30 +457,29 @@ export const SelfRelayExecuteModal = ({ const fundsNeeded = // Give a little extra to cover the authz tx fee. - getRelayerFundsRef.current(chainId) * 1.2 - - Number(currentBalance.amount) - - let msgs: EncodeObject[] = - fundsNeeded > 0 - ? // Send tokens to relayer wallet if needed. - [ - cwMsgToEncodeObject( - chainId, - { - bank: { - send: { - amount: coins( - BigInt(fundsNeeded).toString(), - relayer.feeToken.denomOrAddress - ), - to_address: relayer.relayerAddress, - }, + HugeDecimal.from(getRelayerFundsRef.current(chainId) * 1.2).minus( + currentBalance + ) + + let msgs: EncodeObject[] = fundsNeeded.isPositive() + ? // Send tokens to relayer wallet if needed. + [ + cwMsgToEncodeObject( + chainId, + { + bank: { + send: { + amount: fundsNeeded.toCoins( + relayer.feeToken.denomOrAddress + ), + to_address: relayer.relayerAddress, }, }, - relayer.wallet.address - ), - ] - : [] + }, + relayer.wallet.address + ), + ] + : [] // Add execute message if executing and has not already executed. if (withExecuteRelay && !executeTx && transaction.type === 'execute') { @@ -506,13 +505,11 @@ export const SelfRelayExecuteModal = ({ } // Get new balance of relayer wallet. - const newBalance = Number( - ( - await relayer.client.query.bank.balance( - relayer.relayerAddress, - relayer.feeToken.denomOrAddress - ) - ).amount + const newBalance = HugeDecimal.from( + await relayer.client.query.bank.balance( + relayer.relayerAddress, + relayer.feeToken.denomOrAddress + ) ) setFundedAmount((prev) => ({ ...prev, @@ -541,7 +538,7 @@ export const SelfRelayExecuteModal = ({ authorization: SendAuthorization.toProtoMsg( SendAuthorization.fromPartial({ spendLimit: coins( - BigInt(newBalance).toString(), + newBalance.toString(), relayer.feeToken.denomOrAddress ), }) @@ -1043,15 +1040,16 @@ export const SelfRelayExecuteModal = ({ // @ts-ignore client.gasPrice ) - const remainingTokensAfterFee = - Number(remainingTokens.amount) - Number(fee.amount[0].amount) + const remainingTokensAfterFee = HugeDecimal.from(remainingTokens).minus( + fee.amount[0] + ) // Send remaining tokens if there are more than enough to pay the fee. - if (remainingTokensAfterFee > 0) { + if (remainingTokensAfterFee.isPositive()) { await client.sign.sendTokens( relayerAddress, wallet.address, - coins(BigInt(remainingTokensAfterFee).toString(), feeDenom), + remainingTokensAfterFee.toCoins(feeDenom), fee ) @@ -1171,12 +1169,14 @@ export const SelfRelayExecuteModal = ({ const funds = !relayerFunds.loading && !relayerFunds.errored - ? Number(relayerFunds.data[index].amount) + ? HugeDecimal.from(relayerFunds.data[index]) : // Use the previously funded amount if the step is past. - fundedAmount[chain_id] ?? 0 - const empty = funds === 0 + fundedAmount[chain_id] ?? HugeDecimal.zero + const empty = funds.isZero() - const funded = funds >= getRelayerFundsRef.current(chain_id) + const funded = funds.gte( + getRelayerFundsRef.current(chain_id) + ) const isExecute = index === 0 // If this is the execute, we need to make sure all @@ -1207,11 +1207,12 @@ export const SelfRelayExecuteModal = ({ title={ walletCannotAfford && fundTokenWithBalance ? t('error.insufficientWalletBalance', { - amount: - convertMicroDenomToDenomWithDecimals( - fundTokenWithBalance.balance, - fundTokenWithBalance.token.decimals - ), + amount: HugeDecimal.from( + fundTokenWithBalance.balance + ).toInternationalizedHumanReadableString({ + decimals: + fundTokenWithBalance.token.decimals, + }), tokenSymbol: fundTokenWithBalance.token.symbol, }) @@ -1251,17 +1252,7 @@ export const SelfRelayExecuteModal = ({ ) : ( <div className="flex items-center justify-end gap-2"> <TokenAmountDisplay - amount={ - walletFunds.loading || walletFunds.errored - ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - funds, - walletFunds.data[index].token.decimals - ), - } - } + amount={funds} decimals={ fundTokenWithBalance?.token.decimals ?? 0 } @@ -1386,11 +1377,12 @@ export const SelfRelayExecuteModal = ({ const funds = !relayerFunds.loading && !relayerFunds.errored - ? Number(relayerFunds.data[index].amount) - : 0 - const empty = funds === 0 + ? HugeDecimal.from(relayerFunds.data[index]) + : HugeDecimal.zero + const empty = funds.isZero() - const refunded = refundedAmount[chain_id] ?? 0 + const refunded = + refundedAmount[chain_id] ?? HugeDecimal.zero return ( <Fragment key={chain_id}> @@ -1409,10 +1401,7 @@ export const SelfRelayExecuteModal = ({ <div className="flex items-center justify-end gap-2"> <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - empty ? refunded : funds, - feeToken.decimals - )} + amount={empty ? refunded : funds} decimals={feeToken.decimals} symbol={feeToken.symbol} /> diff --git a/packages/stateful/components/TokenAmountDisplay.tsx b/packages/stateful/components/TokenAmountDisplay.tsx index 911d6ccdd..a7417fc34 100644 --- a/packages/stateful/components/TokenAmountDisplay.tsx +++ b/packages/stateful/components/TokenAmountDisplay.tsx @@ -1,11 +1,18 @@ -import { genericTokenSelector } from '@dao-dao/state/recoil' +import { useQueryClient } from '@tanstack/react-query' + +import { HugeDecimal } from '@dao-dao/math' +import { tokenQueries } from '@dao-dao/state/query' import { TokenAmountDisplay as StatelessTokenAmountDisplay, - useCachedLoading, useChain, } from '@dao-dao/stateless' -import { StatefulTokenAmountDisplayProps, TokenType } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' +import { + GenericToken, + StatefulTokenAmountDisplayProps, + TokenType, +} from '@dao-dao/types' + +import { useQueryLoadingData } from '../hooks' /** * Automatically show a native coin token amount. @@ -15,9 +22,13 @@ export const TokenAmountDisplay = ({ ...props }: StatefulTokenAmountDisplayProps) => { const { chain_id: chainId } = useChain() + const queryClient = useQueryClient() - const loadingGenericToken = useCachedLoading( - genericTokenSelector({ + const loadingGenericToken = useQueryLoadingData< + GenericToken, + GenericToken | undefined + >( + tokenQueries.info(queryClient, { type: TokenType.Native, denomOrAddress: denom, chainId, @@ -32,10 +43,7 @@ export const TokenAmountDisplay = ({ ? { loading: true } : { loading: false, - data: convertMicroDenomToDenomWithDecimals( - amount, - loadingGenericToken.data.decimals - ), + data: HugeDecimal.from(amount), } } decimals={ diff --git a/packages/stateful/components/dao/CreateDaoForm.tsx b/packages/stateful/components/dao/CreateDaoForm.tsx index f2a3be945..09612fdc3 100644 --- a/packages/stateful/components/dao/CreateDaoForm.tsx +++ b/packages/stateful/components/dao/CreateDaoForm.tsx @@ -14,6 +14,7 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilState, useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { averageColorSelector, contractQueries, @@ -33,7 +34,7 @@ import { TooltipInfoIcon, useAppContext, useCachedLoadable, - useDaoInfoContextIfAvailable, + useDaoIfAvailable, useDaoNavHelpers, useSupportedChainContext, useThemeContext, @@ -62,7 +63,6 @@ import { NEW_DAO_TOKEN_DECIMALS, SECRET_GAS, TokenBasedCreatorId, - convertMicroDenomToDenomWithDecimals, decodeJsonFromBase64, encodeJsonToBase64, findWasmAttributeValue, @@ -170,7 +170,7 @@ export const InnerCreateDaoForm = ({ initialPageIndex = 0, }: CreateDaoFormProps) => { const { t } = useTranslation() - const daoInfo = useDaoInfoContextIfAvailable() + const dao = useDaoIfAvailable() const queryClient = useQueryClient() const chainContext = useSupportedChainContext() @@ -787,17 +787,18 @@ export const InnerCreateDaoForm = ({ tokenBalance: daoVotingTokenBasedCreatorData.govTokenType === GovernanceTokenType.New - ? daoVotingTokenBasedCreatorData.newInfo.initialSupply + ? HugeDecimal.fromHumanReadable( + daoVotingTokenBasedCreatorData.newInfo.initialSupply, + NEW_DAO_TOKEN_DECIMALS + ) : // If using existing token but no token info loaded (should // be impossible), just display 0. !daoVotingTokenBasedCreatorData.existingToken || daoVotingTokenBasedCreatorData.existingTokenSupply === undefined - ? 0 - : // If using existing token, convert supply from query using decimals. - convertMicroDenomToDenomWithDecimals( - daoVotingTokenBasedCreatorData.existingTokenSupply, - daoVotingTokenBasedCreatorData.existingToken.decimals + ? HugeDecimal.zero + : HugeDecimal.from( + daoVotingTokenBasedCreatorData.existingTokenSupply ), tokenSymbol: daoVotingTokenBasedCreatorData.govTokenType === @@ -824,7 +825,7 @@ export const InnerCreateDaoForm = ({ } : //! Otherwise display native token, which has a balance of 0 initially. { - tokenBalance: 0, + tokenBalance: HugeDecimal.zero, tokenSymbol: nativeToken.symbol, tokenDecimals: nativeToken.decimals, } @@ -862,7 +863,7 @@ export const InnerCreateDaoForm = ({ data: { proposalCount: 0, tokenWithBalance: { - balance: tokenBalance, + balance: tokenBalance.toHumanReadableNumber(tokenDecimals), symbol: tokenSymbol, decimals: tokenDecimals, }, @@ -976,7 +977,7 @@ export const InnerCreateDaoForm = ({ } : undefined, current: makingSubDao ? t('title.newSubDao') : t('title.newDao'), - daoInfo, + dao, }} centerNode={ !makingSubDao && ( diff --git a/packages/stateful/components/dao/CreateDaoProposal.tsx b/packages/stateful/components/dao/CreateDaoProposal.tsx index ed59dad35..a07d7f54b 100644 --- a/packages/stateful/components/dao/CreateDaoProposal.tsx +++ b/packages/stateful/components/dao/CreateDaoProposal.tsx @@ -27,15 +27,15 @@ import { CreateProposal, PageLoader, ProposalModuleSelector, - useDaoInfoContext, + useDao, useDaoNavHelpers, useUpdatingRef, } from '@dao-dao/stateless' import { BaseNewProposalProps, DaoTabId, + IProposalModuleBase, ProposalDraft, - ProposalModuleInfo, ProposalPrefill, } from '@dao-dao/types' import { @@ -58,11 +58,11 @@ import { SuspenseLoader } from '../SuspenseLoader' import { ProposalDaoInfoCards } from './ProposalDaoInfoCards' export const CreateDaoProposal = () => { - const daoInfo = useDaoInfoContext() + const dao = useDao() const [selectedProposalModule, setSelectedProposalModule] = useState(() => { // Ignore proposals with an approver pre-propose since those are // automatically managed by a pre-propose-approval contract in another DAO. - const validProposalModules = daoInfo.proposalModules.filter( + const validProposalModules = dao.proposalModules.filter( ({ prePropose }) => prePropose?.contractName !== ContractName.PreProposeApprover ) @@ -85,9 +85,9 @@ export const CreateDaoProposal = () => { ({ snapshot }) => async () => setLatestProposalSave( - await snapshot.getPromise(latestProposalSaveAtom(daoInfo.coreAddress)) + await snapshot.getPromise(latestProposalSaveAtom(dao.coreAddress)) ), - [daoInfo.coreAddress] + [dao.coreAddress] ) useEffect(() => { loadLatestProposalSave() @@ -109,8 +109,8 @@ export const CreateDaoProposal = () => { } type InnerCreateDaoProposalProps = { - selectedProposalModule: ProposalModuleInfo - setSelectedProposalModule: Dispatch<SetStateAction<ProposalModuleInfo>> + selectedProposalModule: IProposalModuleBase + setSelectedProposalModule: Dispatch<SetStateAction<IProposalModuleBase>> latestProposalSave: any } @@ -121,7 +121,7 @@ const InnerCreateDaoProposal = ({ }: InnerCreateDaoProposalProps) => { const { t } = useTranslation() const { goToDaoProposal, router, getDaoProposalPath } = useDaoNavHelpers() - const daoInfo = useDaoInfoContext() + const dao = useDao() // Set once prefill has been assessed, indicating NewProposal can load now. const [prefillChecked, setPrefillChecked] = useState(false) @@ -147,7 +147,7 @@ const InnerCreateDaoProposal = ({ const { getValues, reset } = formMethods const setLatestProposalSave = useSetRecoilState( - latestProposalSaveAtom(daoInfo.coreAddress) + latestProposalSaveAtom(dao.coreAddress) ) // Reset form to defaults and clear latest proposal save. @@ -163,7 +163,7 @@ const InnerCreateDaoProposal = ({ const loadPrefill = useCallback( ({ id, data }: ProposalPrefill<any>) => { // Attempt to find proposal module to prefill and set if found. - const matchingProposalModule = daoInfo.proposalModules.find( + const matchingProposalModule = dao.proposalModules.find( ({ contractName }) => matchProposalModuleAdapter(contractName)?.id === id ) @@ -173,7 +173,7 @@ const InnerCreateDaoProposal = ({ reset(data) } }, - [daoInfo.proposalModules, reset, setSelectedProposalModule] + [dao.proposalModules, reset, setSelectedProposalModule] ) // Prefill form with data from parameter once ready. @@ -248,7 +248,7 @@ const InnerCreateDaoProposal = ({ ]) const [drafts, setDrafts] = useRecoilState( - proposalDraftsAtom(daoInfo.coreAddress) + proposalDraftsAtom(dao.coreAddress) ) const [draftIndex, setDraftIndex] = useState<number>() const draft = @@ -389,7 +389,7 @@ const InnerCreateDaoProposal = ({ // Copy link to clipboard. navigator.clipboard.writeText( SITE_URL + - getDaoProposalPath(daoInfo.coreAddress, 'create', { + getDaoProposalPath(dao.coreAddress, 'create', { pi: cid, }) ) @@ -405,7 +405,7 @@ const InnerCreateDaoProposal = ({ sdaLabel: t('title.proposals'), }, current: t('title.createProposal'), - daoInfo, + dao, }} /> @@ -451,7 +451,7 @@ const InnerCreateDaoProposal = ({ // storage periodically. const FormSaver = () => { const { watch, getValues } = useFormContext() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const proposalCreatedCardProps = useRecoilValue(proposalCreatedCardPropsAtom) const setLatestProposalSave = useSetRecoilState( diff --git a/packages/stateful/components/dao/CreateSubDao.tsx b/packages/stateful/components/dao/CreateSubDao.tsx index 609e85e2f..822b1c79e 100644 --- a/packages/stateful/components/dao/CreateSubDao.tsx +++ b/packages/stateful/components/dao/CreateSubDao.tsx @@ -1,4 +1,4 @@ -import { useDaoInfoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { ContractVersion } from '@dao-dao/types' import { getFallbackImage } from '@dao-dao/utils' @@ -12,10 +12,9 @@ export const CreateSubDao = () => { coreVersion, name, imageUrl, - parentDao, - admin, accounts, - } = useDaoInfoContext() + info: { parentDao, admin }, + } = useDao() // Chain x/gov DAO infos have coreAddress set to their name for URL // resolution, so retrieve their gov module address from their accounts list diff --git a/packages/stateful/components/dao/DaoFiatDepositModal.tsx b/packages/stateful/components/dao/DaoFiatDepositModal.tsx index 521085e81..38eb2d35c 100644 --- a/packages/stateful/components/dao/DaoFiatDepositModal.tsx +++ b/packages/stateful/components/dao/DaoFiatDepositModal.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import { KadoModal, useDaoInfoContext } from '@dao-dao/stateless' +import { KadoModal, useDao } from '@dao-dao/stateless' import { DaoFiatDepositModalProps } from '@dao-dao/types' export const DaoFiatDepositModal = ({ @@ -10,7 +10,7 @@ export const DaoFiatDepositModal = ({ }: DaoFiatDepositModalProps) => { const { t } = useTranslation() - const { chainId: daoChainId, coreAddress, accounts } = useDaoInfoContext() + const { chainId: daoChainId, coreAddress, accounts } = useDao() // Deposit address depends on the account type. let depositAddress = accounts.find( (account) => account.chainId === chainId && account.type === accountType diff --git a/packages/stateful/components/dao/DaoPreProposeApprovalProposalContentDisplay.tsx b/packages/stateful/components/dao/DaoPreProposeApprovalProposalContentDisplay.tsx index 3552d45d0..826e0a99e 100644 --- a/packages/stateful/components/dao/DaoPreProposeApprovalProposalContentDisplay.tsx +++ b/packages/stateful/components/dao/DaoPreProposeApprovalProposalContentDisplay.tsx @@ -5,7 +5,7 @@ import { Loader, ProposalContentDisplay, StatusCard, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -28,7 +28,7 @@ export const DaoPreProposeApprovalProposalContentDisplay = ({ proposalInfo, }: DaoPreProposeApprovalProposalContentDisplayProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { id, diff --git a/packages/stateful/components/dao/DaoProposal.tsx b/packages/stateful/components/dao/DaoProposal.tsx index 0efde5d17..7adfcb389 100644 --- a/packages/stateful/components/dao/DaoProposal.tsx +++ b/packages/stateful/components/dao/DaoProposal.tsx @@ -21,7 +21,7 @@ import { ProposalNotFound, ProposalProps, ProposalVotesPrivate, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { CommonProposalInfo, @@ -47,7 +47,7 @@ interface InnerDaoProposalProps { const InnerDaoProposal = ({ proposalInfo }: InnerDaoProposalProps) => { const { t } = useTranslation() - const daoInfo = useDaoInfoContext() + const dao = useDao() const { address } = useWallet() const proposalModuleAdapterContext = useProposalModuleAdapterContext() @@ -138,7 +138,7 @@ const InnerDaoProposal = ({ proposalInfo }: InnerDaoProposalProps) => { // Manually revalidate DAO static props. await fetch( - `/api/revalidate?d=${daoInfo.coreAddress}&p=${proposalInfo.id}` + `/api/revalidate?d=${dao.coreAddress}&p=${proposalInfo.id}` ) // Refresh entire app since any DAO config may have changed. @@ -235,7 +235,7 @@ const InnerDaoProposal = ({ proposalInfo }: InnerDaoProposalProps) => { sdaLabel: t('title.proposals'), }, current: `${t('title.proposal')} ${proposalInfo.id}`, - daoInfo, + dao, }} rightNode={ canVote ? ( @@ -277,7 +277,7 @@ const InnerDaoProposal = ({ proposalInfo }: InnerDaoProposalProps) => { VotesCast={ isPreProposeApprovalProposal ? undefined - : isSecretNetwork(daoInfo.chainId) + : isSecretNetwork(dao.chainId) ? ProposalVotesPrivate : ProposalVotes } @@ -328,7 +328,7 @@ const InnerDaoProposal = ({ proposalInfo }: InnerDaoProposalProps) => { } export const DaoProposal = ({ proposalInfo }: DaoProposalProps) => { - const { coreAddress } = useDaoInfoContext() + const dao = useDao() return proposalInfo ? ( <ProposalModuleAdapterProvider @@ -336,7 +336,7 @@ export const DaoProposal = ({ proposalInfo }: DaoProposalProps) => { // Make sure to refresh when the DAO or proposal ID changes. In case we // redirect to a proposal in the same DAO, this is necessary to refresh // for some reason. - coreAddress + proposalInfo.id + dao.coreAddress + proposalInfo.id } proposalId={proposalInfo.id} > diff --git a/packages/stateful/components/dao/DaoProposalContentDisplay.tsx b/packages/stateful/components/dao/DaoProposalContentDisplay.tsx index 7101dc2c1..ed3cf4e49 100644 --- a/packages/stateful/components/dao/DaoProposalContentDisplay.tsx +++ b/packages/stateful/components/dao/DaoProposalContentDisplay.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { ProposalContentDisplay, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -24,7 +24,7 @@ export type DaoProposalContentDisplayProps = { export const DaoProposalContentDisplay = ({ proposalInfo, }: DaoProposalContentDisplayProps) => { - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { id, diff --git a/packages/stateful/components/dao/DaoRewardsDistributorClaimCard.tsx b/packages/stateful/components/dao/DaoRewardsDistributorClaimCard.tsx index 66918a5f7..145548037 100644 --- a/packages/stateful/components/dao/DaoRewardsDistributorClaimCard.tsx +++ b/packages/stateful/components/dao/DaoRewardsDistributorClaimCard.tsx @@ -9,7 +9,7 @@ import { } from '@dao-dao/state/query' import { DaoRewardsDistributorClaimCard as StatelessDaoRewardsDistributorClaimCard, - useDaoContext, + useDao, } from '@dao-dao/stateless' import { StatefulDaoRewardsDistributorClaimCardProps } from '@dao-dao/types' import { executeSmartContracts, processError } from '@dao-dao/utils' @@ -20,7 +20,7 @@ export const DaoRewardsDistributorClaimCard = ({ ...props }: StatefulDaoRewardsDistributorClaimCardProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { address, isWalletConnected, getSigningClient } = useWallet() const queryClient = useQueryClient() @@ -47,12 +47,12 @@ export const DaoRewardsDistributorClaimCard = ({ } const claimableDistributions = rewards.data.distributions.filter( - ({ rewards }) => rewards > 0 + ({ rewards }) => rewards.isPositive() ) if ( claimableDistributions.length === 0 || - claimableDistributions.every(({ rewards }) => rewards === 0) + claimableDistributions.every(({ rewards }) => rewards.isZero()) ) { toast.error(t('error.noRewardsToClaim')) return diff --git a/packages/stateful/components/dao/DaoTokenCard.tsx b/packages/stateful/components/dao/DaoTokenCard.tsx index da02886e3..3e3a2d1f8 100644 --- a/packages/stateful/components/dao/DaoTokenCard.tsx +++ b/packages/stateful/components/dao/DaoTokenCard.tsx @@ -18,7 +18,7 @@ import { ChainProvider, TokenCard as StatelessTokenCard, useCachedLoading, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -49,14 +49,14 @@ export const DaoTokenCard = ({ const { t } = useTranslation() const router = useRouter() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const lazyInfo = useCachedLoading( tokenCardLazyInfoSelector({ owner: owner.address, token, - unstakedBalance, + unstakedBalance: unstakedBalance.toString(), }), { usdUnitPrice: undefined, @@ -101,7 +101,9 @@ export const DaoTokenCard = ({ const lazyStakes = lazyInfo.loading ? [] : lazyInfo.data.stakingInfo?.stakes ?? [] - const stakesWithRewards = lazyStakes.filter(({ rewards }) => rewards > 0) + const stakesWithRewards = lazyStakes.filter(({ rewards }) => + rewards.isPositive() + ) const nativeToken = getNativeTokenForChainId(token.chainId) @@ -131,37 +133,36 @@ export const DaoTokenCard = ({ // Prefill URL is valid if... const proposeStakeUnstakeHref = // ...there is something to stake or unstake - (unstakedBalance > 0 || lazyStakes.length > 0) && + (unstakedBalance.isPositive() || lazyStakes.length > 0) && // ...and this is the native token token.denomOrAddress === nativeToken.denomOrAddress ? getDaoProposalPath(coreAddress, 'create', { prefill: getDaoProposalSinglePrefill({ // If has unstaked, show stake action by default. - actions: - unstakedBalance > 0 - ? [ - { - actionKey: ActionKey.ManageStaking, - data: { - chainId: token.chainId, - stakeType: StakingActionType.Delegate, - validator: '', - amount: unstakedBalance, - denom: token.denomOrAddress, - }, - }, - ] - : // If has only staked, show unstake actions by default. - lazyStakes.map(({ validator, amount }) => ({ + actions: unstakedBalance.isPositive() + ? [ + { actionKey: ActionKey.ManageStaking, data: { chainId: token.chainId, - stakeType: StakingActionType.Undelegate, - validator, - amount, + stakeType: StakingActionType.Delegate, + validator: '', + amount: unstakedBalance, denom: token.denomOrAddress, }, - })), + }, + ] + : // If has only staked, show unstake actions by default. + lazyStakes.map(({ validator, amount }) => ({ + actionKey: ActionKey.ManageStaking, + data: { + chainId: token.chainId, + stakeType: StakingActionType.Undelegate, + validator, + amount, + denom: token.denomOrAddress, + }, + })), }), }) : undefined diff --git a/packages/stateful/components/dao/DaoTokenDepositModal.tsx b/packages/stateful/components/dao/DaoTokenDepositModal.tsx index 4a32c76e1..87e564b5c 100644 --- a/packages/stateful/components/dao/DaoTokenDepositModal.tsx +++ b/packages/stateful/components/dao/DaoTokenDepositModal.tsx @@ -1,9 +1,9 @@ -import { coins } from '@cosmjs/stargate' import { useCallback, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { useSetRecoilState } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20BaseSelectors, nativeDenomBalanceWithTimestampSelector, @@ -14,15 +14,10 @@ import { TokenDepositModal, TokenDepositModalProps, useCachedLoading, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { Account } from '@dao-dao/types' -import { - CHAIN_GAS_MULTIPLIER, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, - processError, -} from '@dao-dao/utils' +import { CHAIN_GAS_MULTIPLIER, processError } from '@dao-dao/utils' import { Cw20BaseHooks, useWallet } from '../../hooks' import { ConnectWallet } from '../ConnectWallet' @@ -41,7 +36,7 @@ export const DaoTokenDepositModal = ({ ...props }: DaoTokenDepositModalProps) => { const { t } = useTranslation() - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const { isWalletConnected, address, @@ -83,7 +78,7 @@ export const DaoTokenDepositModal = ({ } ) - const [amount, setAmount] = useState(0) + const [amount, setAmount] = useState(HugeDecimal.zero) const [loading, setLoading] = useState(false) const transferCw20 = Cw20BaseHooks.useTransfer({ @@ -92,7 +87,7 @@ export const DaoTokenDepositModal = ({ }) const onDeposit = useCallback( - async (amount: number) => { + async (amount: HugeDecimal) => { if (!address) { toast.error(t('error.logInToContinue')) return @@ -105,22 +100,17 @@ export const DaoTokenDepositModal = ({ setLoading(true) try { - const microAmount = convertDenomToMicroDenomStringWithDecimals( - amount, - token.decimals - ) - if (token.type === 'native') { const signingClient = await getSigningStargateClient() await signingClient.sendTokens( address, depositAddress, - coins(microAmount, token.denomOrAddress), + amount.toCoins(token.denomOrAddress), CHAIN_GAS_MULTIPLIER ) } else if (token.type === 'cw20') { await transferCw20({ - amount: microAmount, + amount: amount.toFixed(0), recipient: depositAddress, }) } @@ -130,8 +120,8 @@ export const DaoTokenDepositModal = ({ toast.success( t('success.depositedTokenIntoDao', { - amount: amount.toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + amount: amount.toInternationalizedHumanReadableString({ + decimals: token.decimals, }), tokenSymbol: token.symbol, daoName, @@ -140,7 +130,7 @@ export const DaoTokenDepositModal = ({ onClose?.() // Clear amount after a timeout to allow closing. - setTimeout(() => setAmount(0), 500) + setTimeout(() => setAmount(HugeDecimal.zero), 500) } catch (err) { console.error(err) toast.error(processError(err)) @@ -176,10 +166,9 @@ export const DaoTokenDepositModal = ({ : { loading: false, data: { - amount: convertMicroDenomToDenomWithDecimals( - loadingBalance.data.amount, - token.decimals - ), + amount: HugeDecimal.from( + loadingBalance.data.amount + ).toHumanReadableNumber(token.decimals), timestamp: loadingBalance.data.timestamp, }, } diff --git a/packages/stateful/components/dao/DaoTokenLine.tsx b/packages/stateful/components/dao/DaoTokenLine.tsx index 8fab1ae43..f6bd66df7 100644 --- a/packages/stateful/components/dao/DaoTokenLine.tsx +++ b/packages/stateful/components/dao/DaoTokenLine.tsx @@ -15,7 +15,7 @@ export const DaoTokenLine = <T extends TokenCardInfo>( tokenCardLazyInfoSelector({ owner: props.owner.address, token: props.token, - unstakedBalance: props.unstakedBalance, + unstakedBalance: props.unstakedBalance.toString(), }), { usdUnitPrice: undefined, diff --git a/packages/stateful/components/dao/DaoTxTreasuryHistory.tsx b/packages/stateful/components/dao/DaoTxTreasuryHistory.tsx index d8aa87b95..7febf33ce 100644 --- a/packages/stateful/components/dao/DaoTxTreasuryHistory.tsx +++ b/packages/stateful/components/dao/DaoTxTreasuryHistory.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRecoilCallback, useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { TransformedTreasuryTransaction, blockHeightSelector, @@ -17,14 +18,12 @@ import { CopyToClipboard, LineGraph, Loader, + TokenAmountDisplay, useChainContext, useConfiguredChainContext, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' -import { - convertMicroDenomToDenomWithDecimals, - processError, -} from '@dao-dao/utils' +import { processError, tokensEqual } from '@dao-dao/utils' import { IconButtonLink } from '../IconButtonLink' import { SuspenseLoader } from '../SuspenseLoader' @@ -64,7 +63,7 @@ export const InnerDaoTxTreasuryHistory = ({ chain: { chain_id: chainId }, nativeToken, } = useChainContext() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() // Initialization. const latestBlockHeight = useRecoilValue( @@ -165,29 +164,29 @@ export const InnerDaoTxTreasuryHistory = ({ }) ) const lineGraphValues = useMemo(() => { - let runningTotal = convertMicroDenomToDenomWithDecimals( - nativeBalance.amount, - nativeToken?.decimals ?? 0 - ) + if (!nativeToken) { + return [] + } + + let runningTotal = HugeDecimal.from( + nativeBalance.amount + ).toHumanReadableNumber(nativeToken.decimals) return ( transactions - .filter(({ denomLabel }) => denomLabel === nativeToken?.symbol) + .filter(({ token }) => tokensEqual(token, nativeToken)) .map(({ amount, outgoing }) => { let currentTotal = runningTotal - runningTotal -= (outgoing ? -1 : 1) * amount + runningTotal -= amount + .times(outgoing ? -1 : 1) + .toHumanReadableNumber(nativeToken.decimals) return currentTotal }) // Reverse since transactions are descending, but we want the graph to // display ascending balance. .reverse() ) - }, [ - nativeBalance.amount, - nativeToken?.decimals, - nativeToken?.symbol, - transactions, - ]) + }, [nativeBalance.amount, nativeToken, transactions]) return ( <div className="flex flex-col gap-y-4"> @@ -257,7 +256,7 @@ const TransactionRenderer = ({ sender, recipient, amount, - denomLabel, + token, outgoing, }, }: TransactionRendererProps) => { @@ -273,9 +272,11 @@ const TransactionRenderer = ({ ) : ( <East className="!h-4 !w-4" /> )} - <p> - {amount} ${denomLabel} - </p> + <TokenAmountDisplay + amount={amount} + decimals={token.decimals} + symbol={token.symbol} + /> </div> <p className="flex flex-row items-center gap-4 text-right font-mono text-xs leading-6"> diff --git a/packages/stateful/components/dao/DaoVotingVaultCard.tsx b/packages/stateful/components/dao/DaoVotingVaultCard.tsx index fc7ca7a71..049567337 100644 --- a/packages/stateful/components/dao/DaoVotingVaultCard.tsx +++ b/packages/stateful/components/dao/DaoVotingVaultCard.tsx @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { neutronVaultQueries } from '@dao-dao/state' import { DaoVotingVaultCard as StatelessDaoVotingVaultCard, @@ -35,9 +36,10 @@ export const DaoVotingVaultCard = (props: StatefulDaoVotingVaultCardProps) => { data: props.totalVotingPower.data === 0 ? 0 - : (Number(props.vault.totalPower) / - props.totalVotingPower.data) * - 100, + : props.vault.totalPower + .div(props.totalVotingPower.data) + .times(100) + .toNumber(), } } walletVotingPowerPercent={ @@ -52,12 +54,12 @@ export const DaoVotingVaultCard = (props: StatefulDaoVotingVaultCardProps) => { } : { loading: false, - data: - props.vault.totalPower === '0' - ? 0 - : (Number(loadingWalletVotingPower.data.power) / - Number(props.vault.totalPower)) * - 100, + data: props.vault.totalPower.isZero() + ? 0 + : HugeDecimal.from(loadingWalletVotingPower.data.power) + .div(props.vault.totalPower) + .times(100) + .toNumber(), } } {...props} diff --git a/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx b/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx index a4c4b36ea..440ace508 100644 --- a/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx +++ b/packages/stateful/components/dao/DiscordNotifierConfigureModal.tsx @@ -17,7 +17,7 @@ import { Tooltip, useCachedLoadable, useChain, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { DaoTabId } from '@dao-dao/types' @@ -37,8 +37,8 @@ export const DiscordNotifierConfigureModal = () => { const { t } = useTranslation() const router = useRouter() const { chain_id: chainId } = useChain() - const { coreAddress } = useDaoInfoContext() const { getDaoPath } = useDaoNavHelpers() + const { coreAddress } = useDao() const { isWalletConnected, hexPublicKey } = useWallet({ loadAccount: true, }) diff --git a/packages/stateful/components/dao/MainDaoInfoCards.tsx b/packages/stateful/components/dao/MainDaoInfoCards.tsx index b07c622e0..6648807a9 100644 --- a/packages/stateful/components/dao/MainDaoInfoCards.tsx +++ b/packages/stateful/components/dao/MainDaoInfoCards.tsx @@ -2,19 +2,16 @@ import uniq from 'lodash.uniq' import { useTranslation } from 'react-i18next' import { useRecoilValueLoadable, waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw1WhitelistSelectors } from '@dao-dao/state' import { DaoInfoCards as StatelessDaoInfoCards, TokenAmountDisplay, useChain, - useDaoContext, + useDao, } from '@dao-dao/stateless' import { PreProposeModuleType } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - formatDate, - formatPercentOf100, -} from '@dao-dao/utils' +import { formatDate, formatPercentOf100 } from '@dao-dao/utils' import { useDaoGovernanceToken, useQueryLoadingData } from '../../hooks' import { useVotingModuleAdapter } from '../../voting-module-adapter' @@ -42,7 +39,7 @@ const InnerMainDaoInfoCards = () => { const votingModuleCards = useMainDaoInfoCards() const tokenInfo = useDaoGovernanceToken() - const { dao } = useDaoContext() + const dao = useDao() const { activeThreshold, created, proposalModules } = dao.info const tvlLoading = useQueryLoadingData(dao.tvlQuery, { @@ -109,12 +106,7 @@ const InnerMainDaoInfoCards = () => { value: ( <TokenAmountDisplay amount={ - tvlLoading.loading - ? { loading: true } - : { - loading: false, - data: tvlLoading.data.amount, - } + tvlLoading.loading ? { loading: true } : tvlLoading.data.amount } dateFetched={ tvlLoading.loading @@ -122,7 +114,6 @@ const InnerMainDaoInfoCards = () => { : new Date(tvlLoading.data.timestamp) } estimatedUsdValue - hideApprox /> ), }, @@ -142,9 +133,8 @@ const InnerMainDaoInfoCards = () => { ) : tokenInfo && ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - activeThreshold.absolute_count.count, - tokenInfo.decimals + amount={HugeDecimal.from( + activeThreshold.absolute_count.count )} decimals={tokenInfo.decimals} symbol={tokenInfo.symbol} diff --git a/packages/stateful/components/dao/ProposalDaoInfoCardSections.tsx b/packages/stateful/components/dao/ProposalDaoInfoCardSections.tsx index 5f3b5bc85..f09a92580 100644 --- a/packages/stateful/components/dao/ProposalDaoInfoCardSections.tsx +++ b/packages/stateful/components/dao/ProposalDaoInfoCardSections.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import { Loader, TooltipInfoIcon, useDaoContext } from '@dao-dao/stateless' +import { Loader, TooltipInfoIcon, useDao } from '@dao-dao/stateless' import { PreProposeModuleType } from '@dao-dao/types' import { ProposalModuleAdapterCommonProvider } from '../../proposal-module-adapter' @@ -11,7 +11,7 @@ import { Trans } from '../Trans' import { ProposalDaoInfoCards } from './ProposalDaoInfoCards' export const ProposalDaoInfoCardSections = () => { - const { dao } = useDaoContext() + const dao = useDao() return ( <> diff --git a/packages/stateful/components/dao/commonVotingConfig/ActiveThresholdVotingConfigItem.tsx b/packages/stateful/components/dao/commonVotingConfig/ActiveThresholdVotingConfigItem.tsx index 5ccbbff17..b652441da 100644 --- a/packages/stateful/components/dao/commonVotingConfig/ActiveThresholdVotingConfigItem.tsx +++ b/packages/stateful/components/dao/commonVotingConfig/ActiveThresholdVotingConfigItem.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { FilmSlateEmoji, FormSwitchCard, - NumberInput, + NumericInput, SelectInput, } from '@dao-dao/stateless' import { @@ -23,12 +23,12 @@ const ActiveThresholdInput = ({ activeThreshold: { enabled, type } = { enabled: false, type: 'percent', - value: 10, + value: '10', }, }, register, setValue, - watch, + getValues, errors, }: DaoCreationVotingConfigItemInputProps<DaoCreationVotingConfigWithActiveThreshold>) => { const { t } = useTranslation() @@ -45,17 +45,17 @@ const ActiveThresholdInput = ({ {enabled && ( <> <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput containerClassName="grow min-w-[8rem]" error={errors?.activeThreshold?.value} fieldName="activeThreshold.value" + getValues={getValues} min={1} register={register} setValue={setValue} sizing="sm" step={type === 'percent' ? 0.001 : 1} - validation={[validatePositive, validateRequired]} - watch={watch} + validation={[validateRequired, validatePositive]} /> <SelectInput @@ -81,7 +81,7 @@ const ActiveThresholdReview = ({ activeThreshold: { enabled, type, value } = { enabled: false, type: 'percent', - value: 10, + value: '10', }, }, }: DaoCreationVotingConfigItemReviewProps<DaoCreationVotingConfigWithActiveThreshold>) => { diff --git a/packages/stateful/components/dao/commonVotingConfig/ProposalDepositVotingConfigItem.tsx b/packages/stateful/components/dao/commonVotingConfig/ProposalDepositVotingConfigItem.tsx index 53e9fa6a2..4213d67f9 100644 --- a/packages/stateful/components/dao/commonVotingConfig/ProposalDepositVotingConfigItem.tsx +++ b/packages/stateful/components/dao/commonVotingConfig/ProposalDepositVotingConfigItem.tsx @@ -3,6 +3,7 @@ import { UseFormWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenSelector } from '@dao-dao/state/recoil' import { GovernanceTokenType, @@ -33,7 +34,6 @@ import { import { NEW_DAO_TOKEN_DECIMALS, TokenBasedCreatorId, - convertMicroDenomToDenomWithDecimals, getChainAssets, isValidBech32Address, makeValidateAddress, @@ -48,6 +48,7 @@ const ProposalDepositInput = ({ }, register, setValue, + getValues, watch, errors, }: DaoCreationVotingConfigItemInputProps<DaoCreationVotingConfigWithProposalDeposit>) => { @@ -155,6 +156,7 @@ const ProposalDepositInput = ({ ? // Only works for cw20. 'voting_module_token' : governanceTokenLoadable.contents.denomOrAddress, + decimals: governanceTokenLoadable.contents.decimals, description: t('title.governanceToken'), }, ] @@ -166,6 +168,7 @@ const ProposalDepositInput = ({ chainId, type: TokenType.Cw20, denomOrAddress: 'other_cw20', + decimals: tokenLoaded?.decimals ?? 0, symbol: (type === TokenType.Cw20 && tokenLoaded?.symbol) || t('form.cw20Token'), imageUrl: (type === TokenType.Cw20 && tokenLoaded?.imageUrl) || undefined, @@ -205,11 +208,11 @@ const ProposalDepositInput = ({ watch: watch as UseFormWatch<DaoCreationVotingConfigWithProposalDeposit>, setValue, + getValues, register, fieldName: 'proposalDeposit.amount', error: errors?.proposalDeposit?.amount, - step: convertMicroDenomToDenomWithDecimals( - 1, + step: HugeDecimal.one.toHumanReadableNumber( tokenLoaded?.decimals ?? 0 ), }} @@ -305,8 +308,11 @@ const ProposalDepositReview = ({ ) : ( <> {t('format.token', { - amount: amount.toLocaleString(undefined, { - maximumFractionDigits: decimals, + amount: HugeDecimal.fromHumanReadable( + amount, + decimals + ).toInternationalizedHumanReadableString({ + decimals, }), symbol, })} diff --git a/packages/stateful/components/dao/commonVotingConfig/QuorumVotingConfigItem.tsx b/packages/stateful/components/dao/commonVotingConfig/QuorumVotingConfigItem.tsx index 5917efb18..961b7dc65 100644 --- a/packages/stateful/components/dao/commonVotingConfig/QuorumVotingConfigItem.tsx +++ b/packages/stateful/components/dao/commonVotingConfig/QuorumVotingConfigItem.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import { MegaphoneEmoji, NumberInput, SelectInput } from '@dao-dao/stateless' +import { MegaphoneEmoji, NumericInput, SelectInput } from '@dao-dao/stateless' import { DaoCreationVotingConfigItem, DaoCreationVotingConfigItemInputProps, @@ -19,7 +19,7 @@ const QuorumInput = ({ }, register, setValue, - watch, + getValues, errors, }: DaoCreationVotingConfigItemInputProps<DaoCreationVotingConfigWithQuorum>) => { const { t } = useTranslation() @@ -27,17 +27,18 @@ const QuorumInput = ({ return ( <div className="flex flex-row gap-2"> {!majority && ( - <NumberInput + <NumericInput containerClassName="grow min-w-[8rem]" error={errors?.quorum?.value} fieldName="quorum.value" + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" step={0.001} validation={[validatePositive, validateRequired]} - watch={watch} /> )} diff --git a/packages/stateful/components/dao/commonVotingConfig/VotingDurationVotingConfigItem.tsx b/packages/stateful/components/dao/commonVotingConfig/VotingDurationVotingConfigItem.tsx index 60dc5bcc6..5905bbccf 100644 --- a/packages/stateful/components/dao/commonVotingConfig/VotingDurationVotingConfigItem.tsx +++ b/packages/stateful/components/dao/commonVotingConfig/VotingDurationVotingConfigItem.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import { HourglassEmoji, NumberInput, SelectInput } from '@dao-dao/stateless' +import { HourglassEmoji, NumericInput, SelectInput } from '@dao-dao/stateless' import { DaoCreationVotingConfigItem, DaoCreationVotingConfigItemInputProps, @@ -19,18 +19,20 @@ export const VotingDurationInput = ({ data: { votingDuration }, register, setValue, - watch, + getValues, errors, }: DaoCreationVotingConfigItemInputProps<DaoCreationVotingConfigWithVotingDuration>) => { const { t } = useTranslation() return ( <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput containerClassName="grow" error={errors?.votingDuration?.value} fieldName="votingDuration.value" + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" @@ -45,7 +47,6 @@ export const VotingDurationInput = ({ value >= 60 || t('error.mustBeAtLeastSixtySeconds'), ]} - watch={watch} /> <SelectInput diff --git a/packages/stateful/components/dao/tabs/AppsTab.tsx b/packages/stateful/components/dao/tabs/AppsTab.tsx index f2bf01653..0a2a29a5b 100644 --- a/packages/stateful/components/dao/tabs/AppsTab.tsx +++ b/packages/stateful/components/dao/tabs/AppsTab.tsx @@ -27,7 +27,7 @@ import { AppsTab as StatelessAppsTab, StatusCard, useActionMatcher, - useDaoInfoContext, + useDao, useLoadingPromise, } from '@dao-dao/stateless' import { @@ -75,10 +75,10 @@ export const AppsTab = () => { name, chainId: currentChainId, coreAddress, - polytoneProxies, proposalModules, accounts, - } = useDaoInfoContext() + info: { polytoneProxies }, + } = useDao() // Select the single choice proposal module to use for proposals. const singleChoiceProposalModule = proposalModules.find( @@ -396,7 +396,7 @@ const InnerActionMatcherAndProposer = ({ actionKeysAndData, }: ActionMatcherAndProposerProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { connected, profile } = useProfile() const { diff --git a/packages/stateful/components/dao/tabs/HomeTab.tsx b/packages/stateful/components/dao/tabs/HomeTab.tsx index 9488535ac..48840ea8b 100644 --- a/packages/stateful/components/dao/tabs/HomeTab.tsx +++ b/packages/stateful/components/dao/tabs/HomeTab.tsx @@ -2,11 +2,12 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { DaoSplashHeader, useAppContext, useCachedLoadable, - useDaoContext, + useDao, } from '@dao-dao/stateless' import { CheckedDepositInfo, DaoPageMode } from '@dao-dao/types' import { getDaoRewardDistributors } from '@dao-dao/utils' @@ -27,7 +28,7 @@ import { MainDaoInfoCards } from '../MainDaoInfoCards' export const HomeTab = () => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { mode } = useAppContext() const { isWalletConnected, isSecretNetworkPermitNeeded } = useDaoWithWalletSecretNetworkPermit() @@ -54,19 +55,21 @@ export const HomeTab = () => { // Get max deposit of governance token across all proposal modules. const maxGovernanceTokenProposalModuleDeposit = proposalModuleDepositInfosLoadable.state !== 'hasValue' - ? 0 - : Math.max( - ...proposalModuleDepositInfosLoadable.contents - .filter( - (depositInfo): depositInfo is CheckedDepositInfo => - !!depositInfo && - ('cw20' in depositInfo.denom - ? depositInfo.denom.cw20 - : depositInfo.denom.native) === governanceDenomOrAddress - ) - .map(({ amount }) => Number(amount)), - 0 - ) + ? HugeDecimal.zero + : proposalModuleDepositInfosLoadable.contents + .filter( + (depositInfo): depositInfo is CheckedDepositInfo => + !!depositInfo && + ('cw20' in depositInfo.denom + ? depositInfo.denom.cw20 + : depositInfo.denom.native) === governanceDenomOrAddress + ) + // Get max. + .reduce( + (acc, { amount }) => + acc.gt(amount) ? acc : HugeDecimal.from(amount), + HugeDecimal.zero + ) const hasRewardDistributors = getDaoRewardDistributors(dao.info.items).length > 0 @@ -77,7 +80,7 @@ export const HomeTab = () => { <DaoSplashHeader ButtonLink={ButtonLink} LinkWrapper={LinkWrapper} - daoInfo={dao.info} + dao={dao} /> )} @@ -89,8 +92,8 @@ export const HomeTab = () => { {isWalletConnected && !isSecretNetworkPermitNeeded ? ( <ProfileCardMemberInfo maxGovernanceTokenDeposit={ - maxGovernanceTokenProposalModuleDeposit > 0 - ? BigInt(maxGovernanceTokenProposalModuleDeposit).toString() + maxGovernanceTokenProposalModuleDeposit.isPositive() + ? maxGovernanceTokenProposalModuleDeposit.toString() : undefined } /> diff --git a/packages/stateful/components/dao/tabs/ProposalsTab.tsx b/packages/stateful/components/dao/tabs/ProposalsTab.tsx index c0c4414ff..2224bdd5c 100644 --- a/packages/stateful/components/dao/tabs/ProposalsTab.tsx +++ b/packages/stateful/components/dao/tabs/ProposalsTab.tsx @@ -1,19 +1,19 @@ import { ProposalsTab as StatelessProposalsTab, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { ButtonLink } from '../../ButtonLink' import { ProposalList } from '../../ProposalList' export const ProposalsTab = () => { - const daoInfo = useDaoInfoContext() + const dao = useDao() return ( <StatelessProposalsTab ButtonLink={ButtonLink} ProposalList={ProposalList} - daoInfo={daoInfo} + dao={dao} /> ) } diff --git a/packages/stateful/components/dao/tabs/SubDaosTab.tsx b/packages/stateful/components/dao/tabs/SubDaosTab.tsx index e0c5edf51..082fe1892 100644 --- a/packages/stateful/components/dao/tabs/SubDaosTab.tsx +++ b/packages/stateful/components/dao/tabs/SubDaosTab.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query' import { SubDaosTab as StatelessSubDaosTab, - useDaoInfoContext, + useDao, useDaoNavHelpers, useInitializedActionForKey, } from '@dao-dao/stateless' @@ -15,7 +15,11 @@ import { ButtonLink } from '../../ButtonLink' import { DaoCard } from '../DaoCard' export const SubDaosTab = () => { - const { chainId, coreAddress, supportedFeatures } = useDaoInfoContext() + const { + chainId, + coreAddress, + info: { supportedFeatures }, + } = useDao() const { getDaoPath, getDaoProposalPath } = useDaoNavHelpers() const { isMember = false } = useMembership() diff --git a/packages/stateful/components/dao/tabs/TreasuryTab.tsx b/packages/stateful/components/dao/tabs/TreasuryTab.tsx index d449396b4..55df4337f 100644 --- a/packages/stateful/components/dao/tabs/TreasuryTab.tsx +++ b/packages/stateful/components/dao/tabs/TreasuryTab.tsx @@ -5,7 +5,7 @@ import { import { TreasuryTab as StatelessTreasuryTab, useCachedLoading, - useDaoInfoContext, + useDao, useDaoNavHelpers, useInitializedActionForKey, } from '@dao-dao/stateless' @@ -26,7 +26,7 @@ import { DaoFiatDepositModal } from '../DaoFiatDepositModal' import { DaoTokenLine } from '../DaoTokenLine' export const TreasuryTab = () => { - const daoInfo = useDaoInfoContext() + const dao = useDao() const { isWalletConnected } = useWallet() const { getDaoProposalPath } = useDaoNavHelpers() @@ -39,8 +39,8 @@ export const TreasuryTab = () => { const tokens = useCachedLoading( treasuryTokenCardInfosForDaoSelector({ - chainId: daoInfo.chainId, - coreAddress: daoInfo.coreAddress, + chainId: dao.chainId, + coreAddress: dao.coreAddress, cw20GovernanceTokenAddress, nativeGovernanceTokenDenom, }), @@ -48,8 +48,8 @@ export const TreasuryTab = () => { ) const nfts = useCachedLoading( lazyNftCardInfosForDaoSelector({ - chainId: daoInfo.chainId, - coreAddress: daoInfo.coreAddress, + chainId: dao.chainId, + coreAddress: dao.coreAddress, governanceCollectionAddress: cw721GovernanceCollectionAddress, }), {} @@ -97,7 +97,7 @@ export const TreasuryTab = () => { configureRebalancerHref={ // Prefill URL only valid if action exists. configureRebalancerAction - ? getDaoProposalPath(daoInfo.coreAddress, 'create', { + ? getDaoProposalPath(dao.coreAddress, 'create', { prefill: configureRebalancerPrefill, }) : undefined @@ -110,7 +110,7 @@ export const TreasuryTab = () => { !createCrossChainAccountAction.loading && !createCrossChainAccountAction.errored && !createCrossChainAccountAction.data.metadata.hideFromPicker - ? getDaoProposalPath(daoInfo.coreAddress, 'create', { + ? getDaoProposalPath(dao.coreAddress, 'create', { prefill: createCrossChainAccountPrefill, }) : undefined diff --git a/packages/stateful/components/gov/CreateGovProposal.tsx b/packages/stateful/components/gov/CreateGovProposal.tsx index e777451a1..ce0c1af44 100644 --- a/packages/stateful/components/gov/CreateGovProposal.tsx +++ b/packages/stateful/components/gov/CreateGovProposal.tsx @@ -1,10 +1,7 @@ import { useRef } from 'react' import { useTranslation } from 'react-i18next' -import { - CreateProposal, - useDaoInfoContextIfAvailable, -} from '@dao-dao/stateless' +import { CreateProposal, useDaoIfAvailable } from '@dao-dao/stateless' import { DaoTabId } from '@dao-dao/types' import { GovActionsProvider } from '../../actions' @@ -13,7 +10,7 @@ import { NewGovProposal } from './NewGovProposal' export const CreateGovProposal = () => { const { t } = useTranslation() - const daoInfo = useDaoInfoContextIfAvailable() + const dao = useDaoIfAvailable() const clearRef = useRef(() => {}) const copyDraftLinkRef = useRef(async () => {}) @@ -27,7 +24,7 @@ export const CreateGovProposal = () => { sdaLabel: t('title.proposals'), }, current: t('title.createProposal'), - daoInfo, + dao, }} /> diff --git a/packages/stateful/components/gov/GovCommunityPoolTab.tsx b/packages/stateful/components/gov/GovCommunityPoolTab.tsx index d1084ed9b..bc20f6959 100644 --- a/packages/stateful/components/gov/GovCommunityPoolTab.tsx +++ b/packages/stateful/components/gov/GovCommunityPoolTab.tsx @@ -1,5 +1,6 @@ import { waitForAny } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { communityPoolBalancesSelector, tokenCardLazyInfoSelector, @@ -15,11 +16,7 @@ import { useTokenSortOptions, } from '@dao-dao/stateless' import { LoadingDataWithError, TokenCardInfo } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - getNativeTokenForChainId, - loadableToLoadingData, -} from '@dao-dao/utils' +import { getNativeTokenForChainId, loadableToLoadingData } from '@dao-dao/utils' import { GovActionsProvider } from '../../actions' import { GovTokenLine } from './GovTokenLine' @@ -33,23 +30,18 @@ export const GovCommunityPoolTab = () => { }), (data) => data - .map(({ owner, token, balance }): TokenCardInfo => { - const unstakedBalance = convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ) - - return { + .map( + ({ owner, token, balance }): TokenCardInfo => ({ owner, token, isGovernanceToken: getNativeTokenForChainId(token.chainId).denomOrAddress === token.denomOrAddress, - unstakedBalance, + unstakedBalance: HugeDecimal.from(balance), hasStakingInfo: false, lazyInfo: { loading: true }, - } - }) + }) + ) // Sort governance token first and factory tokens last. .sort((a, b) => { if (a.isGovernanceToken) { @@ -83,7 +75,7 @@ export const GovCommunityPoolTab = () => { tokenCardLazyInfoSelector({ owner: owner.address, token, - unstakedBalance, + unstakedBalance: unstakedBalance.toString(), }) ) ) diff --git a/packages/stateful/components/gov/GovInfoBar.tsx b/packages/stateful/components/gov/GovInfoBar.tsx index 84cf532aa..52d8ee8ae 100644 --- a/packages/stateful/components/gov/GovInfoBar.tsx +++ b/packages/stateful/components/gov/GovInfoBar.tsx @@ -1,17 +1,13 @@ import { useTranslation } from 'react-i18next' -import { - DaoInfoCards, - TokenAmountDisplay, - useDaoContext, -} from '@dao-dao/stateless' +import { DaoInfoCards, TokenAmountDisplay, useDao } from '@dao-dao/stateless' import { useQueryLoadingData } from '../../hooks' export const GovInfoBar = () => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const tvlLoading = useQueryLoadingData(dao.tvlQuery, { amount: -1, timestamp: Date.now(), @@ -26,12 +22,7 @@ export const GovInfoBar = () => { value: ( <TokenAmountDisplay amount={ - tvlLoading.loading - ? { loading: true } - : { - loading: false, - data: tvlLoading.data.amount, - } + tvlLoading.loading ? { loading: true } : tvlLoading.data.amount } dateFetched={ tvlLoading.loading @@ -39,7 +30,6 @@ export const GovInfoBar = () => { : new Date(tvlLoading.data.timestamp) } estimatedUsdValue - hideApprox /> ), }, diff --git a/packages/stateful/components/gov/GovProposal.tsx b/packages/stateful/components/gov/GovProposal.tsx index 8d667072c..53375d9bc 100644 --- a/packages/stateful/components/gov/GovProposal.tsx +++ b/packages/stateful/components/gov/GovProposal.tsx @@ -10,7 +10,7 @@ import { Proposal, ProposalNotFound, useChain, - useDaoInfoContextIfAvailable, + useDaoIfAvailable, } from '@dao-dao/stateless' import { BaseProposalVotesProps, @@ -46,7 +46,7 @@ type InnerGovProposalProps = { const InnerGovProposal = ({ proposal }: InnerGovProposalProps) => { const { t } = useTranslation() const { chain_id: chainId } = useChain() - const daoInfo = useDaoInfoContextIfAvailable() + const dao = useDaoIfAvailable() const proposalId = proposal.id.toString() const loadingProposal = useLoadingGovProposal(proposalId) @@ -113,7 +113,7 @@ const InnerGovProposal = ({ proposal }: InnerGovProposalProps) => { sdaLabel: t('title.proposals'), }, current: `${t('title.proposal')} ${proposalId}`, - daoInfo, + dao, }} rightNode={ proposal.proposal.status === diff --git a/packages/stateful/components/gov/GovProposalStatusAndInfo.tsx b/packages/stateful/components/gov/GovProposalStatusAndInfo.tsx index 24b458111..3ea166e7e 100644 --- a/packages/stateful/components/gov/GovProposalStatusAndInfo.tsx +++ b/packages/stateful/components/gov/GovProposalStatusAndInfo.tsx @@ -1,5 +1,4 @@ import { EncodeObject } from '@cosmjs/proto-signing' -import { coins } from '@cosmjs/stargate' import { AccountCircleOutlined, AttachMoney, @@ -12,11 +11,12 @@ import { ComponentProps, ComponentType, useCallback, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenSelector } from '@dao-dao/state/recoil' import { GOV_PROPOSAL_STATUS_I18N_KEY_MAP, Logo, - NumberInput, + NumericInput, ProposalStatusAndInfoProps, ProposalStatusAndInfo as StatelessProposalStatusAndInfo, Tooltip, @@ -35,8 +35,6 @@ import { ProposalStatus } from '@dao-dao/types/protobuf/codegen/cosmos/gov/v1bet import { MsgDeposit } from '@dao-dao/types/protobuf/codegen/cosmos/gov/v1beta1/tx' import { CHAIN_GAS_MULTIPLIER, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, formatPercentOf100, getDisplayNameForChainId, processError, @@ -133,10 +131,8 @@ const InnerGovProposalStatusAndInfo = ({ (d) => d.denom === depositToken.denomOrAddress )!.amount - const missingDeposit = convertMicroDenomToDenomWithDecimals( - Number(minDepositAmount) - Number(currentDepositAmount), - depositToken.decimals - ) + const missingDeposit = + HugeDecimal.from(minDepositAmount).minus(currentDepositAmount) const info: ProposalStatusAndInfoProps['info'] = [ { @@ -176,11 +172,10 @@ const InnerGovProposalStatusAndInfo = ({ Value: (props) => ( <p {...props}> {t('format.token', { - amount: convertMicroDenomToDenomWithDecimals( - currentDepositAmount, - depositToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: depositToken.decimals, + amount: HugeDecimal.from( + currentDepositAmount + ).toInternationalizedHumanReadableString({ + decimals: depositToken.decimals, }), symbol: depositToken.symbol, })} @@ -236,13 +231,7 @@ const InnerGovProposalStatusAndInfo = ({ value: { proposalId, depositor: walletAddress, - amount: coins( - convertDenomToMicroDenomStringWithDecimals( - depositValue, - depositToken.decimals - ), - depositToken.denomOrAddress - ), + amount: depositValue.toCoins(depositToken.denomOrAddress), }, } @@ -254,7 +243,9 @@ const InnerGovProposalStatusAndInfo = ({ toast.success( t('success.deposited', { - amount: depositValue, + amount: depositValue.toInternationalizedHumanReadableString({ + decimals: depositToken.decimals, + }), tokenSymbol: depositToken.symbol, }) ) @@ -297,35 +288,35 @@ const InnerGovProposalStatusAndInfo = ({ status === ProposalStatus.PROPOSAL_STATUS_DEPOSIT_PERIOD ? { header: ( - <NumberInput + <NumericInput containerClassName="-mb-1" - max={missingDeposit} - min={convertMicroDenomToDenomWithDecimals( - 1, + max={missingDeposit.toHumanReadableString( + depositToken.decimals + )} + min={HugeDecimal.one.toHumanReadableNumber( depositToken.decimals )} - onInput={(event) => - setDepositValue( - Number( - Number( - (event.target as HTMLInputElement).value - ).toFixed(depositToken.decimals) - ) - ) - } onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() deposit() } }} - setValue={(_, value) => setDepositValue(value)} - step={convertMicroDenomToDenomWithDecimals( - 1, + setValue={(_, value) => + setDepositValue( + HugeDecimal.fromHumanReadable( + value, + depositToken.decimals + ) + ) + } + step={HugeDecimal.one.toHumanReadableNumber( depositToken.decimals )} unit={'$' + depositToken.symbol} - value={depositValue} + value={depositValue.toHumanReadableString( + depositToken.decimals + )} /> ), label: t('button.deposit'), diff --git a/packages/stateful/components/gov/GovProposalVotes.tsx b/packages/stateful/components/gov/GovProposalVotes.tsx index 479c67ae4..deb203a5c 100644 --- a/packages/stateful/components/gov/GovProposalVotes.tsx +++ b/packages/stateful/components/gov/GovProposalVotes.tsx @@ -68,8 +68,7 @@ const InnerGovProposalVotes = ({ voterAddress: voter, vote: options.sort((a, b) => Number(b.weight) - Number(a.weight))[0] .option, - votingPowerPercent: - Number(staked) / Number(BigInt(bondedTokens) / 100n), + votingPowerPercent: staked.div(bondedTokens).div(100).toNumber(), }) ) diff --git a/packages/stateful/components/gov/GovTokenLine.tsx b/packages/stateful/components/gov/GovTokenLine.tsx index a02ce2e25..b499b7644 100644 --- a/packages/stateful/components/gov/GovTokenLine.tsx +++ b/packages/stateful/components/gov/GovTokenLine.tsx @@ -15,7 +15,7 @@ export const GovTokenLine = <T extends TokenCardInfo>( tokenCardLazyInfoSelector({ owner: props.owner.address, token: props.token, - unstakedBalance: props.unstakedBalance, + unstakedBalance: props.unstakedBalance.toString(), }), { usdUnitPrice: undefined, diff --git a/packages/stateful/components/pages/DaoDappHome.tsx b/packages/stateful/components/pages/DaoDappHome.tsx index 1665681b3..21ae21605 100644 --- a/packages/stateful/components/pages/DaoDappHome.tsx +++ b/packages/stateful/components/pages/DaoDappHome.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { DaoDappTabbedHome, FollowingToggle, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -42,14 +42,14 @@ export const InnerDaoDappHome = ({ ...props }: InnerDaoDappHomeProps) => { const { t } = useTranslation() - const daoInfo = useDaoInfoContext() + const dao = useDao() const { getDaoPath, getDaoProposalPath, router } = useDaoNavHelpers() const { isFollowing, setFollowing, setUnfollowing, updatingFollowing } = useFollowingDaos() const following = isFollowing({ - chainId: daoInfo.chainId, - coreAddress: daoInfo.coreAddress, + chainId: dao.chainId, + coreAddress: dao.coreAddress, }) // Just a type-check because some tabs are loaded at the beginning. @@ -61,9 +61,9 @@ export const InnerDaoDappHome = ({ // Pre-fetch tabs. useEffect(() => { tabs?.forEach((tab) => { - router.prefetch(getDaoPath(daoInfo.coreAddress, tab.id)) + router.prefetch(getDaoPath(dao.coreAddress, tab.id)) }) - }, [daoInfo.coreAddress, getDaoPath, router, tabs]) + }, [dao.coreAddress, getDaoPath, router, tabs]) const slug = (router.query.slug || []) as string[] const checkedSlug = useRef(false) @@ -78,11 +78,11 @@ export const InnerDaoDappHome = ({ // If no slug and on current DAO, redirect to first tab. if (slug.length === 0) { - router.replace(getDaoPath(daoInfo.coreAddress, firstTabId), undefined, { + router.replace(getDaoPath(dao.coreAddress, firstTabId), undefined, { shallow: true, }) } - }, [daoInfo.coreAddress, getDaoPath, router, slug.length, firstTabId]) + }, [dao.coreAddress, getDaoPath, router, slug.length, firstTabId]) const tabId = slug.length > 0 && tabs?.some(({ id }) => id === slug[0]) @@ -90,7 +90,7 @@ export const InnerDaoDappHome = ({ : // If tab is invalid, default to first tab. firstTabId const onSelectTabId = (tabId: string) => - router.replace(getDaoPath(daoInfo.coreAddress, tabId), undefined, { + router.replace(getDaoPath(dao.coreAddress, tabId), undefined, { shallow: true, }) @@ -98,8 +98,8 @@ export const InnerDaoDappHome = ({ following, onFollow: () => (following ? setUnfollowing : setFollowing)({ - chainId: daoInfo.chainId, - coreAddress: daoInfo.coreAddress, + chainId: dao.chainId, + coreAddress: dao.coreAddress, }), updatingFollowing, } @@ -109,8 +109,8 @@ export const InnerDaoDappHome = ({ <PageHeaderContent breadcrumbs={{ home: true, - current: daoInfo.name, - daoInfo, + current: dao.name, + dao, }} rightNode={ <> @@ -118,7 +118,7 @@ export const InnerDaoDappHome = ({ <ButtonLink className="hidden md:block" contentContainerClassName="text-text-body text-base !gap-1.5" - href={getDaoProposalPath(daoInfo.coreAddress, 'create')} + href={getDaoProposalPath(dao.coreAddress, 'create')} variant="ghost" > <Add className="!h-5 !w-5 !text-icon-primary" /> @@ -158,10 +158,8 @@ export const DaoDappHome = () => { chainId, coreAddress, name, - contractAdmin, - supportedFeatures, - parentDao, - } = useDaoInfoContext() + info: { contractAdmin, supportedFeatures, parentDao }, + } = useDao() const { isMember = false } = useMembership() // We won't use this value unless there's a parent, so the undefined DAO diff --git a/packages/stateful/components/pages/DaoSdaHome.tsx b/packages/stateful/components/pages/DaoSdaHome.tsx index 75135eb5e..1d4521791 100644 --- a/packages/stateful/components/pages/DaoSdaHome.tsx +++ b/packages/stateful/components/pages/DaoSdaHome.tsx @@ -1,11 +1,7 @@ import { useRouter } from 'next/router' import { useEffect, useRef } from 'react' -import { - DaoSdaWrappedTab, - useDaoInfoContext, - useDaoNavHelpers, -} from '@dao-dao/stateless' +import { DaoSdaWrappedTab, useDao, useDaoNavHelpers } from '@dao-dao/stateless' import { DaoTabId } from '@dao-dao/types' import { useDaoTabs } from '../../hooks' @@ -14,7 +10,7 @@ import { SuspenseLoader } from '../SuspenseLoader' export const DaoSdaHome = () => { const router = useRouter() - const daoInfo = useDaoInfoContext() + const dao = useDao() const { getDaoPath } = useDaoNavHelpers() const loadingTabs = useDaoTabs() @@ -27,9 +23,9 @@ export const DaoSdaHome = () => { // Pre-fetch tabs. useEffect(() => { tabs?.forEach((tab) => { - router.prefetch(getDaoPath(daoInfo.coreAddress, tab.id)) + router.prefetch(getDaoPath(dao.coreAddress, tab.id)) }) - }, [daoInfo.coreAddress, getDaoPath, router, tabs]) + }, [dao.coreAddress, getDaoPath, router, tabs]) const slug = (router.query.slug || []) as string[] const checkedSlug = useRef(false) @@ -44,11 +40,11 @@ export const DaoSdaHome = () => { // If no slug, redirect to first tab. if (slug.length === 0) { - router.replace(getDaoPath(daoInfo.coreAddress, firstTabId), undefined, { + router.replace(getDaoPath(dao.coreAddress, firstTabId), undefined, { shallow: true, }) } - }, [daoInfo.coreAddress, getDaoPath, router, slug.length, firstTabId]) + }, [dao.coreAddress, getDaoPath, router, slug.length, firstTabId]) const selectedTabId = slug.length > 0 && tabs?.some(({ id }) => id === slug[0]) @@ -65,7 +61,7 @@ export const DaoSdaHome = () => { breadcrumbs={{ home: true, current: activeTab?.label, - daoInfo, + dao, }} /> diff --git a/packages/stateful/components/profile/ProfileProposalCard.tsx b/packages/stateful/components/profile/ProfileProposalCard.tsx index f3735a777..d1eb183fc 100644 --- a/packages/stateful/components/profile/ProfileProposalCard.tsx +++ b/packages/stateful/components/profile/ProfileProposalCard.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { useSetRecoilState, waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { updateProfileNftVisibleAtom } from '@dao-dao/state/recoil' import { Loader, @@ -63,19 +64,21 @@ export const ProfileProposalCard = () => { // Get max deposit of governance token across all proposal modules. const maxGovernanceTokenProposalModuleDeposit = proposalModuleDepositInfosLoadable.state !== 'hasValue' - ? 0 - : Math.max( - ...proposalModuleDepositInfosLoadable.contents - .filter( - (depositInfo): depositInfo is CheckedDepositInfo => - !!depositInfo && - ('cw20' in depositInfo.denom - ? depositInfo.denom.cw20 - : depositInfo.denom.native) === governanceDenomOrAddress - ) - .map(({ amount }) => Number(amount)), - 0 - ) + ? HugeDecimal.zero + : proposalModuleDepositInfosLoadable.contents + .filter( + (depositInfo): depositInfo is CheckedDepositInfo => + !!depositInfo && + ('cw20' in depositInfo.denom + ? depositInfo.denom.cw20 + : depositInfo.denom.native) === governanceDenomOrAddress + ) + // Get max. + .reduce( + (acc, { amount }) => + acc.gt(amount) ? acc : HugeDecimal.from(amount), + HugeDecimal.zero + ) // If wallet is a member right now as opposed to when the proposal was open. // Relevant for showing them membership join info or not. @@ -134,8 +137,8 @@ export const ProfileProposalCard = () => { <ProfileCardMemberInfo cantVoteOnProposal maxGovernanceTokenDeposit={ - maxGovernanceTokenProposalModuleDeposit > 0 - ? BigInt(maxGovernanceTokenProposalModuleDeposit).toString() + maxGovernanceTokenProposalModuleDeposit.isPositive() + ? maxGovernanceTokenProposalModuleDeposit.toString() : undefined } /> diff --git a/packages/stateful/components/vesting/VestingPaymentCard.tsx b/packages/stateful/components/vesting/VestingPaymentCard.tsx index e50810ee3..3d5e407d9 100644 --- a/packages/stateful/components/vesting/VestingPaymentCard.tsx +++ b/packages/stateful/components/vesting/VestingPaymentCard.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { chainQueries, cwVestingExtraQueries, @@ -23,7 +24,6 @@ import { StatefulVestingPaymentCardProps, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, getDaoProposalSinglePrefill, getNativeTokenForChainId, loadableToLoadingData, @@ -94,14 +94,14 @@ export const VestingPaymentCard = ({ owner: vestingContractAddress, token, // Unused. We just want the USD price and staking info. - unstakedBalance: 0, + unstakedBalance: '0', }) ), { usdUnitPrice: undefined, stakingInfo: undefined, // Unused. We just want the USD price and staking info. - totalBalance: 0, + totalBalance: HugeDecimal.zero, } ) @@ -279,17 +279,11 @@ export const VestingPaymentCard = ({ // Canceled vests have their curves set to constant. 'constant' in vest.vested } - claimedAmount={convertMicroDenomToDenomWithDecimals( - vest.claimed, - token.decimals - )} + claimedAmount={HugeDecimal.from(vest.claimed)} claiming={claiming} cw20Address={cw20Address} description={vest.description} - distributableAmount={convertMicroDenomToDenomWithDecimals( - distributable, - token.decimals - )} + distributableAmount={distributable} endDate={endDate} isWalletConnected={isWalletConnected} lazyInfo={lazyInfoLoading} @@ -302,10 +296,7 @@ export const VestingPaymentCard = ({ recipient={vest.recipient} recipientEntity={recipientEntity} recipientIsWallet={recipientIsWallet} - remainingBalanceVesting={convertMicroDenomToDenomWithDecimals( - Number(total) - Number(vested), - token.decimals - )} + remainingBalanceVesting={total.minus(vested)} startDate={startDate} steps={steps} title={vest.title} diff --git a/packages/stateful/components/vesting/VestingStakingModal.tsx b/packages/stateful/components/vesting/VestingStakingModal.tsx index eaf9cadc0..ae424553b 100644 --- a/packages/stateful/components/vesting/VestingStakingModal.tsx +++ b/packages/stateful/components/vesting/VestingStakingModal.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { chainQueries, cwVestingExtraQueries, @@ -14,15 +15,17 @@ import { } from '@dao-dao/state/recoil' import { StakingModal, - StakingModalProps, - StakingMode, useCachedLoadable, useDaoNavHelpers, } from '@dao-dao/stateless' -import { ActionKey, TokenStake, VestingInfo } from '@dao-dao/types' import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, + ActionKey, + StakingModalProps, + StakingMode, + TokenStake, + VestingInfo, +} from '@dao-dao/types' +import { getDaoProposalSinglePrefill, getNativeTokenForChainId, processError, @@ -73,7 +76,7 @@ export const VestingStakingModal = ({ const awaitNextBlock = useAwaitNextBlock() - const [amount, setAmount] = useState(0) + const [amount, setAmount] = useState(HugeDecimal.zero) const [loading, setLoading] = useState(false) const { address: walletAddress = '' } = useWallet({ @@ -100,7 +103,7 @@ export const VestingStakingModal = ({ const onAction = async ( mode: StakingMode, - amount: number, + amount: HugeDecimal, validator?: string, fromValidator?: string ) => { @@ -114,10 +117,7 @@ export const VestingStakingModal = ({ try { if (mode === StakingMode.Stake) { const data = { - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - nativeToken.decimals - ), + amount: amount.toFixed(0), validator, } @@ -149,10 +149,7 @@ export const VestingStakingModal = ({ } } else if (mode === StakingMode.Unstake) { const data = { - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - nativeToken.decimals - ), + amount: amount.toFixed(0), validator, } @@ -188,10 +185,6 @@ export const VestingStakingModal = ({ return } - const convertedAmount = convertDenomToMicroDenomStringWithDecimals( - amount, - nativeToken.decimals - ) if (recipientIsDao) { await goToDaoProposal(recipient, 'create', { prefill: getDaoProposalSinglePrefill({ @@ -204,7 +197,7 @@ export const VestingStakingModal = ({ message: JSON.stringify( { redelegate: { - amount: convertedAmount, + amount: amount.toFixed(0), src_validator: fromValidator, dst_validator: validator, }, @@ -221,7 +214,7 @@ export const VestingStakingModal = ({ }) } else { await redelegate({ - amount: convertedAmount, + amount: amount.toFixed(0), srcValidator: fromValidator, dstValidator: validator, }) @@ -293,17 +286,14 @@ export const VestingStakingModal = ({ amount={amount} claimableTokens={ // Tokens are claimable somewhere else. - 0 + HugeDecimal.zero } enableRestaking initialMode={StakingMode.Stake} loading={loading} loadingStakableTokens={{ loading: false, - data: convertMicroDenomToDenomWithDecimals( - stakable, - nativeToken.decimals - ), + data: HugeDecimal.from(stakable), }} onAction={onAction} setAmount={setAmount} diff --git a/packages/stateful/components/wallet/WalletStakingModal.tsx b/packages/stateful/components/wallet/WalletStakingModal.tsx index 006a06fd3..a5c6f0d6a 100644 --- a/packages/stateful/components/wallet/WalletStakingModal.tsx +++ b/packages/stateful/components/wallet/WalletStakingModal.tsx @@ -1,9 +1,9 @@ -import { coin } from '@cosmjs/stargate' import { useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { chainQueries } from '@dao-dao/state/query' import { nativeUnstakingDurationSecondsSelector, @@ -11,18 +11,15 @@ import { } from '@dao-dao/state/recoil' import { StakingModal, - StakingModalProps, - StakingMode, useCachedLoadable, useChainContext, } from '@dao-dao/stateless' -import { cwMsgToEncodeObject } from '@dao-dao/types' import { - CHAIN_GAS_MULTIPLIER, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, - processError, -} from '@dao-dao/utils' + StakingModalProps, + StakingMode, + cwMsgToEncodeObject, +} from '@dao-dao/types' +import { CHAIN_GAS_MULTIPLIER, processError } from '@dao-dao/utils' import { useAwaitNextBlock, @@ -61,16 +58,15 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { address: walletAddress, }) : undefined, - 0, + HugeDecimal.zero, { - transform: ({ amount }) => - convertMicroDenomToDenomWithDecimals(amount, nativeToken.decimals), + transform: ({ amount }) => HugeDecimal.from(amount), } ) const awaitNextBlock = useAwaitNextBlock() - const [amount, setAmount] = useState(0) + const [amount, setAmount] = useState(HugeDecimal.zero) const [loading, setLoading] = useState(false) const validatorsLoadable = useCachedLoadable( @@ -94,14 +90,8 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { delegations.map(({ validator, delegated, pendingReward }) => ({ token: nativeToken, validator, - amount: convertMicroDenomToDenomWithDecimals( - delegated.amount, - nativeToken.decimals - ), - rewards: convertMicroDenomToDenomWithDecimals( - pendingReward.amount, - nativeToken.decimals - ), + amount: HugeDecimal.from(delegated.amount), + rewards: HugeDecimal.from(pendingReward.amount), })) ) const stakes = @@ -109,7 +99,7 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { const onAction = async ( mode: StakingMode, - amount: number, + amount: HugeDecimal, validator?: string | undefined ) => { // Should never happen. @@ -126,11 +116,6 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { try { const signingClient = await getSigningStargateClient() - const microAmount = convertDenomToMicroDenomStringWithDecimals( - amount, - nativeToken.decimals - ) - if (mode === StakingMode.Stake) { await signingClient.signAndBroadcast( walletAddress, @@ -140,7 +125,7 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { { staking: { delegate: { - amount: coin(microAmount, nativeToken.denomOrAddress), + amount: amount.toCoin(nativeToken.denomOrAddress), validator, }, }, @@ -159,7 +144,7 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { { staking: { undelegate: { - amount: coin(microAmount, nativeToken.denomOrAddress), + amount: amount.toCoin(nativeToken.denomOrAddress), validator, }, }, @@ -192,7 +177,7 @@ export const WalletStakingModal = (props: WalletStakingModalProps) => { amount={amount} claimableTokens={ // Tokens are claimable somewhere else. - 0 + HugeDecimal.zero } initialMode={StakingMode.Stake} loading={loading} diff --git a/packages/stateful/components/wallet/WalletTokenCard.tsx b/packages/stateful/components/wallet/WalletTokenCard.tsx index 1a1df9234..62ccaa12b 100644 --- a/packages/stateful/components/wallet/WalletTokenCard.tsx +++ b/packages/stateful/components/wallet/WalletTokenCard.tsx @@ -71,7 +71,7 @@ export const WalletTokenCard = (props: TokenCardInfo) => { tokenCardLazyInfoSelector({ owner: props.owner.address, token: props.token, - unstakedBalance: props.unstakedBalance, + unstakedBalance: props.unstakedBalance.toString(), }), { usdUnitPrice: undefined, diff --git a/packages/stateful/components/wallet/WalletTokenCardReadonly.tsx b/packages/stateful/components/wallet/WalletTokenCardReadonly.tsx index 3821dcb67..ca9308982 100644 --- a/packages/stateful/components/wallet/WalletTokenCardReadonly.tsx +++ b/packages/stateful/components/wallet/WalletTokenCardReadonly.tsx @@ -14,7 +14,7 @@ export const WalletTokenCardReadonly = (props: TokenCardInfo) => { tokenCardLazyInfoSelector({ owner: props.owner.address, token: props.token, - unstakedBalance: props.unstakedBalance, + unstakedBalance: props.unstakedBalance.toString(), }), { usdUnitPrice: undefined, diff --git a/packages/stateful/components/wallet/WalletTokenLine.tsx b/packages/stateful/components/wallet/WalletTokenLine.tsx index af666a652..45cf6ac2e 100644 --- a/packages/stateful/components/wallet/WalletTokenLine.tsx +++ b/packages/stateful/components/wallet/WalletTokenLine.tsx @@ -15,7 +15,7 @@ export const WalletTokenLine = <T extends TokenCardInfo>( tokenCardLazyInfoSelector({ owner: props.owner.address, token: props.token, - unstakedBalance: props.unstakedBalance, + unstakedBalance: props.unstakedBalance.toString(), }), { usdUnitPrice: undefined, diff --git a/packages/stateful/components/wallet/WalletTokenLineReadonly.tsx b/packages/stateful/components/wallet/WalletTokenLineReadonly.tsx index d24cc30a7..8ca6fe38c 100644 --- a/packages/stateful/components/wallet/WalletTokenLineReadonly.tsx +++ b/packages/stateful/components/wallet/WalletTokenLineReadonly.tsx @@ -15,7 +15,7 @@ export const WalletTokenLineReadonly = <T extends TokenCardInfo>( tokenCardLazyInfoSelector({ owner: props.owner.address, token: props.token, - unstakedBalance: props.unstakedBalance, + unstakedBalance: props.unstakedBalance.toString(), }), { usdUnitPrice: undefined, diff --git a/packages/stateful/creators/MembershipBased/GovernanceConfigurationInput.tsx b/packages/stateful/creators/MembershipBased/GovernanceConfigurationInput.tsx index cf6295d9e..ba65e8ec8 100644 --- a/packages/stateful/creators/MembershipBased/GovernanceConfigurationInput.tsx +++ b/packages/stateful/creators/MembershipBased/GovernanceConfigurationInput.tsx @@ -32,6 +32,7 @@ export const GovernanceConfigurationInput = ({ register, watch, setValue, + getValues, setError, clearErrors, }, @@ -175,6 +176,7 @@ export const GovernanceConfigurationInput = ({ control={control} data={data} errors={errors} + getValues={getValues} register={register} remove={tierFields.length === 1 ? undefined : () => removeTier(idx)} setValue={setValue} diff --git a/packages/stateful/creators/MembershipBased/TierCard.tsx b/packages/stateful/creators/MembershipBased/TierCard.tsx index d652239f6..b58b34b6d 100644 --- a/packages/stateful/creators/MembershipBased/TierCard.tsx +++ b/packages/stateful/creators/MembershipBased/TierCard.tsx @@ -2,6 +2,7 @@ import { Add, Close } from '@mui/icons-material' import { Control, FormState, + UseFormGetValues, UseFormRegister, UseFormSetValue, UseFormWatch, @@ -14,7 +15,7 @@ import { IconButton, InputErrorMessage, InputLabel, - NumberInput, + NumericInput, TextInput, useChain, } from '@dao-dao/stateless' @@ -41,6 +42,7 @@ export interface TierCardProps { watch: UseFormWatch<NewDao<CreatorData>> errors: FormState<NewDao<CreatorData>>['errors'] setValue: UseFormSetValue<NewDao<CreatorData>> + getValues: UseFormGetValues<NewDao<CreatorData>> remove?: () => void } @@ -58,6 +60,7 @@ export const TierCard = ({ watch, errors, setValue, + getValues, showColorDotOnMember, } = props @@ -135,15 +138,16 @@ export const TierCard = ({ })} /> - <NumberInput + <NumericInput error={errors.creator?.data?.tiers?.[tierIndex]?.weight} fieldName={`creator.data.tiers.${tierIndex}.weight`} + getValues={getValues} min={0} + numericValue register={register} setValue={setValue} step={1} validation={[validateNonNegative, validateRequired]} - watch={watch} /> <InputErrorMessage diff --git a/packages/stateful/creators/NftBased/UnstakingDurationVotingConfigItem.tsx b/packages/stateful/creators/NftBased/UnstakingDurationVotingConfigItem.tsx index a9ab15af8..db27cf3ad 100644 --- a/packages/stateful/creators/NftBased/UnstakingDurationVotingConfigItem.tsx +++ b/packages/stateful/creators/NftBased/UnstakingDurationVotingConfigItem.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import { ClockEmoji, NumberInput, SelectInput } from '@dao-dao/stateless' +import { ClockEmoji, NumericInput, SelectInput } from '@dao-dao/stateless' import { DaoCreationVotingConfigItem, DaoCreationVotingConfigItemInputProps, @@ -19,24 +19,25 @@ export const UnstakingDurationInput = ({ data: { unstakingDuration }, register, setValue, - watch, + getValues, errors, }: DaoCreationVotingConfigItemInputProps<CreatorData>) => { const { t } = useTranslation() return ( <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput containerClassName="grow" error={errors?.unstakingDuration?.value} fieldName="unstakingDuration.value" + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" step={1} validation={[validatePositive, validateRequired]} - watch={watch} /> <SelectInput diff --git a/packages/stateful/creators/NftBased/getInstantiateInfo.ts b/packages/stateful/creators/NftBased/getInstantiateInfo.ts index 20ac2de0c..fce56a636 100644 --- a/packages/stateful/creators/NftBased/getInstantiateInfo.ts +++ b/packages/stateful/creators/NftBased/getInstantiateInfo.ts @@ -22,7 +22,7 @@ export const getInstantiateInfo: DaoCreatorGetInstantiateInfo<CreatorData> = ({ ? !activeThreshold.type || activeThreshold.type === 'percent' ? { percentage: { - percent: (activeThreshold.value / 100).toString(), + percent: (Number(activeThreshold.value) / 100).toString(), }, } : { diff --git a/packages/stateful/creators/TokenBased/GovernanceConfigurationInput.tsx b/packages/stateful/creators/TokenBased/GovernanceConfigurationInput.tsx index 4e1b0009e..ae580fdab 100644 --- a/packages/stateful/creators/TokenBased/GovernanceConfigurationInput.tsx +++ b/packages/stateful/creators/TokenBased/GovernanceConfigurationInput.tsx @@ -7,6 +7,7 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20BaseSelectors, genericTokenSelector, @@ -19,7 +20,7 @@ import { InputErrorMessage, InputLabel, Loader, - NumberInput, + NumericInput, SegmentedControls, TextInput, VotingPowerDistribution, @@ -37,7 +38,6 @@ import { TokenInfoResponse } from '@dao-dao/types/contracts/Cw20Base' import { DISTRIBUTION_COLORS, NEW_DAO_TOKEN_DECIMALS, - convertMicroDenomToDenomWithDecimals, formatPercentOf100, humanReadableList, isValidBech32Address, @@ -67,6 +67,7 @@ export const GovernanceConfigurationInput = ({ formState: { errors }, register, setValue, + getValues, setError, clearErrors, watch, @@ -277,7 +278,7 @@ export const GovernanceConfigurationInput = ({ const existingTokenSupply = existingGovernanceTokenSupply.state === 'hasValue' ? typeof existingGovernanceTokenSupply.contents === 'number' - ? BigInt(existingGovernanceTokenSupply.contents).toString() + ? HugeDecimal.from(existingGovernanceTokenSupply.contents).toString() : existingGovernanceTokenSupply.contents?.total_supply : undefined setValue('creator.data.existingTokenSupply', existingTokenSupply) @@ -530,7 +531,7 @@ export const GovernanceConfigurationInput = ({ <div className="flex grow flex-col"> <div className="flex grow flex-row items-center gap-2"> - <NumberInput + <NumericInput className="symbol-small-body-text font-mono leading-5 text-text-secondary" containerClassName="grow" error={errors.creator?.data?.newInfo?.maxSupply} @@ -538,17 +539,16 @@ export const GovernanceConfigurationInput = ({ ghost min={data.newInfo.initialSupply} register={register} - step={convertMicroDenomToDenomWithDecimals( - 1, + step={HugeDecimal.one.toHumanReadableNumber( NEW_DAO_TOKEN_DECIMALS )} validation={[ validatePositive, validateRequired, (maxSupply) => - (typeof maxSupply === 'number' && - maxSupply >= data.newInfo.initialSupply) || - t('error.maxSupplyMustBeAtLeastInitialSupply'), + HugeDecimal.from(maxSupply || 0).gte( + data.newInfo.initialSupply + ) || t('error.maxSupplyMustBeAtLeastInitialSupply'), ]} /> <p className="symbol-small-body-text font-mono leading-5 text-text-tertiary"> @@ -578,15 +578,14 @@ export const GovernanceConfigurationInput = ({ <div className="flex grow flex-col"> <div className="flex grow flex-row items-center gap-2"> - <NumberInput + <NumericInput className="symbol-small-body-text font-mono leading-5 text-text-secondary" containerClassName="grow" error={errors.creator?.data?.newInfo?.initialSupply} fieldName="creator.data.newInfo.initialSupply" ghost register={register} - step={convertMicroDenomToDenomWithDecimals( - 1, + step={HugeDecimal.one.toHumanReadableNumber( NEW_DAO_TOKEN_DECIMALS )} validation={[validatePositive, validateRequired]} @@ -613,7 +612,7 @@ export const GovernanceConfigurationInput = ({ <div className="flex grow flex-col"> <div className="flex grow flex-row items-center gap-2"> - <NumberInput + <NumericInput className="symbol-small-body-text font-mono leading-5 text-text-secondary" containerClassName="grow" error={ @@ -621,6 +620,9 @@ export const GovernanceConfigurationInput = ({ } fieldName="creator.data.newInfo.initialTreasuryPercent" ghost + max={100} + min={0} + numericValue register={register} step={0.0001} validation={[ @@ -685,6 +687,7 @@ export const GovernanceConfigurationInput = ({ control={control} data={data} errors={errors} + getValues={getValues} register={register} remove={ tierFields.length === 1 ? undefined : () => removeTier(idx) diff --git a/packages/stateful/creators/TokenBased/TierCard.tsx b/packages/stateful/creators/TokenBased/TierCard.tsx index 50c4a9447..f78d84559 100644 --- a/packages/stateful/creators/TokenBased/TierCard.tsx +++ b/packages/stateful/creators/TokenBased/TierCard.tsx @@ -2,6 +2,7 @@ import { Add, Close } from '@mui/icons-material' import { Control, FormState, + UseFormGetValues, UseFormRegister, UseFormSetValue, UseFormWatch, @@ -9,12 +10,13 @@ import { } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, IconButton, InputErrorMessage, InputLabel, - NumberInput, + NumericInput, TextInput, TooltipInfoIcon, useChain, @@ -25,6 +27,7 @@ import { NEW_DAO_TOKEN_DECIMALS, formatPercentOf100, makeValidateAddress, + validatePercent, validatePositive, validateRequired, } from '@dao-dao/utils' @@ -44,6 +47,7 @@ export interface TierCardProps { watch: UseFormWatch<NewDao<CreatorData>> errors: FormState<NewDao<CreatorData>>['errors'] setValue: UseFormSetValue<NewDao<CreatorData>> + getValues: UseFormGetValues<NewDao<CreatorData>> remove?: () => void } @@ -61,6 +65,7 @@ export const TierCard = ({ watch, errors, setValue, + getValues, showColorDotOnMember, } = props @@ -139,16 +144,17 @@ export const TierCard = ({ /> <div className="flex flex-row items-center gap-2"> - <NumberInput + <NumericInput containerClassName="grow" error={errors.creator?.data?.tiers?.[tierIndex]?.weight} fieldName={`creator.data.tiers.${tierIndex}.weight`} - min={1 / 10 ** NEW_DAO_TOKEN_DECIMALS} + getValues={getValues} + max={100} + min={0} + numericValue register={register} setValue={setValue} - step={1 / 10 ** NEW_DAO_TOKEN_DECIMALS} - validation={[validatePositive, validateRequired]} - watch={watch} + validation={[validatePercent, validatePositive, validateRequired]} /> <InputLabel name="%" /> </div> @@ -215,12 +221,16 @@ export const TierCard = ({ <TooltipInfoIcon title={t('info.tierMemberGovTokenAllocationTooltip', { - tokens: ( - (tierVotingWeight / members.length / 100) * - data.newInfo.initialSupply - ).toLocaleString(undefined, { - maximumFractionDigits: NEW_DAO_TOKEN_DECIMALS, - }), + tokens: HugeDecimal.fromHumanReadable( + data.newInfo.initialSupply, + NEW_DAO_TOKEN_DECIMALS + ) + .times(tierVotingWeight) + .div(members.length) + .div(100) + .toInternationalizedHumanReadableString({ + decimals: NEW_DAO_TOKEN_DECIMALS, + }), tokenSymbol: data.newInfo.symbol || t('info.tokens'), })} /> diff --git a/packages/stateful/creators/TokenBased/UnstakingDurationVotingConfigItem.tsx b/packages/stateful/creators/TokenBased/UnstakingDurationVotingConfigItem.tsx index f2251d807..fd447775e 100644 --- a/packages/stateful/creators/TokenBased/UnstakingDurationVotingConfigItem.tsx +++ b/packages/stateful/creators/TokenBased/UnstakingDurationVotingConfigItem.tsx @@ -6,7 +6,7 @@ import { contractQueries } from '@dao-dao/state/query' import { ClockEmoji, InputErrorMessage, - NumberInput, + NumericInput, SelectInput, SwitchCard, useHoldingKey, @@ -41,6 +41,7 @@ export const UnstakingDurationInput = ({ }, register, setValue, + getValues, watch, errors, }: DaoCreationVotingConfigItemInputProps<CreatorData>) => { @@ -127,17 +128,18 @@ export const UnstakingDurationInput = ({ </div> ) : ( <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput containerClassName="grow" error={errors?.unstakingDuration?.value} fieldName="unstakingDuration.value" + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" step={1} validation={[validatePositive, validateRequired]} - watch={watch} /> <SelectInput diff --git a/packages/stateful/creators/TokenBased/getInstantiateInfo.ts b/packages/stateful/creators/TokenBased/getInstantiateInfo.ts index 7d685641b..539512ebb 100644 --- a/packages/stateful/creators/TokenBased/getInstantiateInfo.ts +++ b/packages/stateful/creators/TokenBased/getInstantiateInfo.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { ChainId, DaoCreatorGetInstantiateInfo, @@ -7,8 +8,6 @@ import { ExecuteMsg as BtsgFtFactoryExecuteMsg } from '@dao-dao/types/contracts/ import { InitialBalance } from '@dao-dao/types/contracts/DaoVotingTokenStaked' import { NEW_DAO_TOKEN_DECIMALS, - convertDenomToMicroDenomStringWithDecimals, - convertDenomToMicroDenomWithDecimals, convertDurationWithUnitsToDuration, isSecretNetwork, } from '@dao-dao/utils' @@ -53,12 +52,17 @@ export const getInstantiateInfo: DaoCreatorGetInstantiateInfo<CreatorData> = ({ ? !activeThreshold.type || activeThreshold.type === 'percent' ? { percentage: { - percent: (activeThreshold.value / 100).toString(), + percent: (Number(activeThreshold.value) / 100).toString(), }, } : { absolute_count: { - count: BigInt(activeThreshold.value).toString(), + count: HugeDecimal.fromHumanReadable( + activeThreshold.value, + govTokenType === GovernanceTokenType.New + ? NEW_DAO_TOKEN_DECIMALS + : existingToken?.decimals ?? 0 + ).toString(), }, } : null, @@ -73,26 +77,31 @@ export const getInstantiateInfo: DaoCreatorGetInstantiateInfo<CreatorData> = ({ ({ weight, members }) => members.map(({ address }) => ({ address, - amount: convertDenomToMicroDenomStringWithDecimals( - // Governance Token-based DAOs distribute tier weights evenly - // amongst members. - (weight / members.length / 100) * initialSupply, + // Governance Token-based DAOs distribute tier weights evenly amongst + // members. + amount: HugeDecimal.fromHumanReadable( + initialSupply, NEW_DAO_TOKEN_DECIMALS - ), + ) + .times(weight) + .div(members.length) + .div(100) + .toFixed(0), })) ) // To prevent rounding issues, treasury balance becomes the remaining tokens // after the member weights are distributed. - const microInitialTreasuryBalance = BigInt( - convertDenomToMicroDenomWithDecimals( - initialSupply, - NEW_DAO_TOKEN_DECIMALS - ) - + const microInitialTreasuryBalance = HugeDecimal.fromHumanReadable( + initialSupply, + NEW_DAO_TOKEN_DECIMALS + ) + .minus( microInitialBalances.reduce( - (acc, { amount }) => acc + Number(amount), - 0 + (acc, { amount }) => acc.plus(amount), + HugeDecimal.zero ) - ).toString() + ) + .toString() // Secret Network only supports creating new CW20 DAOs (SNIP20). Native // tokens are supported on other chains. This should never happen, but just @@ -113,7 +122,7 @@ export const getInstantiateInfo: DaoCreatorGetInstantiateInfo<CreatorData> = ({ throw new Error('tokenCreationFactoryAddress not set') } - if (!maxSupply) { + if (!maxSupply || maxSupply === '0') { throw new Error('Max supply not set') } @@ -125,10 +134,10 @@ export const getInstantiateInfo: DaoCreatorGetInstantiateInfo<CreatorData> = ({ issue: { symbol: symbol.toLowerCase(), name, - max_supply: convertDenomToMicroDenomStringWithDecimals( + max_supply: HugeDecimal.fromHumanReadable( maxSupply, NEW_DAO_TOKEN_DECIMALS - ), + ).toString(), uri: metadataUrl || '', initial_balances: microInitialBalances, initial_dao_balance: microInitialTreasuryBalance, diff --git a/packages/stateful/creators/TokenBased/index.ts b/packages/stateful/creators/TokenBased/index.ts index f4f6b2987..8fad7e999 100644 --- a/packages/stateful/creators/TokenBased/index.ts +++ b/packages/stateful/creators/TokenBased/index.ts @@ -41,9 +41,9 @@ export const TokenBasedCreator: DaoCreator<CreatorData> = { selectedTokenType: tokenDaoType === 'both' ? TokenType.Native : tokenDaoType, newInfo: { - initialSupply: 10000000, + initialSupply: '10000000', initialTreasuryPercent: 90, - maxSupply: 100000000, + maxSupply: '100000000', symbol: '', name: '', }, @@ -55,7 +55,7 @@ export const TokenBasedCreator: DaoCreator<CreatorData> = { activeThreshold: { enabled: false, type: 'percent', - value: 10, + value: '10', }, }), governanceConfig: { diff --git a/packages/stateful/creators/TokenBased/types.ts b/packages/stateful/creators/TokenBased/types.ts index c7fec5294..e8d5ec287 100644 --- a/packages/stateful/creators/TokenBased/types.ts +++ b/packages/stateful/creators/TokenBased/types.ts @@ -21,10 +21,10 @@ export type CreatorData = { govTokenType: GovernanceTokenType selectedTokenType: TokenType newInfo: { - initialSupply: number + initialSupply: string initialTreasuryPercent: number // For BitSong which needs an up front max. - maxSupply?: number + maxSupply?: string imageUrl?: string // For Bitsong, which needs a JSON URL containing the image. metadataUrl?: string diff --git a/packages/stateful/hooks/useDaoClient.ts b/packages/stateful/hooks/useDaoClient.ts index 8331436a5..97cca280b 100644 --- a/packages/stateful/hooks/useDaoClient.ts +++ b/packages/stateful/hooks/useDaoClient.ts @@ -1,7 +1,7 @@ import { useQueryClient } from '@tanstack/react-query' import { useMemo } from 'react' -import { useDaoContextIfAvailable } from '@dao-dao/stateless' +import { useDaoIfAvailable } from '@dao-dao/stateless' import { DaoSource, IDaoBase } from '@dao-dao/types' import { getDao } from '../clients/dao' @@ -28,7 +28,7 @@ export const useDaoClient = ({ dao: daoSource, }: UseDaoClientOptions): UseDaoClientReturn => { const queryClient = useQueryClient() - const currentDao = useDaoContextIfAvailable()?.dao + const currentDao = useDaoIfAvailable() // Get DAO client. If matches current DAO context, use that one instead. const dao = useMemo( diff --git a/packages/stateful/hooks/useDaoGovernanceToken.ts b/packages/stateful/hooks/useDaoGovernanceToken.ts index 215ae4745..35f44a24c 100644 --- a/packages/stateful/hooks/useDaoGovernanceToken.ts +++ b/packages/stateful/hooks/useDaoGovernanceToken.ts @@ -1,6 +1,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' -import { useDaoContextIfAvailable } from '@dao-dao/stateless' +import { useDaoIfAvailable } from '@dao-dao/stateless' import { GenericToken } from '@dao-dao/types' /** @@ -9,7 +9,7 @@ import { GenericToken } from '@dao-dao/types' * context. Should never error. */ export const useDaoGovernanceToken = () => { - const dao = useDaoContextIfAvailable()?.dao + const dao = useDaoIfAvailable() return useSuspenseQuery<GenericToken | null>( dao?.maybeVotingModule?.getGovernanceTokenQuery?.() || { queryKey: ['null'], diff --git a/packages/stateful/hooks/useDaoProposalSinglePublishProposal.ts b/packages/stateful/hooks/useDaoProposalSinglePublishProposal.ts index da8ec7b7b..59ab9867d 100644 --- a/packages/stateful/hooks/useDaoProposalSinglePublishProposal.ts +++ b/packages/stateful/hooks/useDaoProposalSinglePublishProposal.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import { useDaoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { DaoProposalSingleAdapterId } from '@dao-dao/utils' import { @@ -16,10 +16,10 @@ import { PublishProposal } from '../proposal-module-adapter/adapters/DaoProposal export const useDaoProposalSinglePublishProposal = (): | PublishProposal | undefined => { - const { dao } = useDaoContext() + const dao = useDao() // Memoize hook getter since we don't want to create the hook more than once. - // `useDaoInfoContext` always returns the same instances of the data, so no + // `useDao` always returns the same instances of the data, so no // hook rules are violated here. const useProposalModule = useMemo(() => { const daoProposalSingleModule = dao.proposalModules.find( diff --git a/packages/stateful/hooks/useOnSecretNetworkPermitUpdate.ts b/packages/stateful/hooks/useOnSecretNetworkPermitUpdate.ts index 74bd105b4..329ae20f9 100644 --- a/packages/stateful/hooks/useOnSecretNetworkPermitUpdate.ts +++ b/packages/stateful/hooks/useOnSecretNetworkPermitUpdate.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { useDaoContextIfAvailable, useUpdatingRef } from '@dao-dao/stateless' +import { useDaoIfAvailable, useUpdatingRef } from '@dao-dao/stateless' import { DaoSource, PermitForPermitData } from '@dao-dao/types' import { serializeDaoSource } from '@dao-dao/utils' @@ -37,7 +37,7 @@ export const useOnSecretNetworkPermitUpdate = ({ reRender = true, callback, }: OnSecretNetworkPermitUpdateOptions = {}) => { - const currentDaoSource = useDaoContextIfAvailable()?.dao.source + const currentDaoSource = useDaoIfAvailable()?.source // Memoize callback into a ref. const callbackRef = useUpdatingRef(callback) diff --git a/packages/stateful/hooks/useProposalActionState.tsx b/packages/stateful/hooks/useProposalActionState.tsx index 18602f6fa..6b3c69452 100644 --- a/packages/stateful/hooks/useProposalActionState.tsx +++ b/packages/stateful/hooks/useProposalActionState.tsx @@ -10,7 +10,7 @@ import { ProposalStatusAndInfoProps, TextInput, useConfiguredChainContext, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { ChainId, @@ -60,7 +60,10 @@ export const useProposalActionState = ({ const { chain: { chain_id: chainId }, } = useConfiguredChainContext() - const { coreAddress, items } = useDaoInfoContext() + const { + coreAddress, + info: { items }, + } = useDao() const { options: { proposalNumber }, proposalModule, diff --git a/packages/stateful/hooks/useProposalRelayState.ts b/packages/stateful/hooks/useProposalRelayState.ts index 4a85cb7a4..d2275e20b 100644 --- a/packages/stateful/hooks/useProposalRelayState.ts +++ b/packages/stateful/hooks/useProposalRelayState.ts @@ -20,7 +20,7 @@ import { import { useCachedLoading, useCachedLoadingWithError, - useDaoInfoContext, + useDao, useSupportedChainContext, } from '@dao-dao/stateless' import { @@ -66,7 +66,7 @@ export const useProposalRelayState = ({ openSelfRelayExecute, loadingTxHash, }: UseProposalRelayStateOptions): UseProposalRelayStateReturn => { - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { chain: { chain_id: srcChainId }, } = useSupportedChainContext() diff --git a/packages/stateful/hooks/useProposalVetoState.tsx b/packages/stateful/hooks/useProposalVetoState.tsx index d81e40155..0a124d0f4 100644 --- a/packages/stateful/hooks/useProposalVetoState.tsx +++ b/packages/stateful/hooks/useProposalVetoState.tsx @@ -9,7 +9,7 @@ import { ProposalStatusAndInfoProps, Tooltip, useConfiguredChainContext, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -70,7 +70,7 @@ export const useProposalVetoState = ({ const { chain: { chain_id: chainId }, } = useConfiguredChainContext() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { proposalModule, proposalNumber } = useProposalModuleAdapterOptions() const { address: walletAddress = '', getSigningClient } = useWallet() diff --git a/packages/stateful/hooks/useTokenSwapStatusInfoForContract.ts b/packages/stateful/hooks/useTokenSwapStatusInfoForContract.ts index 37e03802e..0bd8c5ea2 100644 --- a/packages/stateful/hooks/useTokenSwapStatusInfoForContract.ts +++ b/packages/stateful/hooks/useTokenSwapStatusInfoForContract.ts @@ -1,11 +1,11 @@ import { useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { CwTokenSwapSelectors, genericTokenSelector, } from '@dao-dao/state/recoil' import { TokenSwapStatusProps, TokenType, WithChainId } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' import { EntityDisplay } from '../components' @@ -50,12 +50,11 @@ export const useTokenSwapStatusInfoForContract = ({ : selfParty.promise.native.denom, }) ) - const selfPartyAmount = convertMicroDenomToDenomWithDecimals( + const selfPartyAmount = HugeDecimal.from( 'cw20' in selfParty.promise ? selfParty.promise.cw20.amount - : selfParty.promise.native.amount, - selfPartyTokenInfo.decimals - ) + : selfParty.promise.native.amount + ).toHumanReadableNumber(selfPartyTokenInfo.decimals) const counterpartyTokenInfo = useRecoilValue( genericTokenSelector({ @@ -67,12 +66,11 @@ export const useTokenSwapStatusInfoForContract = ({ : counterparty.promise.native.denom, }) ) - const counterpartyAmount = convertMicroDenomToDenomWithDecimals( + const counterpartyAmount = HugeDecimal.from( 'cw20' in counterparty.promise ? counterparty.promise.cw20.amount - : counterparty.promise.native.amount, - counterpartyTokenInfo.decimals - ) + : counterparty.promise.native.amount + ).toHumanReadableNumber(counterpartyTokenInfo.decimals) const props: TokenSwapStatusProps = { selfParty: { diff --git a/packages/stateful/hooks/useWebSocket.ts b/packages/stateful/hooks/useWebSocket.ts index dbbc572f3..96fce4766 100644 --- a/packages/stateful/hooks/useWebSocket.ts +++ b/packages/stateful/hooks/useWebSocket.ts @@ -11,7 +11,7 @@ import { } from '@dao-dao/state/recoil' import { useCachedLoadingWithError, - useDaoInfoContext, + useDao, useUpdatingRef, } from '@dao-dao/stateless' import { ParametersExceptFirst } from '@dao-dao/types' @@ -243,6 +243,6 @@ export const useOnDaoWebSocketMessage = ( export const useOnCurrentDaoWebSocketMessage = ( ...args: ParametersExceptFirst<typeof useOnWebSocketMessage> ) => { - const { chainId, coreAddress } = useDaoInfoContext() + const { chainId, coreAddress } = useDao() return useOnDaoWebSocketMessage(chainId, coreAddress, ...args) } diff --git a/packages/stateful/jest.config.js b/packages/stateful/jest.config.js new file mode 100644 index 000000000..50200e3d7 --- /dev/null +++ b/packages/stateful/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config') diff --git a/packages/stateful/package.json b/packages/stateful/package.json index 701927edd..4212a9fd8 100644 --- a/packages/stateful/package.json +++ b/packages/stateful/package.json @@ -40,6 +40,7 @@ "@cosmos-kit/web3auth": "^2.8.0", "@cosmos-kit/xdefi": "^2.8.0", "@dao-dao/i18n": "2.5.0-rc.3", + "@dao-dao/math": "2.5.0-rc.3", "@dao-dao/state": "2.5.0-rc.3", "@dao-dao/stateless": "2.5.0-rc.3", "@dao-dao/utils": "2.5.0-rc.3", diff --git a/packages/stateful/proposal-module-adapter/README.md b/packages/stateful/proposal-module-adapter/README.md index a58b69b79..f8323c78f 100644 --- a/packages/stateful/proposal-module-adapter/README.md +++ b/packages/stateful/proposal-module-adapter/README.md @@ -101,9 +101,10 @@ view the voting configuration for each one: ```tsx import { matchAndLoadCommon } from '@dao-dao/stateful/proposal-module-adapter' +import { useDao } from '@dao-dao/stateless' export const DaoInfo = () => { - const { coreAddress, proposalModules } = useDaoInfoContext() + const { chainId, coreAddress, proposalModules } = useDao() const components = useMemo( () => diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx index 5870b4765..aee539e4d 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx @@ -19,7 +19,7 @@ export default { makeReactHookFormDecorator<UpdatePreProposeConfigData>({ depositRequired: true, depositInfo: { - amount: Math.pow(10, 6), + amount: Math.pow(10, 6).toString(), type: 'native', denomOrAddress: '', token: undefined, diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx index 88927bb6e..762ca871c 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx @@ -1,6 +1,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { AddressInput, FormSwitchCard, @@ -21,7 +22,6 @@ import { TokenType, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, getChainAssets, getNativeTokenForChainId, isValidBech32Address, @@ -34,7 +34,7 @@ const DepositRefundPolicyValues = Object.values(DepositRefundPolicy) export interface UpdatePreProposeConfigData { depositRequired: boolean depositInfo: { - amount: number + amount: string // Token input fields. type: 'native' | 'cw20' | 'voting_module_token' denomOrAddress: string @@ -64,7 +64,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< const { chain: { chain_id: chainId, bech32_prefix: bech32Prefix }, } = useActionOptions() - const { register, setValue, watch } = + const { register, setValue, getValues, watch } = useFormContext<UpdatePreProposeConfigData>() const depositRequired = watch( @@ -98,6 +98,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< chainId, type: TokenType.Cw20, denomOrAddress: 'other_cw20', + decimals: depositInfo.token?.decimals ?? 0, symbol: (depositInfo.type === TokenType.Cw20 && depositInfo.token?.symbol) || t('form.cw20Token'), @@ -124,8 +125,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< token.denomOrAddress === depositInfo.denomOrAddress) ) - const minimumDeposit = convertMicroDenomToDenomWithDecimals( - 1, + const minimumDeposit = HugeDecimal.one.toHumanReadableNumber( depositInfo.token?.decimals ?? 0 ) @@ -161,6 +161,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'depositInfo.amount') as 'depositInfo.amount', diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/index.tsx index e46b604d2..15756973c 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdatePreProposeConfig/index.tsx @@ -3,6 +3,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { contractQueries, daoPreProposeMultipleQueries, @@ -29,8 +30,6 @@ import { } from '@dao-dao/types/contracts/DaoPreProposeMultiple' import { DAO_PRE_PROPOSE_MULTIPLE_CONTRACT_NAMES, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, getNativeTokenForChainId, isFeatureSupportedByVersion, isValidBech32Address, @@ -156,7 +155,7 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< const config = await this.options.queryClient.fetchQuery( daoPreProposeMultipleQueries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.prePropose.address, }) ) @@ -166,7 +165,7 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< const token = config.deposit_info ? await this.options.queryClient.fetchQuery( tokenQueries.info(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, type: 'native' in config.deposit_info.denom ? TokenType.Native @@ -186,10 +185,9 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< const depositInfo: UpdatePreProposeConfigData['depositInfo'] = config.deposit_info ? { - amount: convertMicroDenomToDenomWithDecimals( - config.deposit_info.amount, - token?.decimals ?? 0 - ), + amount: HugeDecimal.from( + config.deposit_info.amount + ).toHumanReadableString(token?.decimals ?? 0), type: isVotingModuleToken ? 'voting_module_token' : 'native' in config.deposit_info.denom @@ -204,10 +202,10 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< refundPolicy: config.deposit_info.refund_policy, } : { - amount: 1, + amount: '1', type: 'native', denomOrAddress: getNativeTokenForChainId( - this.proposalModule.dao.chainId + this.proposalModule.chainId ).denomOrAddress, token: undefined, refundPolicy: DepositRefundPolicy.OnlyPassed, @@ -242,10 +240,10 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< update_config: { deposit_info: depositRequired ? { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( depositInfo.amount, depositInfo.token?.decimals ?? 0 - ), + ).toString(), denom: depositInfo.type === 'voting_module_token' ? { @@ -301,7 +299,7 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< } return makeExecuteSmartContractMessage({ - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.prePropose.address, sender: this.options.address, msg: updateConfigMessage, @@ -326,7 +324,7 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< }, }, }) && - chainId === this.proposalModule.dao.chainId && + chainId === this.proposalModule.chainId && (await this.options.queryClient.fetchQuery( contractQueries.isContract(this.options.queryClient, { chainId, @@ -389,7 +387,7 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< return { depositRequired: false, depositInfo: { - amount: 1, + amount: '1', type: 'native', denomOrAddress: getNativeTokenForChainId(chainId).denomOrAddress, refundPolicy: DepositRefundPolicy.OnlyPassed, @@ -406,8 +404,7 @@ export class DaoProposalMultipleUpdatePreProposeConfigAction extends ActionBase< : 'cw20' const depositInfo: UpdatePreProposeConfigData['depositInfo'] = { - amount: convertMicroDenomToDenomWithDecimals( - configDepositInfo.amount, + amount: HugeDecimal.from(configDepositInfo.amount).toHumanReadableString( token.decimals ), type, diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/UpdateProposalConfigComponent.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/UpdateProposalConfigComponent.tsx index 617a5615b..19e4932d4 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/UpdateProposalConfigComponent.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/UpdateProposalConfigComponent.tsx @@ -8,7 +8,7 @@ import { FormSwitchCard, InputErrorMessage, KeyEmoji, - NumberInput, + NumericInput, PeopleEmoji, ProposalVetoConfigurer, RecycleEmoji, @@ -67,7 +67,7 @@ export const UpdateProposalConfigComponent: ActionComponent< }, }) => { const { t } = useTranslation() - const { register, setValue, watch } = + const { register, setValue, getValues, watch } = useFormContext<UpdateProposalConfigData>() const onlyMembersExecute = watch( @@ -99,18 +99,24 @@ export const UpdateProposalConfigComponent: ActionComponent< <div className="flex grow flex-row flex-wrap gap-2"> {percentageQuorumSelected && ( <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.quorumPercentage} fieldName={ (fieldNamePrefix + 'quorumPercentage') as 'quorumPercentage' } - min={1} + getValues={getValues} + max={100} + min={0} + numericValue register={register} setValue={setValue} sizing="sm" - validation={[validateRequired, validatePercent]} - watch={watch} + validation={[ + validateRequired, + validatePercent, + validatePositive, + ]} /> <InputErrorMessage error={errors?.quorumPercentage} /> </div> @@ -139,14 +145,16 @@ export const UpdateProposalConfigComponent: ActionComponent< </div> <div className="flex grow flex-row flex-wrap gap-2"> <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.votingDuration?.value} fieldName={ (fieldNamePrefix + 'votingDuration.value') as 'votingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" @@ -161,7 +169,6 @@ export const UpdateProposalConfigComponent: ActionComponent< value >= 60 || t('error.mustBeAtLeastSixtySeconds'), ]} - watch={watch} /> <InputErrorMessage error={errors?.proposalDuration} /> </div> diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/index.tsx index 26f15a757..d054458bb 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/actions/UpdateProposalConfig/index.tsx @@ -180,7 +180,7 @@ export class DaoProposalMultipleUpdateConfigAction extends ActionBase<UpdateProp async setup() { const config = await this.options.queryClient.fetchQuery( daoProposalMultipleQueries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, }) ) @@ -193,7 +193,7 @@ export class DaoProposalMultipleUpdateConfigAction extends ActionBase<UpdateProp cw1WhitelistExtraQueries.adminsIfCw1Whitelist( this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, address: config.veto.vetoer, } ) @@ -214,7 +214,7 @@ export class DaoProposalMultipleUpdateConfigAction extends ActionBase<UpdateProp async encode(data: UpdateProposalConfigData): Promise<UnifiedCosmosMsg> { const config = await this.options.queryClient.fetchQuery( daoProposalMultipleQueries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, }) ) @@ -251,7 +251,7 @@ export class DaoProposalMultipleUpdateConfigAction extends ActionBase<UpdateProp } return makeExecuteSmartContractMessage({ - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, sender: this.options.address, msg: updateConfigMessage, @@ -288,7 +288,7 @@ export class DaoProposalMultipleUpdateConfigAction extends ActionBase<UpdateProp }, }, }) && - chainId === this.proposalModule.dao.chainId && + chainId === this.proposalModule.chainId && decodedMessage.wasm.execute.contract_addr === this.proposalModule.address ) } @@ -306,7 +306,7 @@ export class DaoProposalMultipleUpdateConfigAction extends ActionBase<UpdateProp cw1WhitelistExtraQueries.adminsIfCw1Whitelist( this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, address: config.veto.vetoer, } ) diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/components/NewProposal.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/components/NewProposal.tsx index e8822a291..373e38229 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/components/NewProposal.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/components/NewProposal.tsx @@ -16,7 +16,7 @@ import { useActionsContext, useCachedLoadable, useChain, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { BaseNewProposalProps, IProposalModuleBase } from '@dao-dao/types' import { @@ -56,9 +56,8 @@ export const NewProposal = ({ name: daoName, imageUrl: daoImageUrl, coreAddress, - isActive, - activeThreshold, - } = useDaoInfoContext() + info: { isActive, activeThreshold }, + } = useDao() const { isWalletConnecting, isWalletConnected, getStargateClient } = useWallet() diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/hooks/makeUsePublishProposal.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/hooks/makeUsePublishProposal.ts index 818ccf033..a41206407 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/hooks/makeUsePublishProposal.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/common/hooks/makeUsePublishProposal.ts @@ -4,6 +4,7 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20BaseSelectors, nativeDenomBalanceSelector } from '@dao-dao/state' import { useCachedLoadable } from '@dao-dao/stateless' import { @@ -60,13 +61,15 @@ export const makeUsePublishProposal = 'native' in depositInfo.contents.denom ? depositInfo.contents.denom.native : undefined - const requiredProposalDeposit = Number( - depositInfo.valueMaybe()?.amount ?? '0' + const requiredProposalDeposit = HugeDecimal.from( + depositInfo.valueMaybe()?.amount ?? 0 ) // For checking allowance and increasing if necessary. const cw20DepositTokenAllowanceResponseLoadable = useCachedLoadable( - depositInfoCw20TokenAddress && requiredProposalDeposit && walletAddress + depositInfoCw20TokenAddress && + requiredProposalDeposit.isPositive() && + walletAddress ? Cw20BaseSelectors.allowanceSelector({ chainId, contractAddress: depositInfoCw20TokenAddress, @@ -87,7 +90,9 @@ export const makeUsePublishProposal = : undefined const cw20DepositTokenBalanceLoadable = useCachedLoadable( - requiredProposalDeposit && walletAddress && depositInfoCw20TokenAddress + requiredProposalDeposit.isPositive() && + walletAddress && + depositInfoCw20TokenAddress ? Cw20BaseSelectors.balanceSelector({ chainId, contractAddress: depositInfoCw20TokenAddress, @@ -101,7 +106,9 @@ export const makeUsePublishProposal = : undefined const nativeDepositTokenBalanceLoadable = useCachedLoadable( - requiredProposalDeposit && walletAddress && depositInfoNativeTokenDenom + requiredProposalDeposit.isPositive() && + walletAddress && + depositInfoNativeTokenDenom ? nativeDenomBalanceSelector({ chainId, walletAddress, @@ -117,16 +124,15 @@ export const makeUsePublishProposal = // True if deposit is needed and cannot be paid. const depositUnsatisfied = // Requires deposit. - requiredProposalDeposit > 0 && + requiredProposalDeposit.isPositive() && // Has cw20 deposit and insufficient balance. ((!!depositInfoCw20TokenAddress && (!cw20DepositTokenBalance || - Number(cw20DepositTokenBalance.balance) < requiredProposalDeposit)) || + requiredProposalDeposit.gt(cw20DepositTokenBalance.balance))) || // Has native deposit and insufficient balance. (!!depositInfoNativeTokenDenom && (!nativeDepositTokenBalance || - Number(nativeDepositTokenBalance.amount) < - requiredProposalDeposit))) + requiredProposalDeposit.gt(nativeDepositTokenBalance.amount)))) const increaseCw20DepositAllowance = Cw20BaseHooks.useIncreaseAllowance({ contractAddress: depositInfoCw20TokenAddress ?? '', @@ -256,22 +262,22 @@ export const makeUsePublishProposal = throw new Error(t('error.loadingData')) } - const remainingAllowanceNeeded = - requiredProposalDeposit - + const remainingAllowanceNeeded = requiredProposalDeposit.minus( // If allowance expired, none. - (expirationExpired( + expirationExpired( cw20DepositTokenAllowanceResponse.expires, (await (await getSigningClient()).getBlock()).header.height ) ? 0 - : Number(cw20DepositTokenAllowanceResponse.allowance)) + : cw20DepositTokenAllowanceResponse.allowance + ) // Request to increase the contract's allowance for the proposal // deposit if needed. if (remainingAllowanceNeeded) { try { await increaseCw20DepositAllowance({ - amount: BigInt(remainingAllowanceNeeded).toString(), + amount: remainingAllowanceNeeded.toString(), spender: // If pre-propose address set, give that one deposit allowance // instead of proposal module. @@ -296,7 +302,7 @@ export const makeUsePublishProposal = const proposeFunds = requiredProposalDeposit && depositInfoNativeTokenDenom ? coins( - BigInt(requiredProposalDeposit).toString(), + requiredProposalDeposit.toString(), depositInfoNativeTokenDenom ) : undefined diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalStatusAndInfo.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalStatusAndInfo.tsx index 9686b8d6e..06921b23e 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalStatusAndInfo.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalStatusAndInfo.tsx @@ -23,7 +23,7 @@ import { Tooltip, TooltipTruncatedText, useConfiguredChainContext, - useDaoInfoContext, + useDao, useExecuteAt, useTranslatedTimeDeltaFormatter, } from '@dao-dao/stateless' @@ -109,7 +109,7 @@ const InnerProposalStatusAndInfo = ({ chain: { chain_id: chainId }, config: { explorerUrlTemplates }, } = useConfiguredChainContext() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { proposalModule, proposalNumber } = useProposalModuleAdapterOptions() const config = useRecoilValue( @@ -389,7 +389,7 @@ const InnerProposalStatusAndInfoLoader = ( props: BaseProposalStatusAndInfoProps ) => { const { t } = useTranslation() - const { name: daoName, coreAddress } = useDaoInfoContext() + const { name: daoName, coreAddress } = useDao() const LoaderP: ComponentType<{ className: string }> = ({ className }) => ( <p className={clsx('animate-pulse', className)}>...</p> diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalVotes/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalVotes/index.tsx index f33030687..3a0279bef 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalVotes/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/components/ProposalVotes/index.tsx @@ -2,6 +2,7 @@ import uniqBy from 'lodash.uniqby' import { useEffect, useState } from 'react' import { useRecoilCallback } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { DaoProposalMultipleSelectors } from '@dao-dao/state' import { ProposalVote, @@ -33,8 +34,8 @@ export const ProposalVotes = (props: BaseProposalVotesProps) => { const voteOptions = useLoadingVoteOptions() const totalPower = loadingProposal.loading - ? 0 - : Number(loadingProposal.data.total_power) + ? HugeDecimal.zero + : HugeDecimal.from(loadingProposal.data.total_power) const [loading, setLoading] = useState(true) const [noMoreVotes, setNoMoreVotes] = useState(false) @@ -79,8 +80,12 @@ export const ProposalVotes = (props: BaseProposalVotesProps) => { ({ vote, voter, power, rationale, votedAt }): ProposalVote => ({ voterAddress: voter, vote, - votingPowerPercent: - totalPower === 0 ? 0 : (Number(power) / totalPower) * 100, + votingPowerPercent: totalPower.isZero() + ? 0 + : HugeDecimal.from(power) + .div(totalPower) + .times(100) + .toNumber(), rationale, votedAt: votedAt ? new Date(votedAt) : undefined, }) @@ -150,8 +155,9 @@ export const ProposalVotes = (props: BaseProposalVotesProps) => { ({ vote, voter, power, rationale, votedAt }): ProposalVote => ({ voterAddress: voter, vote, - votingPowerPercent: - totalPower === 0 ? 0 : (Number(power) / totalPower) * 100, + votingPowerPercent: totalPower.isZero() + ? 0 + : HugeDecimal.from(power).div(totalPower).times(100).toNumber(), rationale, votedAt: votedAt ? new Date(votedAt) : undefined, }) diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts index d2076bf35..3acef33a5 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/daoCreation/getInstantiateInfo.ts @@ -1,7 +1,7 @@ +import { HugeDecimal } from '@dao-dao/math' import { DaoCreationGetInstantiateInfo, TokenType } from '@dao-dao/types' import { TokenBasedCreatorId, - convertDenomToMicroDenomStringWithDecimals, convertDurationWithUnitsToDuration, convertVetoConfigToCosmos, isSecretNetwork, @@ -60,10 +60,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< ...commonConfig, deposit: proposalDeposit.enabled ? { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( proposalDeposit.amount, proposalDeposit.token?.decimals ?? 0 - ), + ).toString(), denom: proposalDeposit.type === 'voting_module_token' ? { @@ -103,10 +103,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< ...commonConfig, deposit: proposalDeposit.enabled ? { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( proposalDeposit.amount, proposalDeposit.token?.decimals ?? 0 - ), + ).toString(), denom: proposalDeposit.type === 'voting_module_token' ? { diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalDaoInfoCards.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalDaoInfoCards.tsx index a5d78192f..6961fa07f 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalDaoInfoCards.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalDaoInfoCards.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' import { constSelector } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw1WhitelistSelectors, DaoProposalMultipleSelectors, @@ -18,7 +19,6 @@ import { } from '@dao-dao/types' import { convertDurationToHumanReadableString, - convertMicroDenomToDenomWithDecimals, isFeatureSupportedByVersion, } from '@dao-dao/utils' @@ -35,7 +35,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { const config = useCachedLoadingWithError( DaoProposalMultipleSelectors.configSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: proposalModule.address, }) ) @@ -45,7 +45,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { ? undefined : !depositInfo.errored && depositInfo.data ? genericTokenSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, type: 'native' in depositInfo.data.denom ? TokenType.Native @@ -73,7 +73,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { : config.errored || !('veto' in config.data) || !config.data.veto ? constSelector(undefined) : Cw1WhitelistSelectors.adminsIfCw1Whitelist({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: config.data.veto.vetoer, }) ) @@ -151,10 +151,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { '<error>' ) : depositInfo.data && depositTokenInfo.data ? ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - depositInfo.data.amount, - depositTokenInfo.data.decimals - )} + amount={HugeDecimal.from(depositInfo.data.amount)} decimals={depositTokenInfo.data.decimals} iconUrl={depositTokenInfo.data.imageUrl} showFullAmount diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalRefreshers.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalRefreshers.ts index 0fc01e69e..93aac5d20 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalRefreshers.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/hooks/useProposalRefreshers.ts @@ -33,7 +33,7 @@ export const useProposalRefreshers = (): ProposalRefreshers => { // Invalidate indexer query first. queryClient.invalidateQueries({ queryKey: indexerQueries.queryContract(queryClient, { - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: proposalModule.address, formula: 'daoProposalMultiple/vote', args: { diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/index.tsx index 4e62385bb..1118a5302 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalMultiple/index.tsx @@ -52,7 +52,7 @@ export const DaoProposalMultipleAdapter: ProposalModuleAdapter< loadCommon: ({ proposalModule }) => { // Make here so we can pass into common hooks and components that need it. const depositInfoSelector = makeDepositInfoSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, version: proposalModule.version, preProposeAddress: proposalModule.prePropose?.address ?? null, @@ -84,19 +84,19 @@ export const DaoProposalMultipleAdapter: ProposalModuleAdapter< // Selectors selectors: { proposalCount: proposalCountSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, }), reverseProposalInfos: (props) => reverseProposalInfosSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, proposalModulePrefix: proposalModule.prefix, ...props, }), depositInfo: depositInfoSelector, maxVotingPeriod: maxVotingPeriodSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, }), }, diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx index 4b773b752..d47e12754 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.stories.tsx @@ -19,7 +19,7 @@ export default { makeReactHookFormDecorator<UpdatePreProposeConfigData>({ depositRequired: true, depositInfo: { - amount: Math.pow(10, 6), + amount: Math.pow(10, 6).toString(), type: 'native', denomOrAddress: '', token: undefined, diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx index 507ce9e40..0691ffd48 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/UpdatePreProposeConfigComponent.tsx @@ -1,6 +1,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { AddressInput, FormSwitchCard, @@ -21,7 +22,6 @@ import { TokenType, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, getChainAssets, getNativeTokenForChainId, isValidBech32Address, @@ -34,7 +34,7 @@ const DepositRefundPolicyValues = Object.values(DepositRefundPolicy) export interface UpdatePreProposeConfigData { depositRequired: boolean depositInfo: { - amount: number + amount: string // Token input fields. type: 'native' | 'cw20' | 'voting_module_token' denomOrAddress: string @@ -65,7 +65,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< chain: { chain_id: chainId, bech32_prefix: bech32Prefix }, } = useActionOptions() - const { register, setValue, watch } = + const { register, setValue, getValues, watch } = useFormContext<UpdatePreProposeConfigData>() const depositRequired = watch( @@ -99,6 +99,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< chainId, type: TokenType.Cw20, denomOrAddress: 'other_cw20', + decimals: depositInfo.token?.decimals ?? 0, symbol: (depositInfo.type === TokenType.Cw20 && depositInfo.token?.symbol) || t('form.cw20Token'), @@ -125,8 +126,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< token.denomOrAddress === depositInfo.denomOrAddress) ) - const minimumDeposit = convertMicroDenomToDenomWithDecimals( - 1, + const minimumDeposit = HugeDecimal.one.toHumanReadableNumber( depositInfo.token?.decimals ?? 0 ) @@ -162,6 +162,7 @@ export const UpdatePreProposeConfigComponent: ActionComponent< amount={{ watch, setValue, + getValues, register, fieldName: (fieldNamePrefix + 'depositInfo.amount') as 'depositInfo.amount', diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/index.tsx index ae015c2fe..b5cbbd0d6 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdatePreProposeConfig/index.tsx @@ -3,6 +3,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { contractQueries, genericTokenSelector, @@ -30,8 +31,6 @@ import { import { ContractName, DAO_PRE_PROPOSE_SINGLE_CONTRACT_NAMES, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, getNativeTokenForChainId, isFeatureSupportedByVersion, isValidBech32Address, @@ -165,7 +164,7 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up const config = await this.options.queryClient.fetchQuery( daoPreProposeSingleQueries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.prePropose.address, }) ) @@ -175,7 +174,7 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up const token = config.deposit_info ? await this.options.queryClient.fetchQuery( tokenQueries.info(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, type: 'native' in config.deposit_info.denom ? TokenType.Native @@ -195,10 +194,9 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up const depositInfo: UpdatePreProposeConfigData['depositInfo'] = config.deposit_info ? { - amount: convertMicroDenomToDenomWithDecimals( - config.deposit_info.amount, - token?.decimals ?? 0 - ), + amount: HugeDecimal.from( + config.deposit_info.amount + ).toHumanReadableString(token?.decimals ?? 0), type: isVotingModuleToken ? 'voting_module_token' : 'native' in config.deposit_info.denom @@ -213,10 +211,10 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up refundPolicy: config.deposit_info.refund_policy, } : { - amount: 1, + amount: '1', type: 'native', denomOrAddress: getNativeTokenForChainId( - this.proposalModule.dao.chainId + this.proposalModule.chainId ).denomOrAddress, token: undefined, refundPolicy: DepositRefundPolicy.OnlyPassed, @@ -251,10 +249,10 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up update_config: { deposit_info: depositRequired ? { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( depositInfo.amount, depositInfo.token?.decimals ?? 0 - ), + ).toString(), denom: depositInfo.type === 'voting_module_token' ? { @@ -310,7 +308,7 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up } return makeExecuteSmartContractMessage({ - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.prePropose.address, sender: this.options.address, msg: updateConfigMessage, @@ -335,7 +333,7 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up }, }, }) && - chainId === this.proposalModule.dao.chainId && + chainId === this.proposalModule.chainId && (await this.options.queryClient.fetchQuery( contractQueries.isContract(this.options.queryClient, { chainId, @@ -398,7 +396,7 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up return { depositRequired: false, depositInfo: { - amount: 1, + amount: '1', type: 'native', denomOrAddress: getNativeTokenForChainId(chainId).denomOrAddress, refundPolicy: DepositRefundPolicy.OnlyPassed, @@ -415,8 +413,7 @@ export class DaoProposalSingleUpdatePreProposeConfigAction extends ActionBase<Up : 'cw20' const depositInfo: UpdatePreProposeConfigData['depositInfo'] = { - amount: convertMicroDenomToDenomWithDecimals( - configDepositInfo.amount, + amount: HugeDecimal.from(configDepositInfo.amount).toHumanReadableString( token.decimals ), type, diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.stories.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.stories.tsx index 39d3f5fe9..bb4b5dc45 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.stories.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.stories.tsx @@ -17,7 +17,7 @@ export default { onlyMembersExecute: true, depositRequired: true, depositInfo: { - deposit: 123, + deposit: '123', refundFailedProposals: true, }, thresholdType: '%', diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.tsx index 13e0d6c45..a17d2621f 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/UpdateProposalConfigComponent.tsx @@ -1,6 +1,7 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { ChartEmoji, ClockEmoji, @@ -8,7 +9,7 @@ import { InputErrorMessage, KeyEmoji, MoneyEmoji, - NumberInput, + NumericInput, PeopleEmoji, RecycleEmoji, SelectInput, @@ -31,7 +32,7 @@ export type UpdateProposalConfigData = { depositRequired: boolean depositInfo?: { - deposit: number + deposit: string refundFailedProposals: boolean } @@ -60,7 +61,7 @@ export const UpdateProposalConfigComponent: ActionComponent< options: { commonGovernanceTokenInfo }, }) => { const { t } = useTranslation() - const { register, setValue, watch } = + const { register, setValue, watch, getValues } = useFormContext<UpdateProposalConfigData>() const onlyMembersExecute = watch( @@ -118,16 +119,21 @@ export const UpdateProposalConfigComponent: ActionComponent< {depositRequired && ( <div className="flex flex-col gap-1"> <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.depositInfo?.deposit} fieldName={ (fieldNamePrefix + 'depositInfo.deposit') as 'depositInfo.deposit' } - min={1 / 10 ** commonGovernanceTokenInfo.decimals} + getValues={getValues} + min={HugeDecimal.one.toHumanReadableNumber( + commonGovernanceTokenInfo.decimals + )} register={register} - step={1 / 10 ** commonGovernanceTokenInfo.decimals} + step={HugeDecimal.one.toHumanReadableNumber( + commonGovernanceTokenInfo.decimals + )} unit={'$' + commonGovernanceTokenInfo.symbol} validation={[validateRequired, validatePositive]} /> @@ -166,19 +172,25 @@ export const UpdateProposalConfigComponent: ActionComponent< <div className="flex grow flex-row flex-wrap gap-2"> {percentageThresholdSelected && ( <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.thresholdPercentage} fieldName={ (fieldNamePrefix + 'thresholdPercentage') as 'thresholdPercentage' } - min={1} + getValues={getValues} + max={100} + min={0} + numericValue register={register} setValue={setValue} sizing="sm" - validation={[validateRequired, validatePercent]} - watch={watch} + validation={[ + validateRequired, + validatePercent, + validatePositive, + ]} /> <InputErrorMessage error={errors?.thresholdPercentage} /> </div> @@ -222,18 +234,24 @@ export const UpdateProposalConfigComponent: ActionComponent< <div className="flex grow flex-row flex-wrap gap-2"> {percentageQuorumSelected && ( <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.quorumPercentage} fieldName={ (fieldNamePrefix + 'quorumPercentage') as 'quorumPercentage' } - min={1} + getValues={getValues} + max={100} + min={0} + numericValue register={register} setValue={setValue} sizing="sm" - validation={[validateRequired, validatePercent]} - watch={watch} + validation={[ + validateRequired, + validatePercent, + validatePositive, + ]} /> <InputErrorMessage error={errors?.quorumPercentage} /> </div> @@ -264,14 +282,16 @@ export const UpdateProposalConfigComponent: ActionComponent< </div> <div className="flex grow flex-row flex-wrap gap-2"> <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.proposalDuration} fieldName={ (fieldNamePrefix + 'votingDuration.value') as 'votingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" @@ -286,7 +306,6 @@ export const UpdateProposalConfigComponent: ActionComponent< value >= 60 || t('error.mustBeAtLeastSixtySeconds'), ]} - watch={watch} /> <InputErrorMessage error={errors?.proposalDuration} /> </div> diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/index.tsx index 421661892..979362ac4 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV1/index.tsx @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { cwProposalSingleV1Queries, tokenQueries } from '@dao-dao/state' import { ActionBase, BallotDepositEmoji } from '@dao-dao/stateless' import { @@ -12,10 +13,8 @@ import { } from '@dao-dao/types' import { Threshold } from '@dao-dao/types/contracts/DaoProposalSingle.common' import { - convertDenomToMicroDenomStringWithDecimals, convertDurationToDurationWithUnits, convertDurationWithUnitsToDuration, - convertMicroDenomToDenomWithDecimals, makeExecuteSmartContractMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -122,7 +121,7 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp async setup() { const config = await this.options.queryClient.fetchQuery( cwProposalSingleV1Queries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, }) ) @@ -130,7 +129,7 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp const token = config.deposit_info ? await this.options.queryClient.fetchQuery( tokenQueries.info(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, type: TokenType.Cw20, denomOrAddress: config.deposit_info.token, }) @@ -142,16 +141,14 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp const depositInfo = config.deposit_info && token ? { - deposit: convertMicroDenomToDenomWithDecimals( - Number(config.deposit_info.deposit), - // A deposit being configured implies that a token will be - // present. - token.decimals - ), + // A deposit being configured implies that a token will be present. + deposit: HugeDecimal.from( + config.deposit_info.deposit + ).toHumanReadableString(token.decimals), refundFailedProposals: config.deposit_info.refund_failed_proposals, } : { - deposit: 0, + deposit: '0', refundFailedProposals: false, } @@ -171,7 +168,7 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp async encode(data: UpdateProposalConfigData): Promise<UnifiedCosmosMsg> { return makeExecuteSmartContractMessage({ - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, sender: this.options.address, msg: { @@ -206,7 +203,7 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp ...(data.depositInfo && data.depositRequired && { deposit_info: { - deposit: convertDenomToMicroDenomStringWithDecimals( + deposit: HugeDecimal.fromHumanReadable( data.depositInfo.deposit, this.proposalModule.dao.votingModule.getGovernanceTokenQuery ? ( @@ -215,7 +212,7 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp ) ).decimals : 0 - ), + ).toString(), refund_failed_proposals: data.depositInfo.refundFailedProposals, token: { voting_module_token: {} }, }, @@ -249,7 +246,7 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp }, }, }) && - chainId === this.proposalModule.dao.chainId && + chainId === this.proposalModule.chainId && decodedMessage.wasm.execute.contract_addr === this.proposalModule.address ) } @@ -263,8 +260,9 @@ export class DaoProposalSingleV1UpdateConfigAction extends ActionBase<UpdateProp const depositRequired = !!config.deposit_info const depositInfo = config.deposit_info ? { - deposit: convertMicroDenomToDenomWithDecimals( - Number(config.deposit_info.deposit), + deposit: HugeDecimal.from( + config.deposit_info.deposit + ).toHumanReadableString( this.proposalModule.dao.votingModule.getGovernanceTokenQuery ? ( await this.options.queryClient.fetchQuery( diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/UpdateProposalConfigComponent.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/UpdateProposalConfigComponent.tsx index 56335db17..ab6934781 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/UpdateProposalConfigComponent.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/UpdateProposalConfigComponent.tsx @@ -9,7 +9,7 @@ import { FormSwitchCard, InputErrorMessage, KeyEmoji, - NumberInput, + NumericInput, PeopleEmoji, ProposalVetoConfigurer, RecycleEmoji, @@ -72,7 +72,7 @@ export const UpdateProposalConfigComponent: ActionComponent< }, }) => { const { t } = useTranslation() - const { register, setValue, watch } = + const { register, setValue, getValues, watch } = useFormContext<UpdateProposalConfigData>() const onlyMembersExecute = watch( @@ -111,19 +111,24 @@ export const UpdateProposalConfigComponent: ActionComponent< <div className="flex grow flex-row flex-wrap gap-2"> {percentageThresholdSelected && ( <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.thresholdPercentage} fieldName={ (fieldNamePrefix + 'thresholdPercentage') as 'thresholdPercentage' } - min={1} + getValues={getValues} + max={100} + min={0} register={register} setValue={setValue} sizing="sm" - validation={[validateRequired, validatePercent]} - watch={watch} + validation={[ + validateRequired, + validatePercent, + validatePositive, + ]} /> <InputErrorMessage error={errors?.thresholdPercentage} /> </div> @@ -165,18 +170,23 @@ export const UpdateProposalConfigComponent: ActionComponent< <div className="flex grow flex-row flex-wrap gap-2"> {percentageQuorumSelected && ( <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.quorumPercentage} fieldName={ (fieldNamePrefix + 'quorumPercentage') as 'quorumPercentage' } - min={1} + max={100} + min={0} + numericValue register={register} setValue={setValue} sizing="sm" - validation={[validateRequired, validatePercent]} - watch={watch} + validation={[ + validateRequired, + validatePercent, + validatePositive, + ]} /> <InputErrorMessage error={errors?.quorumPercentage} /> </div> @@ -207,14 +217,16 @@ export const UpdateProposalConfigComponent: ActionComponent< </div> <div className="flex grow flex-row flex-wrap gap-2"> <div className="flex flex-col gap-1"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.proposalDuration} fieldName={ (fieldNamePrefix + 'votingDuration.value') as 'votingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="sm" @@ -229,7 +241,6 @@ export const UpdateProposalConfigComponent: ActionComponent< value >= 60 || t('error.mustBeAtLeastSixtySeconds'), ]} - watch={watch} /> <InputErrorMessage error={errors?.proposalDuration} /> </div> diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/index.tsx index 1fa03982b..0362d2b0b 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/actions/UpdateProposalConfigV2/index.tsx @@ -214,7 +214,7 @@ export class DaoProposalSingleV2UpdateConfigAction extends ActionBase<UpdateProp async setup() { const config = await this.options.queryClient.fetchQuery( daoProposalSingleV2Queries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, }) ) @@ -227,7 +227,7 @@ export class DaoProposalSingleV2UpdateConfigAction extends ActionBase<UpdateProp cw1WhitelistExtraQueries.adminsIfCw1Whitelist( this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, address: config.veto.vetoer, } ) @@ -248,7 +248,7 @@ export class DaoProposalSingleV2UpdateConfigAction extends ActionBase<UpdateProp async encode(data: UpdateProposalConfigData): Promise<UnifiedCosmosMsg> { const config = await this.options.queryClient.fetchQuery( daoProposalSingleV2Queries.config(this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, }) ) @@ -298,7 +298,7 @@ export class DaoProposalSingleV2UpdateConfigAction extends ActionBase<UpdateProp } return makeExecuteSmartContractMessage({ - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, contractAddress: this.proposalModule.address, sender: this.options.address, msg: updateConfigMessage, @@ -329,7 +329,7 @@ export class DaoProposalSingleV2UpdateConfigAction extends ActionBase<UpdateProp }, }, }) && - chainId === this.proposalModule.dao.chainId && + chainId === this.proposalModule.chainId && decodedMessage.wasm.execute.contract_addr === this.proposalModule.address ) } @@ -347,7 +347,7 @@ export class DaoProposalSingleV2UpdateConfigAction extends ActionBase<UpdateProp cw1WhitelistExtraQueries.adminsIfCw1Whitelist( this.options.queryClient, { - chainId: this.proposalModule.dao.chainId, + chainId: this.proposalModule.chainId, address: config.veto.vetoer, } ) diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/components/NewProposal.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/components/NewProposal.tsx index 2a565a7cf..35c58d039 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/components/NewProposal.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/components/NewProposal.tsx @@ -16,7 +16,7 @@ import { useActionsContext, useCachedLoadable, useChain, - useDaoInfoContext, + useDao, useProcessTQ, } from '@dao-dao/stateless' import { BaseNewProposalProps, IProposalModuleBase } from '@dao-dao/types' @@ -55,9 +55,8 @@ export const NewProposal = ({ name: daoName, imageUrl: daoImageUrl, coreAddress, - isActive, - activeThreshold, - } = useDaoInfoContext() + info: { isActive, activeThreshold }, + } = useDao() const { isWalletConnecting, isWalletConnected, getStargateClient } = useWallet() diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/hooks/makeUsePublishProposal.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/hooks/makeUsePublishProposal.ts index 2f3d470f3..3d2605c58 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/hooks/makeUsePublishProposal.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/common/hooks/makeUsePublishProposal.ts @@ -4,6 +4,7 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValueLoadable } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20BaseSelectors, nativeDenomBalanceSelector } from '@dao-dao/state' import { useCachedLoadable } from '@dao-dao/stateless' import { @@ -59,13 +60,15 @@ export const makeUsePublishProposal = 'native' in depositInfo.contents.denom ? depositInfo.contents.denom.native : undefined - const requiredProposalDeposit = Number( - depositInfo.valueMaybe()?.amount ?? '0' + const requiredProposalDeposit = HugeDecimal.from( + depositInfo.valueMaybe()?.amount ?? 0 ) // For checking allowance and increasing if necessary. const cw20DepositTokenAllowanceResponseLoadable = useCachedLoadable( - depositInfoCw20TokenAddress && requiredProposalDeposit && walletAddress + depositInfoCw20TokenAddress && + requiredProposalDeposit.isPositive() && + walletAddress ? Cw20BaseSelectors.allowanceSelector({ chainId, contractAddress: depositInfoCw20TokenAddress, @@ -86,7 +89,9 @@ export const makeUsePublishProposal = : undefined const cw20DepositTokenBalanceLoadable = useCachedLoadable( - requiredProposalDeposit && walletAddress && depositInfoCw20TokenAddress + requiredProposalDeposit.isPositive() && + walletAddress && + depositInfoCw20TokenAddress ? Cw20BaseSelectors.balanceSelector({ chainId, contractAddress: depositInfoCw20TokenAddress, @@ -100,7 +105,9 @@ export const makeUsePublishProposal = : undefined const nativeDepositTokenBalanceLoadable = useCachedLoadable( - requiredProposalDeposit && walletAddress && depositInfoNativeTokenDenom + requiredProposalDeposit.isPositive() && + walletAddress && + depositInfoNativeTokenDenom ? nativeDenomBalanceSelector({ chainId, walletAddress, @@ -116,16 +123,15 @@ export const makeUsePublishProposal = // True if deposit is needed and cannot be paid. const depositUnsatisfied = // Requires deposit. - requiredProposalDeposit > 0 && + requiredProposalDeposit.isPositive() && // Has cw20 deposit and insufficient balance. ((!!depositInfoCw20TokenAddress && (!cw20DepositTokenBalance || - Number(cw20DepositTokenBalance.balance) < requiredProposalDeposit)) || + requiredProposalDeposit.gt(cw20DepositTokenBalance.balance))) || // Has native deposit and insufficient balance. (!!depositInfoNativeTokenDenom && (!nativeDepositTokenBalance || - Number(nativeDepositTokenBalance.amount) < - requiredProposalDeposit))) + requiredProposalDeposit.gt(nativeDepositTokenBalance.amount)))) const increaseCw20DepositAllowance = Cw20BaseHooks.useIncreaseAllowance({ contractAddress: depositInfoCw20TokenAddress ?? '', @@ -227,22 +233,22 @@ export const makeUsePublishProposal = throw new Error(t('error.loadingData')) } - const remainingAllowanceNeeded = - requiredProposalDeposit - + const remainingAllowanceNeeded = requiredProposalDeposit.minus( // If allowance expired, none. - (expirationExpired( + expirationExpired( cw20DepositTokenAllowanceResponse.expires, (await (await getSigningClient()).getBlock()).header.height ) ? 0 - : Number(cw20DepositTokenAllowanceResponse.allowance)) + : cw20DepositTokenAllowanceResponse.allowance + ) // Request to increase the contract's allowance for the proposal // deposit if needed. if (remainingAllowanceNeeded) { try { await increaseCw20DepositAllowance({ - amount: BigInt(remainingAllowanceNeeded).toString(), + amount: remainingAllowanceNeeded.toString(), spender: // If pre-propose address set, give that one deposit allowance // instead of proposal module. @@ -267,7 +273,7 @@ export const makeUsePublishProposal = const proposeFunds = requiredProposalDeposit && depositInfoNativeTokenDenom ? coins( - BigInt(requiredProposalDeposit).toString(), + requiredProposalDeposit.toString(), depositInfoNativeTokenDenom ) : undefined diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/PreProposeApprovalProposalStatusAndInfo.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/PreProposeApprovalProposalStatusAndInfo.tsx index 3ad6a845a..914a40fa0 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/PreProposeApprovalProposalStatusAndInfo.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/PreProposeApprovalProposalStatusAndInfo.tsx @@ -18,7 +18,7 @@ import { ProposalStatusAndInfoProps, ProposalStatusAndInfo as StatelessProposalStatusAndInfo, Tooltip, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -69,7 +69,7 @@ const InnerPreProposeApprovalProposalStatusAndInfo = ({ proposal: PreProposeApprovalProposalWithMeteadata }) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { proposalModule: { prefix, prePropose }, diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalInnerContentDisplay.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalInnerContentDisplay.tsx index b16c2ac60..b23be1498 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalInnerContentDisplay.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalInnerContentDisplay.tsx @@ -8,7 +8,7 @@ import { ActionsMatchAndRender, Button, RawActionsRenderer, - useDaoInfoContext, + useDao, } from '@dao-dao/stateless' import { ActionKeyAndData, @@ -52,7 +52,7 @@ const InnerProposalInnerContentDisplay = ({ }) => { const { t } = useTranslation() const [showRaw, setShowRaw] = useState(false) - const { chainId, coreVersion } = useDaoInfoContext() + const { chainId, coreVersion } = useDao() const actionMessagesToDisplay = useMemo(() => { let messages = proposal.msgs diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfo.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfo.tsx index a5290e495..4d27cbe2a 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfo.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfo.tsx @@ -22,7 +22,7 @@ import { ProposalStatusAndInfo as StatelessProposalStatusAndInfo, Tooltip, useConfiguredChainContext, - useDaoInfoContext, + useDao, useDaoNavHelpers, useExecuteAt, useTranslatedTimeDeltaFormatter, @@ -123,7 +123,7 @@ const InnerProposalStatusAndInfo = ({ chain: { chain_id: chainId }, config: { explorerUrlTemplates }, } = useConfiguredChainContext() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { proposalModule, proposalNumber } = useProposalModuleAdapterOptions() diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfoLoader.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfoLoader.tsx index 4b5051d8c..0a0e411be 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfoLoader.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalStatusAndInfoLoader.tsx @@ -11,7 +11,7 @@ import { Logo, ProposalStatusAndInfoProps, ProposalStatusAndInfo as StatelessProposalStatusAndInfo, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { BaseProposalStatusAndInfoProps } from '@dao-dao/types' @@ -22,7 +22,7 @@ export const ProposalStatusAndInfoLoader = ( props: Pick<BaseProposalStatusAndInfoProps, 'inline'> ) => { const { t } = useTranslation() - const { name: daoName, coreAddress } = useDaoInfoContext() + const { name: daoName, coreAddress } = useDao() const { getDaoPath } = useDaoNavHelpers() const LoaderP: ComponentType<{ className: string }> = ({ className }) => ( diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalVotes/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalVotes/index.tsx index 52a063309..14ffa572d 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalVotes/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/components/ProposalVotes/index.tsx @@ -2,6 +2,7 @@ import uniqBy from 'lodash.uniqby' import { useEffect, useState } from 'react' import { useRecoilCallback } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { DaoProposalSingleCommonSelectors } from '@dao-dao/state' import { ProposalVote, @@ -28,8 +29,8 @@ export const ProposalVotes = (props: BaseProposalVotesProps) => { const loadingProposal = useLoadingProposal() const totalPower = loadingProposal.loading - ? 0 - : Number(loadingProposal.data.total_power) + ? HugeDecimal.zero + : HugeDecimal.from(loadingProposal.data.total_power) const [loading, setLoading] = useState(true) const [noMoreVotes, setNoMoreVotes] = useState(false) @@ -74,8 +75,12 @@ export const ProposalVotes = (props: BaseProposalVotesProps) => { ({ vote, voter, power, rationale, votedAt }): ProposalVote => ({ voterAddress: voter, vote, - votingPowerPercent: - totalPower === 0 ? 0 : (Number(power) / totalPower) * 100, + votingPowerPercent: totalPower.isZero() + ? 0 + : HugeDecimal.from(power) + .div(totalPower) + .times(100) + .toNumber(), rationale, votedAt: votedAt ? new Date(votedAt) : undefined, }) @@ -145,8 +150,9 @@ export const ProposalVotes = (props: BaseProposalVotesProps) => { ({ vote, voter, power, rationale, votedAt }): ProposalVote => ({ voterAddress: voter, vote, - votingPowerPercent: - totalPower === 0 ? 0 : (Number(power) / totalPower) * 100, + votingPowerPercent: totalPower.isZero() + ? 0 + : HugeDecimal.from(power).div(totalPower).times(100).toNumber(), rationale, votedAt: votedAt ? new Date(votedAt) : undefined, }) diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/ThresholdVotingConfigItem.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/ThresholdVotingConfigItem.tsx index 0e0c7ccf2..231a7dbc1 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/ThresholdVotingConfigItem.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/ThresholdVotingConfigItem.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { BallotDepositEmoji, FormSwitchCard, - NumberInput, + NumericInput, SelectInput, } from '@dao-dao/stateless' import { @@ -13,6 +13,7 @@ import { } from '@dao-dao/types' import { formatPercentOf100, + validatePercent, validatePositive, validateRequired, } from '@dao-dao/utils' @@ -26,7 +27,7 @@ const ThresholdInput = ({ }, register, setValue, - watch, + getValues, errors, }: DaoCreationVotingConfigItemInputProps<DaoCreationExtraVotingConfig>) => { const { t } = useTranslation() @@ -35,17 +36,18 @@ const ThresholdInput = ({ <div className="flex flex-col gap-4"> <div className="flex flex-row gap-2"> {!majority && ( - <NumberInput + <NumericInput containerClassName="grow min-w-[8rem]" error={errors?.threshold?.value} fieldName="threshold.value" - min={1} + getValues={getValues} + max={100} + min={0} + numericValue register={register} setValue={setValue} sizing="sm" - step={0.001} - validation={[validatePositive, validateRequired]} - watch={watch} + validation={[validatePercent, validatePositive, validateRequired]} /> )} diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts index 715ee00c8..1214bc393 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/daoCreation/getInstantiateInfo.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { DaoCreationGetInstantiateInfo, PercentOrMajorityValue, @@ -6,7 +7,6 @@ import { import { PercentageThreshold } from '@dao-dao/types/contracts/DaoProposalSingle.common' import { TokenBasedCreatorId, - convertDenomToMicroDenomStringWithDecimals, convertDurationWithUnitsToDuration, convertVetoConfigToCosmos, isSecretNetwork, @@ -79,10 +79,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< ...commonConfig, deposit: proposalDeposit.enabled ? { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( proposalDeposit.amount, proposalDeposit.token?.decimals ?? 0 - ), + ).toString(), denom: proposalDeposit.type === 'voting_module_token' ? { @@ -120,10 +120,10 @@ export const getInstantiateInfo: DaoCreationGetInstantiateInfo< ...commonConfig, deposit: proposalDeposit.enabled ? { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( proposalDeposit.amount, proposalDeposit.token?.decimals ?? 0 - ), + ).toString(), denom: proposalDeposit.type === 'voting_module_token' ? { diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalDaoInfoCards.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalDaoInfoCards.tsx index 68c48fcca..8645d79dc 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalDaoInfoCards.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalDaoInfoCards.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' import { constSelector } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw1WhitelistSelectors, DaoProposalSingleCommonSelectors, @@ -19,7 +20,6 @@ import { } from '@dao-dao/types' import { convertDurationToHumanReadableString, - convertMicroDenomToDenomWithDecimals, isFeatureSupportedByVersion, } from '@dao-dao/utils' @@ -35,7 +35,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { const config = useCachedLoadingWithError( DaoProposalSingleCommonSelectors.configSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: proposalModule.address, }) ) @@ -45,7 +45,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { ? undefined : !depositInfo.errored && depositInfo.data ? genericTokenSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, type: 'native' in depositInfo.data.denom ? TokenType.Native @@ -73,7 +73,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { : config.errored || !('veto' in config.data) || !config.data.veto ? constSelector(undefined) : Cw1WhitelistSelectors.adminsIfCw1Whitelist({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: config.data.veto.vetoer, }) ) @@ -167,10 +167,7 @@ export const useProposalDaoInfoCards = (): DaoInfoCard[] => { '<error>' ) : depositInfo.data && depositTokenInfo.data ? ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - depositInfo.data.amount, - depositTokenInfo.data.decimals - )} + amount={HugeDecimal.from(depositInfo.data.amount)} decimals={depositTokenInfo.data.decimals} iconUrl={depositTokenInfo.data.imageUrl} showFullAmount diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalRefreshers.ts b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalRefreshers.ts index 23d70233c..8247ae9d0 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalRefreshers.ts +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/hooks/useProposalRefreshers.ts @@ -38,7 +38,7 @@ export const useProposalRefreshers = (): ProposalRefreshers => { // Invalidate indexer query first. queryClient.invalidateQueries({ queryKey: indexerQueries.queryContract(queryClient, { - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: proposalModule.address, formula: 'daoProposalSingle/vote', args: { @@ -65,7 +65,7 @@ export const useProposalRefreshers = (): ProposalRefreshers => { !isPreProposeApprovalProposal ? DaoProposalSingleCommonSelectors.proposalSelector({ contractAddress: proposalModule.address, - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, params: [ { proposalId: proposalNumber, @@ -79,7 +79,7 @@ export const useProposalRefreshers = (): ProposalRefreshers => { const loadingPreProposeApprovalProposal = useCachedLoading( isPreProposeApprovalProposal && proposalModule.prePropose ? DaoPreProposeApprovalSingleSelectors.queryExtensionSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, contractAddress: proposalModule.prePropose.address, params: [ { diff --git a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/index.tsx b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/index.tsx index 7501c680f..0e6be0de0 100644 --- a/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/index.tsx +++ b/packages/stateful/proposal-module-adapter/adapters/DaoProposalSingle/index.tsx @@ -63,7 +63,7 @@ export const DaoProposalSingleAdapter: ProposalModuleAdapter< loadCommon: ({ proposalModule }) => { // Make here so we can pass into common hooks and components that need it. const depositInfoSelector = makeDepositInfoSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, version: proposalModule.version, preProposeAddress: proposalModule.prePropose?.address ?? null, @@ -97,12 +97,12 @@ export const DaoProposalSingleAdapter: ProposalModuleAdapter< // Selectors selectors: { proposalCount: proposalCountSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, }), reverseProposalInfos: (props) => reverseProposalInfosSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, proposalModulePrefix: proposalModule.prefix, ...props, @@ -112,14 +112,14 @@ export const DaoProposalSingleAdapter: ProposalModuleAdapter< ? { reversePreProposePendingProposalInfos: (props) => reversePreProposePendingProposalInfosSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.prePropose!.address, proposalModulePrefix: proposalModule.prefix, ...props, }), reversePreProposeCompletedProposalInfos: (props) => reversePreProposeCompletedProposalInfosSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.prePropose!.address, proposalModulePrefix: proposalModule.prefix, ...props, @@ -127,7 +127,7 @@ export const DaoProposalSingleAdapter: ProposalModuleAdapter< } : {}), maxVotingPeriod: maxVotingPeriodSelector({ - chainId: proposalModule.dao.chainId, + chainId: proposalModule.chainId, proposalModuleAddress: proposalModule.address, }), }, diff --git a/packages/stateful/proposal-module-adapter/react/provider.tsx b/packages/stateful/proposal-module-adapter/react/provider.tsx index 5d545bc1c..da5100a55 100644 --- a/packages/stateful/proposal-module-adapter/react/provider.tsx +++ b/packages/stateful/proposal-module-adapter/react/provider.tsx @@ -1,6 +1,6 @@ import { ReactNode, useMemo } from 'react' -import { useDaoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { IProposalModuleCommonContext, IProposalModuleContext, @@ -25,7 +25,7 @@ export const ProposalModuleAdapterProvider = ({ proposalId, children, }: ProposalModuleAdapterProviderProps) => { - const { dao } = useDaoContext() + const dao = useDao() const { context, commonContext } = useMemo(() => { const context = matchAndLoadAdapter(dao, proposalId) const commonContext = commonContextFromAdapterContext(context) @@ -54,7 +54,7 @@ export const ProposalModuleAdapterCommonProvider = ({ proposalModuleAddress, children, }: ProposalModuleAdapterCommonProviderProps) => { - const { dao } = useDaoContext() + const dao = useDao() const context = useMemo( () => matchAndLoadCommonContext(dao, proposalModuleAddress), [dao, proposalModuleAddress] diff --git a/packages/stateful/voting-module-adapter/README.md b/packages/stateful/voting-module-adapter/README.md index 4a2df1085..8a169977a 100644 --- a/packages/stateful/voting-module-adapter/README.md +++ b/packages/stateful/voting-module-adapter/README.md @@ -141,52 +141,3 @@ const MyVotingModuleAdapter: VotingModuleAdapter = { }), } ``` - -There's one more thing to be aware of when writing adapters... the -`useVotingModuleAdapterOptions` hook! - -### **useVotingModuleAdapterOptions** - -This hook simply provides the `options` passed to the -`VotingModuleAdapterProvider`, so you can easily access the `coreAddress` as -well as other common info instead of needing to manually pass them around. - -Example: - -<details> -<summary>`DaoVotingCw4/hooks/useMainDaoInfoCards.ts`</summary> - -```tsx -import { PeopleAltOutlined } from '@mui/icons-material' -import { useTranslation } from 'react-i18next' - -import { DaoInfoBarItem } from '@dao-dao/stateless' - -// IMPORT HOOK: -import { useVotingModuleAdapterOptions } from '../../../react/context' -// OR: -// import { useVotingModuleAdapterOptions } from '@dao-dao/stateful/voting-module-adapter/react/context' - -import { useVotingModule } from './useVotingModule' - -export const useMainDaoInfoCards = (): DaoInfoBarItem[] => { - const { t } = useTranslation() - // USE HOOK TO GET `coreAddress` FROM OPTIONS: - const { coreAddress } = useVotingModuleAdapterOptions() - const { members } = useVotingModule(coreAddress, { fetchMembers: true }) - - if (!members) { - throw new Error(t('error.loadingData')) - } - - return [ - { - Icon: PeopleAltOutlined, - label: t('title.members'), - value: members.length, - }, - ] -} -``` - -</details> diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.stories.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.stories.tsx index 6c557d73f..7bca77534 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.stories.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.stories.tsx @@ -12,7 +12,7 @@ export default { component: MintComponent, decorators: [ makeReactHookFormDecorator<MintData>({ - amount: 100000, + amount: '100000', to: '', }), ], diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.tsx index b17506f45..29e9aa498 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/MintComponent.tsx @@ -6,9 +6,10 @@ import clsx from 'clsx' import { ComponentType } from 'react' import { useFormContext } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' import { InputErrorMessage, - NumberInput, + NumericInput, useActionOptions, useDetectWrap, } from '@dao-dao/stateless' @@ -25,13 +26,13 @@ import { export type MintData = { to: string - amount: number + amount: string } export type MintOptions = { govToken: GenericToken // Used to display the profile of the address receiving minted tokens. - AddressInput: ComponentType<AddressInputProps> + AddressInput: ComponentType<AddressInputProps<MintData>> } export const MintComponent: ActionComponent<MintOptions> = ({ @@ -43,7 +44,7 @@ export const MintComponent: ActionComponent<MintOptions> = ({ const { chain: { bech32_prefix: bech32Prefix }, } = useActionOptions() - const { register, watch, setValue } = useFormContext() + const { register, setValue, getValues } = useFormContext<MintData>() const { containerRef, childRef, wrapped } = useDetectWrap() const Icon = wrapped ? SubdirectoryArrowRightRounded : ArrowRightAltRounded @@ -54,19 +55,19 @@ export const MintComponent: ActionComponent<MintOptions> = ({ className="flex flex-row flex-wrap items-stretch gap-x-3 gap-y-2" ref={containerRef} > - <NumberInput + <NumericInput containerClassName="w-full sm:w-auto" disabled={!isCreating} error={errors?.amount} - fieldName={fieldNamePrefix + 'amount'} - min={1 / 10 ** govToken.decimals} + fieldName={(fieldNamePrefix + 'amount') as 'amount'} + getValues={getValues} + min={HugeDecimal.one.toHumanReadableNumber(govToken.decimals)} register={register} setValue={setValue} sizing="none" - step={1 / 10 ** govToken.decimals} + step={HugeDecimal.one.toHumanReadableNumber(govToken.decimals)} unit={'$' + govToken.symbol} validation={[validateRequired, validatePositive]} - watch={watch} /> <div @@ -83,7 +84,7 @@ export const MintComponent: ActionComponent<MintOptions> = ({ containerClassName="grow" disabled={!isCreating} error={errors?.to} - fieldName={fieldNamePrefix + 'to'} + fieldName={(fieldNamePrefix + 'to') as 'to'} register={register} validation={[validateRequired, makeValidateAddress(bech32Prefix)]} /> diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/index.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/index.tsx index edba8cf84..0b5d6988f 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/index.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/Mint/index.tsx @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { ActionBase, HerbEmoji } from '@dao-dao/stateless' import { GenericToken, TokenType, UnifiedCosmosMsg } from '@dao-dao/types' import { @@ -9,8 +10,6 @@ import { ProcessedMessage, } from '@dao-dao/types/actions' import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, makeExecuteSmartContractMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -51,7 +50,7 @@ export class MintAction extends ActionBase<MintData> { this.defaults = { to: options.address, - amount: 1, + amount: '1', } } @@ -80,10 +79,10 @@ export class MintAction extends ActionBase<MintData> { contractAddress: this.governanceToken.denomOrAddress, msg: { mint: { - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( amount, this.governanceToken.decimals - ), + ).toString(), recipient: to, }, }, @@ -114,10 +113,9 @@ export class MintAction extends ActionBase<MintData> { return { to: decodedMessage.wasm.execute.msg.mint.recipient, - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.wasm.execute.msg.mint.amount, - this.governanceToken.decimals - ), + amount: HugeDecimal.from( + decodedMessage.wasm.execute.msg.mint.amount + ).toHumanReadableString(this.governanceToken.decimals), } } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/Component.tsx index 9e2fa6f60..b6fdbdd12 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/Component.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/Component.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { FormSwitchCard, InputErrorMessage, - NumberInput, + NumericInput, SelectInput, } from '@dao-dao/stateless' import { @@ -26,7 +26,7 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ isCreating, }) => { const { t } = useTranslation() - const { register, watch, setValue } = + const { register, watch, setValue, getValues } = useFormContext<UpdateStakingConfigData>() const unstakingDurationEnabled = watch( @@ -57,14 +57,16 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ {unstakingDurationEnabled && ( <> <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.unstakingDuration?.value} fieldName={ (fieldNamePrefix + 'unstakingDuration.value') as 'unstakingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="md" @@ -77,7 +79,6 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ : undefined } validation={[validatePositive, validateRequired]} - watch={watch} /> {isCreating && ( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/README.md index 389b34430..639d6f65f 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/actions/UpdateStakingConfig/README.md @@ -17,7 +17,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). { "unstakingDurationEnabled": <true | false>, "unstakingDuration": { - "value": "<NUMBER>", + "value": <NUMBER>, "units": "<seconds | minutes | hours | days | weeks | months | years>" } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/MembersTab.tsx index 0915b37bb..fe5b2267d 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/MembersTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/MembersTab.tsx @@ -1,10 +1,13 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { indexerQueries } from '@dao-dao/state/query' -import { MembersTab as StatelessMembersTab } from '@dao-dao/stateless' +import { + MembersTab as StatelessMembersTab, + useVotingModule, +} from '@dao-dao/stateless' import { StatefulDaoMemberCardProps } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' import { ButtonLink, @@ -12,19 +15,18 @@ import { EntityDisplay, } from '../../../../components' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo } from '../hooks/useGovernanceTokenInfo' export const MembersTab = () => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { governanceToken } = useGovernanceTokenInfo() const queryClient = useQueryClient() const members = useQueryLoadingDataWithError( indexerQueries.queryContract(queryClient, { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, formula: 'daoVotingCw20Staked/topStakers', noFallback: true, }), @@ -41,10 +43,7 @@ export const MembersTab = () => { loading: false, data: { token: governanceToken, - amount: convertMicroDenomToDenomWithDecimals( - balance, - governanceToken.decimals - ), + amount: HugeDecimal.from(balance), }, }, votingPowerPercent: { diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx index e2b90a8f6..b9cb93c25 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx @@ -3,17 +3,14 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { constSelector, useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20StakeSelectors, blockHeightSelector, blocksPerYearSelector, stakingLoadingAtom, } from '@dao-dao/state' -import { - useCachedLoadable, - useChain, - useDaoInfoContext, -} from '@dao-dao/stateless' +import { useCachedLoadable, useChain, useDao } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps, UnstakingTask, @@ -21,7 +18,6 @@ import { } from '@dao-dao/types' import { convertExpirationToDate, - convertMicroDenomToDenomWithDecimals, durationToSeconds, processError, } from '@dao-dao/utils' @@ -42,7 +38,7 @@ export const ProfileCardMemberInfo = ({ }: BaseProfileCardMemberInfoProps) => { const { t } = useTranslation() const { chain_id: chainId } = useChain() - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const { address: walletAddress, isWalletConnected, @@ -64,7 +60,7 @@ export const ProfileCardMemberInfo = ({ refreshTotals, claimsPending, claimsAvailable, - sumClaimsAvailable, + sumClaimsAvailable = HugeDecimal.zero, loadingWalletStakedValue, loadingTotalStakedValue, refreshClaims, @@ -111,7 +107,7 @@ export const ProfileCardMemberInfo = ({ if (!isWalletConnected) { return toast.error(t('error.logInToContinue')) } - if (!sumClaimsAvailable) { + if (sumClaimsAvailable.isZero()) { return toast.error(t('error.noClaimsAvailable')) } @@ -135,12 +131,12 @@ export const ProfileCardMemberInfo = ({ refreshClaims?.() toast.success( - `Claimed ${convertMicroDenomToDenomWithDecimals( - sumClaimsAvailable, - governanceToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.claimedTokens', { + amount: sumClaimsAvailable.toInternationalizedHumanReadableString({ + decimals: governanceToken.decimals, + }), + tokenSymbol: governanceToken.symbol, + }) ) } catch (err) { console.error(err) @@ -179,10 +175,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsPending ?? []).map(({ amount, release_at }) => ({ token: governanceToken, status: UnstakingTaskStatus.Unstaking, - amount: convertMicroDenomToDenomWithDecimals( - amount, - governanceToken.decimals - ), + amount: HugeDecimal.from(amount), date: blocksPerYearLoadable.state === 'hasValue' ? convertExpirationToDate( @@ -197,10 +190,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsAvailable ?? []).map(({ amount, release_at }) => ({ token: governanceToken, status: UnstakingTaskStatus.ReadyToClaim, - amount: convertMicroDenomToDenomWithDecimals( - amount, - governanceToken.decimals - ), + amount: HugeDecimal.from(amount), date: blocksPerYearLoadable.state === 'hasValue' ? convertExpirationToDate( @@ -232,14 +222,8 @@ export const ProfileCardMemberInfo = ({ data: [ { token: governanceToken, - staked: convertMicroDenomToDenomWithDecimals( - loadingWalletStakedValue.data, - governanceToken.decimals - ), - unstaked: convertMicroDenomToDenomWithDecimals( - loadingUnstakedBalance.data, - governanceToken.decimals - ), + staked: loadingWalletStakedValue.data, + unstaked: loadingUnstakedBalance.data, }, ], } @@ -252,10 +236,10 @@ export const ProfileCardMemberInfo = ({ ? { loading: true } : { loading: false, - data: - (loadingWalletStakedValue.data / - loadingTotalStakedValue.data) * - 100, + data: loadingWalletStakedValue.data + .div(loadingTotalStakedValue.data) + .times(100) + .toNumber(), } } onClaim={onClaim} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx index f3fd63e06..e601b3c92 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx @@ -9,6 +9,7 @@ import { waitForAll, } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20StakeSelectors, refreshDaoVotingPowerAtom, @@ -17,18 +18,12 @@ import { } from '@dao-dao/state' import { ModalLoader, - StakingMode, StakingModal as StatelessStakingModal, useCachedLoadable, + useVotingModule, } from '@dao-dao/stateless' -import { BaseStakingModalProps } from '@dao-dao/types' -import { - convertDenomToMicroDenomStringWithDecimals, - convertDenomToMicroDenomWithDecimals, - convertMicroDenomToDenomWithDecimals, - encodeJsonToBase64, - processError, -} from '@dao-dao/utils' +import { BaseStakingModalProps, StakingMode } from '@dao-dao/types' +import { encodeJsonToBase64, processError } from '@dao-dao/utils' import { SuspenseLoader } from '../../../../components' import { @@ -38,7 +33,6 @@ import { useAwaitNextBlock, useWallet, } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo, useStakingInfo } from '../hooks' export const StakingModal = (props: BaseStakingModalProps) => ( @@ -61,7 +55,7 @@ const InnerStakingModal = ({ isWalletConnected, refreshBalances, } = useWallet() - const { chainId, coreAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const [stakingLoading, setStakingLoading] = useRecoilState(stakingLoadingAtom) @@ -73,7 +67,7 @@ const InnerStakingModal = ({ stakingContractAddress, unstakingDuration, refreshTotals, - sumClaimsAvailable, + sumClaimsAvailable = HugeDecimal.zero, loadingWalletStakedValue, refreshClaims, } = useStakingInfo({ @@ -85,16 +79,16 @@ const InnerStakingModal = ({ useRecoilValue( waitForAll([ Cw20StakeSelectors.isOraichainProxySnapshotContractSelector({ - chainId, + chainId: votingModule.chainId, contractAddress: stakingContractAddress, }), Cw20StakeSelectors.totalStakedAtHeightSelector({ - chainId, + chainId: votingModule.chainId, contractAddress: stakingContractAddress, params: [{}], }), Cw20StakeSelectors.totalValueSelector({ - chainId, + chainId: votingModule.chainId, contractAddress: stakingContractAddress, params: [], }), @@ -104,7 +98,7 @@ const InnerStakingModal = ({ const oraichainCw20StakingConfig = useRecoilValue( isOraichainCustomStaking ? Cw20StakeSelectors.oraichainProxySnapshotConfigSelector({ - chainId, + chainId: votingModule.chainId, contractAddress: stakingContractAddress, }) : constSelector(undefined) @@ -120,7 +114,7 @@ const InnerStakingModal = ({ const walletStakedBalanceLoadable = useCachedLoadable( walletAddress ? Cw20StakeSelectors.stakedBalanceAtHeightSelector({ - chainId, + chainId: votingModule.chainId, contractAddress: stakingContractAddress, params: [{ address: walletAddress }], }) @@ -129,10 +123,10 @@ const InnerStakingModal = ({ const walletStakedBalance = walletStakedBalanceLoadable.state === 'hasValue' && walletStakedBalanceLoadable.contents - ? Number(walletStakedBalanceLoadable.contents.balance) + ? walletStakedBalanceLoadable.contents.balance : undefined - const [amount, setAmount] = useState(0) + const [amount, setAmount] = useState(HugeDecimal.zero) const doCw20SendAndExecute = Cw20BaseHooks.useSend({ contractAddress: governanceToken.denomOrAddress, @@ -152,7 +146,7 @@ const InnerStakingModal = ({ }) const setRefreshDaoVotingPower = useSetRecoilState( - refreshDaoVotingPowerAtom(coreAddress) + refreshDaoVotingPowerAtom(votingModule.dao.coreAddress) ) const setRefreshFollowedDaos = useSetRecoilState(refreshFollowingDaosAtom) const refreshDaoVotingPower = () => { @@ -161,7 +155,7 @@ const InnerStakingModal = ({ } const awaitNextBlock = useAwaitNextBlock() - const onAction = async (mode: StakingMode, amount: number) => { + const onAction = async (mode: StakingMode, amount: HugeDecimal) => { if (!isWalletConnected) { toast.error(t('error.logInToContinue')) return @@ -175,10 +169,7 @@ const InnerStakingModal = ({ try { await doCw20SendAndExecute({ - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - governanceToken.decimals - ), + amount: amount.toString(), contract: stakingContractToExecute, msg: encodeJsonToBase64({ [isOraichainCustomStaking ? 'bond' : 'stake']: {}, @@ -192,11 +183,15 @@ const InnerStakingModal = ({ refreshTotals() refreshDaoVotingPower() - setAmount(0) + setAmount(HugeDecimal.zero) + toast.success( - `Staked ${amount.toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.stakedTokens', { + amount: amount.toInternationalizedHumanReadableString({ + decimals: governanceToken.decimals, + }), + tokenSymbol: governanceToken.symbol, + }) ) // Close once done. @@ -226,40 +221,31 @@ const InnerStakingModal = ({ // value = amount_staked * total_value / staked_total // // => amount_staked = staked_total * value / total_value - let amountToUnstake = - (Number(totalStakedBalance.total) * amount) / Number(totalValue.total) + let amountToUnstake = amount + .times(totalStakedBalance.total) + .div(totalValue.total) // We have limited precision and on the contract side division rounds // down, so division and multiplication don't commute. Handle the common // case here where someone is attempting to unstake all of their funds. if ( - Math.abs( - walletStakedBalance - - convertDenomToMicroDenomWithDecimals( - amountToUnstake, - governanceToken.decimals - ) - ) <= 1 + HugeDecimal.from(walletStakedBalance) + .minus(amountToUnstake) + .abs() + .lte(1) ) { - amountToUnstake = convertMicroDenomToDenomWithDecimals( - walletStakedBalance, - governanceToken.decimals - ) + amountToUnstake = HugeDecimal.from(walletStakedBalance) } try { - const convertedAmount = convertDenomToMicroDenomStringWithDecimals( - amountToUnstake, - governanceToken.decimals - ) if (isOraichainCustomStaking) { await doOraichainUnbond({ - amount: convertedAmount, + amount: amountToUnstake.toString(), stakingToken: governanceToken.denomOrAddress, }) } else { await doUnstake({ - amount: convertedAmount, + amount: amountToUnstake.toString(), }) } @@ -271,11 +257,14 @@ const InnerStakingModal = ({ refreshClaims?.() refreshDaoVotingPower() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Unstaked ${amount.toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.unstakedTokens', { + amount: amount.toInternationalizedHumanReadableString({ + decimals: governanceToken.decimals, + }), + tokenSymbol: governanceToken.symbol, + }) ) // Close once done. @@ -290,8 +279,9 @@ const InnerStakingModal = ({ break } case StakingMode.Claim: { - if (sumClaimsAvailable === 0) { - return toast.error('No claims available.') + if (sumClaimsAvailable.isZero()) { + toast.error(t('error.noClaimsAvailable')) + return } setStakingLoading(true) @@ -313,15 +303,17 @@ const InnerStakingModal = ({ refreshTotals() refreshClaims?.() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Claimed ${convertMicroDenomToDenomWithDecimals( - sumClaimsAvailable || 0, - governanceToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.claimedTokens', { + amount: sumClaimsAvailable.toInternationalizedHumanReadableString( + { + decimals: governanceToken.decimals, + } + ), + tokenSymbol: governanceToken.symbol, + }) ) // Close once done. @@ -343,43 +335,16 @@ const InnerStakingModal = ({ return ( <StatelessStakingModal amount={amount} - claimableTokens={sumClaimsAvailable || 0} + claimableTokens={sumClaimsAvailable} error={isWalletConnected ? undefined : t('error.logInToContinue')} initialMode={initialMode} loading={stakingLoading} - loadingStakableTokens={ - !loadingUnstakedBalance || loadingUnstakedBalance.loading - ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingUnstakedBalance.data, - governanceToken.decimals - ), - } - } - loadingUnstakableTokens={ - !loadingWalletStakedValue || loadingWalletStakedValue.loading - ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingWalletStakedValue.data, - governanceToken.decimals - ), - } - } + loadingStakableTokens={loadingUnstakedBalance ?? { loading: true }} + loadingUnstakableTokens={loadingWalletStakedValue} onAction={onAction} onClose={onClose} - proposalDeposit={ - maxDeposit - ? convertMicroDenomToDenomWithDecimals( - maxDeposit, - governanceToken.decimals - ) - : undefined - } - setAmount={(newAmount) => setAmount(newAmount)} + proposalDeposit={maxDeposit ? HugeDecimal.from(maxDeposit) : undefined} + setAmount={setAmount} token={governanceToken} unstakingDuration={unstakingDuration ?? null} visible={visible} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useGovernanceTokenInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useGovernanceTokenInfo.ts index ec0eff630..854f61ba1 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useGovernanceTokenInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useGovernanceTokenInfo.ts @@ -1,17 +1,18 @@ -import { constSelector, useRecoilValue, waitForAll } from 'recoil' +import { useQueryClient, useSuspenseQueries } from '@tanstack/react-query' +import { constSelector } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20BaseSelectors, - DaoVotingCw20StakedSelectors, - genericTokenSelector, + cw20BaseQueries, + daoVotingCw20StakedQueries, + tokenQueries, usdPriceSelector, } from '@dao-dao/state' -import { useCachedLoading } from '@dao-dao/stateless' +import { useCachedLoading, useDao } from '@dao-dao/stateless' import { TokenType } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseGovernanceTokenInfoOptions, UseGovernanceTokenInfoResponse, @@ -22,39 +23,38 @@ export const useGovernanceTokenInfo = ({ fetchTreasuryBalance = false, fetchUsdcPrice = false, }: UseGovernanceTokenInfoOptions = {}): UseGovernanceTokenInfoResponse => { + const dao = useDao() const { address: walletAddress } = useWallet() - const { chainId, coreAddress, votingModuleAddress } = - useVotingModuleAdapterOptions() + const queryClient = useQueryClient() - const [stakingContractAddress, governanceTokenAddress] = useRecoilValue( - waitForAll([ - DaoVotingCw20StakedSelectors.stakingContractSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], - }), - DaoVotingCw20StakedSelectors.tokenContractSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], - }), - ]) - ) + const [{ data: stakingContractAddress }, { data: governanceTokenAddress }] = + useSuspenseQueries({ + queries: [ + daoVotingCw20StakedQueries.stakingContract(queryClient, { + chainId: dao.chainId, + contractAddress: dao.votingModule.address, + }), + daoVotingCw20StakedQueries.tokenContract(queryClient, { + chainId: dao.chainId, + contractAddress: dao.votingModule.address, + }), + ], + }) - const [governanceToken, cw20TokenInfo] = useRecoilValue( - waitForAll([ - genericTokenSelector({ - chainId, - type: TokenType.Cw20, - denomOrAddress: governanceTokenAddress, - }), - Cw20BaseSelectors.tokenInfoSelector({ - chainId, - contractAddress: governanceTokenAddress, - params: [], - }), - ]) - ) + const [{ data: governanceToken }, { data: cw20TokenInfo }] = + useSuspenseQueries({ + queries: [ + tokenQueries.info(queryClient, { + chainId: dao.chainId, + type: TokenType.Cw20, + denomOrAddress: governanceTokenAddress, + }), + cw20BaseQueries.tokenInfo(queryClient, { + chainId: dao.chainId, + contractAddress: governanceTokenAddress, + }), + ], + }) /// Optional @@ -62,7 +62,7 @@ export const useGovernanceTokenInfo = ({ const loadingWalletBalance = useCachedLoading( fetchWalletBalance && walletAddress ? Cw20BaseSelectors.balanceSelector({ - chainId, + chainId: dao.chainId, contractAddress: governanceTokenAddress, params: [{ address: walletAddress }], }) @@ -74,9 +74,9 @@ export const useGovernanceTokenInfo = ({ const loadingTreasuryBalance = useCachedLoading( fetchTreasuryBalance ? Cw20BaseSelectors.balanceSelector({ - chainId, + chainId: dao.chainId, contractAddress: governanceTokenAddress, - params: [{ address: coreAddress }], + params: [{ address: dao.coreAddress }], }) : constSelector(undefined), undefined @@ -86,8 +86,8 @@ export const useGovernanceTokenInfo = ({ const loadingPrice = useCachedLoading( fetchUsdcPrice ? usdPriceSelector({ + chainId: dao.chainId, type: TokenType.Cw20, - chainId, denomOrAddress: governanceTokenAddress, }) : constSelector(undefined), @@ -96,10 +96,7 @@ export const useGovernanceTokenInfo = ({ return { governanceToken, - supply: convertMicroDenomToDenomWithDecimals( - cw20TokenInfo.total_supply, - governanceToken.decimals - ), + supply: HugeDecimal.from(cw20TokenInfo.total_supply), stakingContractAddress, /// Optional // Wallet balance @@ -109,7 +106,7 @@ export const useGovernanceTokenInfo = ({ ? undefined : { loading: false, - data: Number(loadingWalletBalance.data.balance), + data: HugeDecimal.from(loadingWalletBalance.data.balance), }, // Treasury balance loadingTreasuryBalance: loadingTreasuryBalance.loading @@ -118,7 +115,7 @@ export const useGovernanceTokenInfo = ({ ? undefined : { loading: false, - data: Number(loadingTreasuryBalance.data.balance), + data: HugeDecimal.from(loadingTreasuryBalance.data.balance), }, // Price loadingPrice: loadingPrice.loading diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useMainDaoInfoCards.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useMainDaoInfoCards.tsx index 6428a6385..084070575 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useMainDaoInfoCards.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useMainDaoInfoCards.tsx @@ -1,24 +1,23 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { indexerQueries } from '@dao-dao/state' -import { TokenAmountDisplay } from '@dao-dao/stateless' +import { TokenAmountDisplay, useVotingModule } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' import { - convertDenomToMicroDenomWithDecimals, convertDurationToHumanReadableString, formatPercentOf100, isSecretNetwork, } from '@dao-dao/utils' import { useMembership, useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo } from './useGovernanceTokenInfo' import { useStakingInfo } from './useStakingInfo' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { totalVotingWeight } = useMembership() const { unstakingDuration } = useStakingInfo() @@ -31,8 +30,8 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { const queryClient = useQueryClient() const loadingMembers = useQueryLoadingDataWithError( indexerQueries.queryContract(queryClient, { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, formula: 'daoVotingCw20Staked/topStakers', noFallback: true, }) @@ -40,7 +39,7 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { return [ // Can't view members on Secret Network. - ...(isSecretNetwork(chainId) + ...(isSecretNetwork(votingModule.chainId) ? [] : [ { @@ -77,9 +76,10 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { totalVotingWeight === undefined ? undefined : formatPercentOf100( - (totalVotingWeight / - convertDenomToMicroDenomWithDecimals(supply, decimals)) * - 100 + HugeDecimal.from(totalVotingWeight) + .div(HugeDecimal.fromHumanReadable(supply, decimals)) + .times(100) + .toNumber() ), }, { diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useStakingInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useStakingInfo.ts index e510f16f8..755e4eadc 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useStakingInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/hooks/useStakingInfo.ts @@ -1,18 +1,20 @@ +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useCallback } from 'react' -import { constSelector, useRecoilValue, useSetRecoilState } from 'recoil' +import { constSelector, useSetRecoilState } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { Cw20StakeSelectors, - DaoVotingCw20StakedSelectors, blockHeightSelector, + cw20StakeQueries, + daoVotingCw20StakedQueries, refreshClaimsIdAtom, refreshWalletBalancesIdAtom, } from '@dao-dao/state' -import { useCachedLoadable, useCachedLoading } from '@dao-dao/stateless' +import { useCachedLoadable, useCachedLoading, useDao } from '@dao-dao/stateless' import { claimAvailable } from '@dao-dao/utils' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseStakingInfoOptions, UseStakingInfoResponse } from '../types' export const useStakingInfo = ({ @@ -20,25 +22,24 @@ export const useStakingInfo = ({ fetchTotalStakedValue = false, fetchWalletStakedValue = false, }: UseStakingInfoOptions = {}): UseStakingInfoResponse => { + const dao = useDao() const { address: walletAddress } = useWallet() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const queryClient = useQueryClient() - const stakingContractAddress = useRecoilValue( - DaoVotingCw20StakedSelectors.stakingContractSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], + const { data: stakingContractAddress } = useSuspenseQuery( + daoVotingCw20StakedQueries.stakingContract(queryClient, { + chainId: dao.chainId, + contractAddress: dao.votingModule.address, }) ) const unstakingDuration = - useRecoilValue( - Cw20StakeSelectors.getConfigSelector({ - chainId, + useSuspenseQuery( + cw20StakeQueries.getConfig(queryClient, { + chainId: dao.chainId, contractAddress: stakingContractAddress, - params: [], }) - ).unstaking_duration ?? undefined + ).data.unstaking_duration ?? undefined const setRefreshTotalBalancesId = useSetRecoilState( refreshWalletBalancesIdAtom(undefined) @@ -55,7 +56,7 @@ export const useStakingInfo = ({ const blockHeightLoadable = useCachedLoadable( fetchClaims ? blockHeightSelector({ - chainId, + chainId: dao.chainId, }) : undefined ) @@ -73,7 +74,7 @@ export const useStakingInfo = ({ const loadingClaims = useCachedLoading( fetchClaims && walletAddress ? Cw20StakeSelectors.claimsSelector({ - chainId, + chainId: dao.chainId, contractAddress: stakingContractAddress, params: [{ address: walletAddress }], }) @@ -93,15 +94,15 @@ export const useStakingInfo = ({ ? claims?.filter((c) => claimAvailable(c, blockHeight)) : undefined const sumClaimsAvailable = claimsAvailable?.reduce( - (p, c) => p + Number(c.amount), - 0 + (sum, c) => sum.plus(c.amount), + HugeDecimal.zero ) // Total staked value const loadingTotalStakedValue = useCachedLoading( fetchTotalStakedValue ? Cw20StakeSelectors.totalValueSelector({ - chainId, + chainId: dao.chainId, contractAddress: stakingContractAddress, params: [], }) @@ -113,7 +114,7 @@ export const useStakingInfo = ({ const loadingWalletStakedValue = useCachedLoading( fetchWalletStakedValue && walletAddress ? Cw20StakeSelectors.stakedValueSelector({ - chainId, + chainId: dao.chainId, contractAddress: stakingContractAddress, params: [{ address: walletAddress }], }) @@ -143,7 +144,7 @@ export const useStakingInfo = ({ ? undefined : { loading: false, - data: Number(loadingTotalStakedValue.data.total), + data: HugeDecimal.from(loadingTotalStakedValue.data.total), }, // Wallet staked value loadingWalletStakedValue: loadingWalletStakedValue.loading @@ -152,7 +153,7 @@ export const useStakingInfo = ({ ? undefined : { loading: false, - data: Number(loadingWalletStakedValue.data.value), + data: HugeDecimal.from(loadingWalletStakedValue.data.value), }, } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/types.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/types.ts index 408269401..1318242de 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/types.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/types.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { Duration, GenericToken, @@ -23,11 +24,11 @@ export interface UseStakingInfoResponse { claims?: Claim[] claimsPending?: Claim[] claimsAvailable?: Claim[] - sumClaimsAvailable?: number + sumClaimsAvailable?: HugeDecimal // Total staked value - loadingTotalStakedValue?: LoadingData<number> + loadingTotalStakedValue?: LoadingData<HugeDecimal> // Wallet staked value - loadingWalletStakedValue?: LoadingData<number> + loadingWalletStakedValue?: LoadingData<HugeDecimal> } export type UseGovernanceTokenInfoOptions = { @@ -53,7 +54,7 @@ export type UseGovernanceTokenInfoResponse = { /** * The supply of the governance token converted to the appropriate decimals. */ - supply: number + supply: HugeDecimal /** * The staking contract address for the governance token. */ @@ -65,12 +66,12 @@ export type UseGovernanceTokenInfoResponse = { * Unstaked governance token balance. Only defined if a wallet is connected * and the option to fetch this is true. */ - loadingWalletBalance?: LoadingData<number> + loadingWalletBalance?: LoadingData<HugeDecimal> /** * The treasury balance of the governance token. Only defined if the option to * fetch this is true. */ - loadingTreasuryBalance?: LoadingData<number> + loadingTreasuryBalance?: LoadingData<HugeDecimal> /** * The price of the governance token. Only defined if the option to fetch this * is true. diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/Component.tsx index 01a0e5a77..266117eea 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/Component.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/Component.tsx @@ -16,7 +16,7 @@ import { InputErrorMessage, InputLabel, Loader, - NumberInput, + NumericInput, useActionOptions, useDetectWrap, } from '@dao-dao/stateless' @@ -58,7 +58,7 @@ export const ManageMembersComponent: ActionComponent< const { chain: { bech32_prefix: bech32Prefix }, } = useActionOptions() - const { register, setValue, watch, control } = + const { register, setValue, watch, control, getValues } = useFormContext<ManageMembersData>() const toRemove = watch((fieldNamePrefix + 'toRemove') as 'toRemove') @@ -102,17 +102,18 @@ export const ManageMembersComponent: ActionComponent< > <div className="flex flex-col gap-1"> <InputLabel name={t('form.votingWeightPlaceholder')} /> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.toAdd?.[index]?.weight} fieldName={weightFieldName} + getValues={getValues} min={0} + numericValue placeholder={t('form.votingWeightPlaceholder')} register={register} setValue={setValue} sizing="fill" validation={[validateRequired, validateNonNegative]} - watch={watch} /> <InputErrorMessage error={errors?.toAdd?.[index]?.weight} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/index.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/index.tsx index 686aa7bb5..26dfe34d9 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/index.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/actions/ManageMembers/index.tsx @@ -1,5 +1,5 @@ import { daoVotingCw4Queries } from '@dao-dao/state/query' -import { ActionBase, PeopleEmoji, useActionOptions } from '@dao-dao/stateless' +import { ActionBase, PeopleEmoji } from '@dao-dao/stateless' import { UnifiedCosmosMsg } from '@dao-dao/types' import { ActionComponent, @@ -16,16 +16,14 @@ import { import { Cw4VotingModule } from '../../../../../clients' import { AddressInput, EntityDisplay } from '../../../../../components' -import { useLoadingVotingModule } from '../../hooks/useLoadingVotingModule' +import { useLoadingVotingModuleInfo } from '../../hooks/useLoadingVotingModuleInfo' import { ManageMembersData, ManageMembersComponent as StatelessManageMembersComponent, } from './Component' const Component: ActionComponent = (props) => { - const { address } = useActionOptions() - - const votingModule = useLoadingVotingModule(address, { + const loadingMembers = useLoadingVotingModuleInfo({ fetchMembers: true, }) @@ -34,11 +32,12 @@ const Component: ActionComponent = (props) => { {...props} options={{ currentMembers: - votingModule.loading || votingModule.errored + loadingMembers.loading || loadingMembers.errored ? { loading: true } : { loading: false, - data: votingModule.data.members?.map(({ addr }) => addr) || [], + data: + loadingMembers.data.members?.map(({ addr }) => addr) || [], }, AddressInput, EntityDisplay, @@ -82,7 +81,7 @@ export class ManageMembersAction extends ActionBase<ManageMembersData> { async setup() { this.cw4GroupAddress = await this.options.queryClient.fetchQuery( daoVotingCw4Queries.groupContract(this.options.queryClient, { - chainId: this.votingModule.dao.chainId, + chainId: this.votingModule.chainId, contractAddress: this.votingModule.address, }) ) diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MainDaoInfoCardsLoader.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MainDaoInfoCardsLoader.tsx index 251f0d407..46bd12ba6 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MainDaoInfoCardsLoader.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MainDaoInfoCardsLoader.tsx @@ -1,11 +1,11 @@ import { useTranslation } from 'react-i18next' -import { DaoInfoCards, useDaoInfoContext } from '@dao-dao/stateless' +import { DaoInfoCards, useDao } from '@dao-dao/stateless' import { formatDate } from '@dao-dao/utils' export const MainDaoInfoCardsLoader = () => { const { t } = useTranslation() - const { activeThreshold, created } = useDaoInfoContext() + const { activeThreshold, created } = useDao().info return ( <DaoInfoCards diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MembersTab.tsx index 4fc8b5a9b..74a8ee7af 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MembersTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/MembersTab.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { MembersTab as StatelessMembersTab, useDaoNavHelpers, + useVotingModule, } from '@dao-dao/stateless' import { ActionKey, @@ -13,29 +14,28 @@ import { getDaoProposalSinglePrefill } from '@dao-dao/utils' import { ButtonLink, DaoMemberCard } from '../../../../components' import { useMembership } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useLoadingVotingModule } from '../hooks/useLoadingVotingModule' +import { useLoadingVotingModuleInfo } from '../hooks/useLoadingVotingModuleInfo' export const MembersTab = () => { const { t } = useTranslation() - const { coreAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { getDaoProposalPath } = useDaoNavHelpers() const { isMember = false, totalVotingWeight } = useMembership() - const votingModule = useLoadingVotingModule(coreAddress, { + const loadingMembers = useLoadingVotingModuleInfo({ fetchMembers: true, }) const members: LoadingDataWithError<StatefulDaoMemberCardProps[]> = - votingModule.loading + loadingMembers.loading ? { loading: true, errored: false } - : votingModule.errored - ? { loading: false, errored: true, error: votingModule.error } + : loadingMembers.errored + ? { loading: false, errored: true, error: loadingMembers.error } : { loading: false, errored: false, data: - votingModule.data.members?.map(({ addr, weight }) => ({ + loadingMembers.data.members?.map(({ addr, weight }) => ({ address: addr, balanceLabel: t('title.votingWeight'), balance: { @@ -58,19 +58,23 @@ export const MembersTab = () => { <StatelessMembersTab ButtonLink={ButtonLink} DaoMemberCard={DaoMemberCard} - addMemberHref={getDaoProposalPath(coreAddress, 'create', { - prefill: getDaoProposalSinglePrefill({ - actions: [ - { - actionKey: ActionKey.ManageMembers, - data: { - toAdd: [{ addr: '', weight: NaN }], - toRemove: [], + addMemberHref={getDaoProposalPath( + votingModule.dao.coreAddress, + 'create', + { + prefill: getDaoProposalSinglePrefill({ + actions: [ + { + actionKey: ActionKey.ManageMembers, + data: { + toAdd: [{ addr: '', weight: NaN }], + toRemove: [], + }, }, - }, - ], - }), - })} + ], + }), + } + )} isMember={isMember} members={members} topVoters={{ diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/ProfileCardMemberInfo/index.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/ProfileCardMemberInfo/index.tsx index dc80113d1..3ec0852b2 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/ProfileCardMemberInfo/index.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/components/ProfileCardMemberInfo/index.tsx @@ -1,4 +1,4 @@ -import { useDaoInfoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps } from '@dao-dao/types' import { useMembership } from '../../../../../hooks' @@ -8,7 +8,7 @@ export const ProfileCardMemberInfo = ({ maxGovernanceTokenDeposit: _, ...props }: BaseProfileCardMemberInfoProps) => { - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const { walletVotingWeight, totalVotingWeight } = useMembership() diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/index.ts index 6138de210..ef775edc7 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/index.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/index.ts @@ -1,3 +1,3 @@ -export * from './useLoadingVotingModule' +export * from './useLoadingVotingModuleInfo' export * from './useMainDaoInfoCards' export * from './useVotingModuleRelevantAddresses' diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useLoadingVotingModule.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useLoadingVotingModuleInfo.ts similarity index 53% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useLoadingVotingModule.ts rename to packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useLoadingVotingModuleInfo.ts index 4faf8a8b1..09fcc1f9a 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useLoadingVotingModule.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useLoadingVotingModuleInfo.ts @@ -1,48 +1,33 @@ import { useQueryClient } from '@tanstack/react-query' import { useMemo } from 'react' -import { - cw4GroupExtraQueries, - daoDaoCoreQueries, - daoVotingCw4Queries, -} from '@dao-dao/state' -import { useChain } from '@dao-dao/stateless' +import { cw4GroupExtraQueries, daoVotingCw4Queries } from '@dao-dao/state' +import { useVotingModule } from '@dao-dao/stateless' import { LoadingDataWithError } from '@dao-dao/types' import { Member } from '@dao-dao/types/contracts/DaoVotingCw4' import { useQueryLoadingDataWithError } from '../../../../hooks' -interface UseVotingModuleOptions { +type UseVotingModuleInfoOptions = { fetchMembers?: boolean } -interface UseVotingModuleReturn { - votingModuleAddress: string +type UseVotingModuleInfoReturn = { cw4GroupAddress: string members: Member[] | undefined } -export const useLoadingVotingModule = ( - coreAddress: string, - { fetchMembers }: UseVotingModuleOptions = {} -): LoadingDataWithError<UseVotingModuleReturn> => { - const { chain_id: chainId } = useChain() +export const useLoadingVotingModuleInfo = ({ + fetchMembers, +}: UseVotingModuleInfoOptions = {}): LoadingDataWithError<UseVotingModuleInfoReturn> => { + const votingModule = useVotingModule() const queryClient = useQueryClient() - const votingModuleAddress = useQueryLoadingDataWithError( - daoDaoCoreQueries.votingModule(queryClient, { - chainId, - contractAddress: coreAddress, - }) - ) - const cw4GroupAddress = useQueryLoadingDataWithError( - votingModuleAddress.loading || votingModuleAddress.errored - ? undefined - : daoVotingCw4Queries.groupContract(queryClient, { - chainId, - contractAddress: votingModuleAddress.data, - }) + daoVotingCw4Queries.groupContract(queryClient, { + chainId: votingModule.chainId, + contractAddress: votingModule.address, + }) ) const members = useQueryLoadingDataWithError( @@ -50,7 +35,7 @@ export const useLoadingVotingModule = ( ? cw4GroupAddress.loading || cw4GroupAddress.errored ? undefined : cw4GroupExtraQueries.listAllMembers(queryClient, { - chainId, + chainId: votingModule.chainId, address: cw4GroupAddress.data, }) : undefined @@ -58,22 +43,16 @@ export const useLoadingVotingModule = ( return useMemo( () => - votingModuleAddress.loading || - cw4GroupAddress.loading || - (fetchMembers && members.loading) + cw4GroupAddress.loading || (fetchMembers && members.loading) ? { loading: true, errored: false, } - : votingModuleAddress.errored || - cw4GroupAddress.errored || - (fetchMembers && members.errored) + : cw4GroupAddress.errored || (fetchMembers && members.errored) ? { loading: false, errored: true, - error: votingModuleAddress.errored - ? votingModuleAddress.error - : cw4GroupAddress.errored + error: cw4GroupAddress.errored ? cw4GroupAddress.error : fetchMembers && members.errored ? members.error @@ -83,11 +62,9 @@ export const useLoadingVotingModule = ( loading: false, errored: false, updating: - votingModuleAddress.updating || cw4GroupAddress.updating || (fetchMembers && !members.loading && members.updating), data: { - votingModuleAddress: votingModuleAddress.data, cw4GroupAddress: cw4GroupAddress.data, members: fetchMembers && !members.loading && !members.errored @@ -95,6 +72,6 @@ export const useLoadingVotingModule = ( : undefined, }, }, - [cw4GroupAddress, members, votingModuleAddress, fetchMembers] + [cw4GroupAddress, members, fetchMembers] ) } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useMainDaoInfoCards.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useMainDaoInfoCards.ts index 9459eaac2..40ae7b290 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useMainDaoInfoCards.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useMainDaoInfoCards.ts @@ -1,15 +1,16 @@ import { useTranslation } from 'react-i18next' +import { useChain } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' import { isSecretNetwork } from '@dao-dao/utils' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useLoadingVotingModule } from './useLoadingVotingModule' +import { useLoadingVotingModuleInfo } from './useLoadingVotingModuleInfo' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { chainId, coreAddress } = useVotingModuleAdapterOptions() - const votingModule = useLoadingVotingModule(coreAddress, { + const { chain_id: chainId } = useChain() + + const loadingMembers = useLoadingVotingModuleInfo({ fetchMembers: true, }) @@ -20,12 +21,12 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { { label: t('title.members'), tooltip: t('info.membersTooltip'), - loading: votingModule.loading, - value: votingModule.loading + loading: loadingMembers.loading, + value: loadingMembers.loading ? undefined - : votingModule.errored + : loadingMembers.errored ? '<error>' - : votingModule.data.members?.length ?? '<error>', + : loadingMembers.data.members?.length ?? '<error>', }, ] } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useVotingModuleRelevantAddresses.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useVotingModuleRelevantAddresses.ts index 0a35302de..bfd94a4e2 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useVotingModuleRelevantAddresses.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw4/hooks/useVotingModuleRelevantAddresses.ts @@ -2,23 +2,21 @@ import { useTranslation } from 'react-i18next' import { VotingModuleRelevantAddress } from '@dao-dao/types' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useLoadingVotingModule } from './useLoadingVotingModule' +import { useLoadingVotingModuleInfo } from './useLoadingVotingModuleInfo' export const useVotingModuleRelevantAddresses = (): VotingModuleRelevantAddress[] => { const { t } = useTranslation() - const { coreAddress } = useVotingModuleAdapterOptions() - const votingModule = useLoadingVotingModule(coreAddress) + const loadingInfo = useLoadingVotingModuleInfo() return [ { label: t('info.groupAddress'), - address: votingModule.loading + address: loadingInfo.loading ? '...' - : votingModule.errored + : loadingInfo.errored ? '<error>' - : votingModule.data.cw4GroupAddress, + : loadingInfo.data.cw4GroupAddress, }, ] } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/Component.tsx index 9e2fa6f60..b6fdbdd12 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/Component.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/Component.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { FormSwitchCard, InputErrorMessage, - NumberInput, + NumericInput, SelectInput, } from '@dao-dao/stateless' import { @@ -26,7 +26,7 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ isCreating, }) => { const { t } = useTranslation() - const { register, watch, setValue } = + const { register, watch, setValue, getValues } = useFormContext<UpdateStakingConfigData>() const unstakingDurationEnabled = watch( @@ -57,14 +57,16 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ {unstakingDurationEnabled && ( <> <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.unstakingDuration?.value} fieldName={ (fieldNamePrefix + 'unstakingDuration.value') as 'unstakingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="md" @@ -77,7 +79,6 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ : undefined } validation={[validatePositive, validateRequired]} - watch={watch} /> {isCreating && ( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/README.md index 74171ed2c..79c2dc74c 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/actions/UpdateStakingConfig/README.md @@ -17,7 +17,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). { "unstakingDurationEnabled": <true | false>, "unstakingDuration": { - "value": "<NUMBER>", + "value": <NUMBER>, "units": "<seconds | minutes | hours | days | weeks | months | years>" } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/MembersTab.tsx index bb4c4feb3..3072fd95f 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/MembersTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/MembersTab.tsx @@ -2,7 +2,10 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { daoVotingCw721StakedExtraQueries } from '@dao-dao/state/query' -import { MembersTab as StatelessMembersTab } from '@dao-dao/stateless' +import { + MembersTab as StatelessMembersTab, + useVotingModule, +} from '@dao-dao/stateless' import { StatefulDaoMemberCardProps } from '@dao-dao/types' import { @@ -14,18 +17,17 @@ import { useDaoGovernanceToken, useQueryLoadingDataWithError, } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' export const MembersTab = () => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const token = useDaoGovernanceToken() ?? undefined const queryClient = useQueryClient() const members = useQueryLoadingDataWithError( daoVotingCw721StakedExtraQueries.topStakers(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }), (data) => data?.map( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/ProfileCardMemberInfo.tsx index d07003b2a..ecd5d28b3 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/ProfileCardMemberInfo.tsx @@ -3,16 +3,13 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { blockHeightSelector, blocksPerYearSelector, stakingLoadingAtom, } from '@dao-dao/state' -import { - useCachedLoadable, - useChain, - useDaoInfoContext, -} from '@dao-dao/stateless' +import { useCachedLoadable, useChain, useDao } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps, UnstakingTask, @@ -39,7 +36,7 @@ export const ProfileCardMemberInfo = ({ }: BaseProfileCardMemberInfoProps) => { const { t } = useTranslation() const { chain_id: chainId } = useChain() - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const { address: walletAddress, isWalletConnected, @@ -83,7 +80,7 @@ export const ProfileCardMemberInfo = ({ if (!isWalletConnected) { return toast.error(t('error.logInToContinue')) } - if (!sumClaimsAvailable) { + if (!sumClaimsAvailable?.isPositive()) { return toast.error(t('error.noClaimsAvailable')) } @@ -99,9 +96,10 @@ export const ProfileCardMemberInfo = ({ refreshClaims?.() toast.success( - `Claimed ${sumClaimsAvailable.toLocaleString()} $${ - collectionInfo.symbol - }` + t('success.claimedTokens', { + amount: sumClaimsAvailable.toInternationalizedHumanReadableString(), + tokenSymbol: collectionInfo.symbol, + }) ) } catch (err) { console.error(err) @@ -136,7 +134,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsPending ?? []).map(({ release_at }) => ({ token, status: UnstakingTaskStatus.Unstaking, - amount: Number(1), + amount: HugeDecimal.one, date: convertExpirationToDate( blocksPerYear, release_at, @@ -148,7 +146,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsAvailable ?? []).map(({ release_at }) => ({ token, status: UnstakingTaskStatus.ReadyToClaim, - amount: Number(1), + amount: HugeDecimal.one, date: convertExpirationToDate( blocksPerYear, release_at, @@ -191,10 +189,10 @@ export const ProfileCardMemberInfo = ({ ? { loading: true } : { loading: false, - data: - (loadingWalletStakedValue.data / - loadingTotalStakedValue.data) * - 100, + data: loadingWalletStakedValue.data + .div(loadingTotalStakedValue.data) + .times(100) + .toNumber(), } } onClaim={onClaim} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/StakingModal.tsx index f662f7419..a5f1b3b36 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/StakingModal.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/StakingModal.tsx @@ -1,10 +1,9 @@ import { useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' -import { constSelector, useRecoilState, useSetRecoilState } from 'recoil' +import { useRecoilState, useSetRecoilState } from 'recoil' import { - DaoVotingCw721StakedSelectors, refreshDaoVotingPowerAtom, refreshWalletBalancesIdAtom, stakingLoadingAtom, @@ -12,13 +11,13 @@ import { import { ModalLoader, SegmentedControls, - StakingMode, - useCachedLoadable, + useVotingModule, } from '@dao-dao/stateless' import { BaseStakingModalProps, LazyNftCardInfo, LoadingDataWithError, + StakingMode, } from '@dao-dao/types' import { getNftKey, processError } from '@dao-dao/utils' @@ -29,7 +28,6 @@ import { useAwaitNextBlock, useWallet, } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceCollectionInfo, useStakingInfo } from '../hooks' export const StakingModal = (props: BaseStakingModalProps) => ( @@ -46,10 +44,8 @@ const InnerStakingModal = ({ initialMode = StakingMode.Stake, }: BaseStakingModalProps) => { const { t } = useTranslation() - const { chainId, coreAddress } = useVotingModuleAdapterOptions() - const { address: walletAddress, isWalletConnected } = useWallet({ - chainId, - }) + const votingModule = useVotingModule() + const { address: walletAddress, isWalletConnected } = useWallet() const setRefreshWalletNftsId = useSetRecoilState( refreshWalletBalancesIdAtom(walletAddress) @@ -82,24 +78,9 @@ const InnerStakingModal = ({ }) const hasStake = - loadingWalletStakedValue !== undefined && + loadingWalletStakedValue && !loadingWalletStakedValue.loading && - loadingWalletStakedValue.data > 0 - - const walletStakedBalanceLoadable = useCachedLoadable( - walletAddress - ? DaoVotingCw721StakedSelectors.votingPowerAtHeightSelector({ - chainId, - contractAddress: stakingContractAddress, - params: [{ address: walletAddress }], - }) - : constSelector(undefined) - ) - const walletStakedBalance = - walletStakedBalanceLoadable.state === 'hasValue' && - walletStakedBalanceLoadable.contents - ? Number(walletStakedBalanceLoadable.contents.power) - : undefined + loadingWalletStakedValue.data.isPositive() const doStakeMultiple = Cw721BaseHooks.useSendNftMultiple({ contractAddress: collectionAddress, @@ -111,7 +92,7 @@ const InnerStakingModal = ({ }) const setRefreshDaoVotingPower = useSetRecoilState( - refreshDaoVotingPowerAtom(coreAddress) + refreshDaoVotingPowerAtom(votingModule.dao.coreAddress) ) const refreshDaoVotingPower = () => setRefreshDaoVotingPower((id) => id + 1) @@ -144,7 +125,10 @@ const InnerStakingModal = ({ refreshDaoVotingPower() toast.success( - `Staked ${stakeTokenIds.length} $${collectionInfo.symbol}` + t('success.stakedTokens', { + amount: stakeTokenIds.length, + tokenSymbol: collectionInfo.symbol, + }) ) setStakeTokenIds([]) @@ -160,11 +144,6 @@ const InnerStakingModal = ({ break } case StakingMode.Unstake: { - if (walletStakedBalance === undefined) { - toast.error(t('error.loadingData')) - return - } - setStakingLoading(true) try { @@ -181,7 +160,10 @@ const InnerStakingModal = ({ refreshDaoVotingPower() toast.success( - `Unstaked ${unstakeTokenIds.length} $${collectionInfo.symbol}` + t('success.unstakedTokens', { + amount: unstakeTokenIds.length, + tokenSymbol: collectionInfo.symbol, + }) ) setUnstakeTokenIds([]) @@ -266,11 +248,11 @@ const InnerStakingModal = ({ selected={mode} tabs={[ { - label: t(`title.stakingModeNfts.stake`), + label: t('title.stakingModeNfts.stake'), value: StakingMode.Stake, }, { - label: t(`title.stakingModeNfts.unstake`), + label: t('title.stakingModeNfts.unstake'), value: StakingMode.Unstake, }, ]} @@ -283,7 +265,7 @@ const InnerStakingModal = ({ onNftClick={onNftClick} onSelectAll={onSelectAll} selectedKeys={currentTokenIds.map((tokenId) => - getNftKey(chainId, collectionAddress, tokenId) + getNftKey(votingModule.chainId, collectionAddress, tokenId) )} unstakingDuration={unstakingDuration} visible={visible} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useGovernanceCollectionInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useGovernanceCollectionInfo.ts index 4290f9d2a..5963be3f5 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useGovernanceCollectionInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useGovernanceCollectionInfo.ts @@ -1,14 +1,12 @@ +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { constSelector, useRecoilValue, waitForAll } from 'recoil' -import { - CommonNftSelectors, - DaoVotingCw721StakedSelectors, -} from '@dao-dao/state' -import { useCachedLoading } from '@dao-dao/stateless' +import { HugeDecimal } from '@dao-dao/math' +import { CommonNftSelectors, daoVotingCw721StakedQueries } from '@dao-dao/state' +import { useCachedLoading, useDao } from '@dao-dao/stateless' import { TokenType } from '@dao-dao/types' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseGovernanceCollectionInfoOptions, UseGovernanceCollectionInfoResponse, @@ -18,30 +16,28 @@ export const useGovernanceCollectionInfo = ({ fetchWalletBalance = false, fetchTreasuryBalance = false, }: UseGovernanceCollectionInfoOptions = {}): UseGovernanceCollectionInfoResponse => { - const { chainId, coreAddress, votingModuleAddress } = - useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) + const dao = useDao() + const { address: walletAddress } = useWallet() + const queryClient = useQueryClient() - const { nft_address: collectionAddress } = useRecoilValue( - DaoVotingCw721StakedSelectors.configSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], + const { + data: { nft_address: collectionAddress }, + } = useSuspenseQuery( + daoVotingCw721StakedQueries.config(queryClient, { + chainId: dao.chainId, + contractAddress: dao.votingModule.address, }) ) const [contractInfo, tokenSupplyInfo] = useRecoilValue( waitForAll([ CommonNftSelectors.contractInfoSelector({ - chainId, + chainId: dao.chainId, contractAddress: collectionAddress, params: [], }), - CommonNftSelectors.numTokensSelector({ - chainId, + chainId: dao.chainId, contractAddress: collectionAddress, params: [], }), @@ -54,7 +50,7 @@ export const useGovernanceCollectionInfo = ({ const loadingWalletBalance = useCachedLoading( fetchWalletBalance && walletAddress ? CommonNftSelectors.unpaginatedAllTokensForOwnerSelector({ - chainId, + chainId: dao.chainId, contractAddress: collectionAddress, owner: walletAddress, }) @@ -66,31 +62,31 @@ export const useGovernanceCollectionInfo = ({ const loadingTreasuryBalance = useCachedLoading( fetchTreasuryBalance ? CommonNftSelectors.unpaginatedAllTokensForOwnerSelector({ - chainId, + chainId: dao.chainId, contractAddress: collectionAddress, - owner: coreAddress, + owner: dao.coreAddress, }) : constSelector(undefined), undefined ) return { - stakingContractAddress: votingModuleAddress, + stakingContractAddress: dao.votingModule.address, collectionAddress, collectionInfo: { name: contractInfo.name, symbol: contractInfo.symbol, - totalSupply: tokenSupplyInfo.count, + totalSupply: HugeDecimal.from(tokenSupplyInfo.count), }, token: { - chainId, + chainId: dao.chainId, type: TokenType.Cw721, denomOrAddress: collectionAddress, symbol: contractInfo.symbol, decimals: 0, imageUrl: undefined, source: { - chainId, + chainId: dao.chainId, type: TokenType.Cw721, denomOrAddress: collectionAddress, }, @@ -103,7 +99,7 @@ export const useGovernanceCollectionInfo = ({ ? undefined : { loading: false, - data: Number(loadingWalletBalance.data.length), + data: HugeDecimal.from(loadingWalletBalance.data.length), }, // Treasury balance loadingTreasuryBalance: loadingTreasuryBalance.loading @@ -112,7 +108,7 @@ export const useGovernanceCollectionInfo = ({ ? undefined : { loading: false, - data: loadingTreasuryBalance.data.length, + data: HugeDecimal.from(loadingTreasuryBalance.data.length), }, } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useMainDaoInfoCards.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useMainDaoInfoCards.tsx index 0994d5c56..b8f08060b 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useMainDaoInfoCards.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useMainDaoInfoCards.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { daoVotingCw721StakedExtraQueries } from '@dao-dao/state' -import { TokenAmountDisplay } from '@dao-dao/stateless' +import { TokenAmountDisplay, useVotingModule } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' import { convertDurationToHumanReadableString, @@ -11,13 +11,12 @@ import { } from '@dao-dao/utils' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceCollectionInfo } from './useGovernanceCollectionInfo' import { useStakingInfo } from './useStakingInfo' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { loadingTotalStakedValue, unstakingDuration } = useStakingInfo({ fetchTotalStakedValue: true, @@ -34,14 +33,14 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { const queryClient = useQueryClient() const loadingMembers = useQueryLoadingDataWithError( daoVotingCw721StakedExtraQueries.topStakers(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }) ) return [ // Can't view members on Secret Network. - ...(isSecretNetwork(chainId) + ...(isSecretNetwork(votingModule.chainId) ? [] : [ { @@ -72,7 +71,7 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { value: loadingTotalStakedValue.loading ? '...' : formatPercentOf100( - (loadingTotalStakedValue.data / totalSupply) * 100 + loadingTotalStakedValue.data.div(totalSupply).times(100).toNumber() ), }, { diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useStakingInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useStakingInfo.ts index 1486fc339..93f79ff6f 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useStakingInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/hooks/useStakingInfo.ts @@ -6,6 +6,7 @@ import { waitForAll, } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { CommonNftSelectors, DaoVotingCw721StakedSelectors, @@ -19,12 +20,12 @@ import { useCachedLoadable, useCachedLoading, useCachedLoadingWithError, + useDao, } from '@dao-dao/stateless' import { NftClaim } from '@dao-dao/types/contracts/DaoVotingCw721Staked' import { claimAvailable } from '@dao-dao/utils' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseStakingInfoOptions, UseStakingInfoResponse } from '../types' import { useGovernanceCollectionInfo } from './useGovernanceCollectionInfo' @@ -34,10 +35,8 @@ export const useStakingInfo = ({ fetchWalletStakedValue = false, fetchWalletUnstakedNfts = false, }: UseStakingInfoOptions = {}): UseStakingInfoResponse => { - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) + const dao = useDao() + const { address: walletAddress } = useWallet() const { collectionAddress: governanceTokenAddress } = useGovernanceCollectionInfo() @@ -46,12 +45,12 @@ export const useStakingInfo = ({ useRecoilValue( waitForAll([ contractVersionSelector({ - chainId, - contractAddress: votingModuleAddress, + chainId: dao.chainId, + contractAddress: dao.votingModule.address, }), DaoVotingCw721StakedSelectors.configSelector({ - chainId, - contractAddress: votingModuleAddress, + chainId: dao.chainId, + contractAddress: dao.votingModule.address, params: [], }), ]) @@ -62,7 +61,7 @@ export const useStakingInfo = ({ ) // Refresh NFTs owned by staking contract. const setRefreshStakedNftsId = useSetRecoilState( - refreshWalletBalancesIdAtom(votingModuleAddress) + refreshWalletBalancesIdAtom(dao.votingModule.address) ) // Refresh totals, mostly for total staked power. const refreshTotals = useCallback(() => { @@ -76,7 +75,7 @@ export const useStakingInfo = ({ const blockHeightLoadable = useCachedLoadable( fetchClaims ? blockHeightSelector({ - chainId, + chainId: dao.chainId, }) : undefined ) @@ -92,8 +91,8 @@ export const useStakingInfo = ({ const loadingClaims = useCachedLoading( fetchClaims && walletAddress ? DaoVotingCw721StakedSelectors.nftClaimsSelector({ - chainId, - contractAddress: votingModuleAddress, + chainId: dao.chainId, + contractAddress: dao.votingModule.address, params: [{ address: walletAddress }], }) : constSelector(undefined), @@ -116,14 +115,14 @@ export const useStakingInfo = ({ const claimsAvailable = blockHeight ? nftClaims?.filter((c) => claimAvailable(c, blockHeight)) : undefined - const sumClaimsAvailable = claimsAvailable?.length + const sumClaimsAvailable = HugeDecimal.from(claimsAvailable?.length || 0) // Total staked value const loadingTotalStakedValue = useCachedLoading( fetchTotalStakedValue ? DaoVotingCw721StakedSelectors.totalPowerAtHeightSelector({ - chainId, - contractAddress: votingModuleAddress, + chainId: dao.chainId, + contractAddress: dao.votingModule.address, params: [{}], }) : constSelector(undefined), @@ -134,8 +133,8 @@ export const useStakingInfo = ({ const loadingWalletStakedNftsLoadable = useCachedLoading( fetchWalletStakedValue && walletAddress ? DaoVotingCw721StakedSelectors.stakedNftsSelector({ - chainId, - contractAddress: votingModuleAddress, + chainId: dao.chainId, + contractAddress: dao.votingModule.address, params: [{ address: walletAddress }], }) : undefined, @@ -148,7 +147,7 @@ export const useStakingInfo = ({ ? waitForAll( loadingWalletStakedNftsLoadable.data?.map((tokenId) => nftCardInfoSelector({ - chainId, + chainId: dao.chainId, collection: governanceTokenAddress, tokenId, }) @@ -160,7 +159,7 @@ export const useStakingInfo = ({ const loadingWalletUnstakedNftsLoadable = useCachedLoadingWithError( fetchWalletUnstakedNfts && walletAddress && governanceTokenAddress ? CommonNftSelectors.unpaginatedAllTokensForOwnerSelector({ - chainId, + chainId: dao.chainId, contractAddress: governanceTokenAddress, owner: walletAddress, }) @@ -174,7 +173,7 @@ export const useStakingInfo = ({ ? waitForAll( loadingWalletUnstakedNftsLoadable.data?.map((tokenId) => nftCardInfoSelector({ - chainId, + chainId: dao.chainId, collection: governanceTokenAddress, tokenId, }) @@ -185,7 +184,7 @@ export const useStakingInfo = ({ return { stakingContractVersion, - stakingContractAddress: votingModuleAddress, + stakingContractAddress: dao.votingModule.address, unstakingDuration: unstakingDuration ?? undefined, refreshTotals, /// Optional @@ -206,7 +205,7 @@ export const useStakingInfo = ({ ? undefined : { loading: false, - data: Number(loadingTotalStakedValue.data.power), + data: HugeDecimal.from(loadingTotalStakedValue.data.power), }, // Wallet staked value loadingWalletStakedValue: loadingWalletStakedNftsLoadable.loading @@ -215,7 +214,7 @@ export const useStakingInfo = ({ ? undefined : { loading: false, - data: loadingWalletStakedNftsLoadable.data.length, + data: HugeDecimal.from(loadingWalletStakedNftsLoadable.data.length), }, loadingWalletStakedNfts, loadingWalletUnstakedNfts, diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/types.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/types.ts index 6a1c30b14..4b6d38032 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/types.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/types.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { ContractVersion, Duration, @@ -27,11 +28,11 @@ export interface UseStakingInfoResponse { claims?: NftClaim[] claimsPending?: NftClaim[] claimsAvailable?: NftClaim[] - sumClaimsAvailable?: number + sumClaimsAvailable?: HugeDecimal // Total staked value - loadingTotalStakedValue?: LoadingData<number> + loadingTotalStakedValue?: LoadingData<HugeDecimal> // Wallet staked value - loadingWalletStakedValue?: LoadingData<number> + loadingWalletStakedValue?: LoadingData<HugeDecimal> loadingWalletStakedNfts?: LoadingDataWithError<NftCardInfo[]> loadingWalletUnstakedNfts?: LoadingDataWithError<NftCardInfo[]> } @@ -48,14 +49,14 @@ export interface UseGovernanceCollectionInfoResponse { collectionInfo: { name: string symbol: string - totalSupply: number + totalSupply: HugeDecimal } token: GenericToken /// Optional // Wallet balance - loadingWalletBalance?: LoadingData<number> + loadingWalletBalance?: LoadingData<HugeDecimal> // Treasury balance - loadingTreasuryBalance?: LoadingData<number> + loadingTreasuryBalance?: LoadingData<HugeDecimal> // Price // loadingPrice?: LoadingData<GenericTokenWithUsdPrice> } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/README.md deleted file mode 100644 index df523e401..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# DaoVotingNativeStaked - -This is the voting module adapter for the -[`dao-voting-native-staked`](https://github.com/DA0-DA0/dao-contracts/tree/7f89ad1604e8022f202aef729853b0c8c7196988/contracts/voting/dao-voting-native-staked) -contract, which determines DAO voting power based on the staked balance of its -chosen native token (IBC, factory, or otherwise), such as `JUNO` or -`factory/junoContract/subdenom`. This is an alternative to the -[CW20](https://docs.cosmwasm.com/cw-plus/0.9.0/cw20/spec) governance token-based -DAO structure, where members are still free to exchange their unstaked tokens -with other parties at any time. However, it uses a native token instead of a -[CW20](https://docs.cosmwasm.com/cw-plus/0.9.0/cw20/spec) token. - -## Layout - -| Location | Summary | -| -------------------------- | ------------------- | -| [components](./components) | React components. | -| [hooks](./hooks) | React hooks. | -| [index.tsx](./index.tsx) | Adapter definition. | diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/Component.stories.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/Component.stories.tsx deleted file mode 100644 index 444a3487e..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/Component.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react' - -import { makeReactHookFormDecorator } from '@dao-dao/storybook' -import { DurationUnits } from '@dao-dao/types' - -import { - UpdateStakingConfigComponent, - UpdateStakingConfigData, -} from './Component' - -export default { - title: - 'DAO DAO / packages / stateful / voting-module-adapter / adapters / DaoVotingNativeStaked / actions / UpdateStakingConfig', - component: UpdateStakingConfigComponent, - decorators: [ - makeReactHookFormDecorator<UpdateStakingConfigData>({ - unstakingDurationEnabled: true, - unstakingDuration: { - value: 4, - units: DurationUnits.Days, - }, - }), - ], -} as ComponentMeta<typeof UpdateStakingConfigComponent> - -const Template: ComponentStory<typeof UpdateStakingConfigComponent> = ( - args -) => <UpdateStakingConfigComponent {...args} /> - -export const Default = Template.bind({}) -Default.args = { - fieldNamePrefix: '', - allActionsWithData: [], - index: 0, - data: {}, - isCreating: true, -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/Component.tsx deleted file mode 100644 index 9e2fa6f60..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/Component.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useFormContext } from 'react-hook-form' -import { useTranslation } from 'react-i18next' - -import { - FormSwitchCard, - InputErrorMessage, - NumberInput, - SelectInput, -} from '@dao-dao/stateless' -import { - ActionComponent, - BooleanFieldNames, - DurationUnitsValues, - DurationWithUnits, -} from '@dao-dao/types' -import { validatePositive, validateRequired } from '@dao-dao/utils' - -export type UpdateStakingConfigData = { - unstakingDurationEnabled: boolean - unstakingDuration: DurationWithUnits -} - -export const UpdateStakingConfigComponent: ActionComponent = ({ - fieldNamePrefix, - errors, - isCreating, -}) => { - const { t } = useTranslation() - const { register, watch, setValue } = - useFormContext<UpdateStakingConfigData>() - - const unstakingDurationEnabled = watch( - (fieldNamePrefix + 'unstakingDurationEnabled') as 'unstakingDurationEnabled' - ) - const unstakingDuration = watch( - (fieldNamePrefix + 'unstakingDuration') as 'unstakingDuration' - ) - - return ( - <> - <FormSwitchCard< - UpdateStakingConfigData, - BooleanFieldNames<UpdateStakingConfigData> - > - containerClassName="self-start" - fieldName={ - (fieldNamePrefix + - 'unstakingDurationEnabled') as 'unstakingDurationEnabled' - } - label={t('form.unstakingDurationTitle')} - readOnly={!isCreating} - setValue={setValue} - tooltip={t('form.unstakingDurationDescription')} - value={unstakingDurationEnabled} - /> - - {unstakingDurationEnabled && ( - <> - <div className="flex flex-row gap-2"> - <NumberInput - disabled={!isCreating} - error={errors?.unstakingDuration?.value} - fieldName={ - (fieldNamePrefix + - 'unstakingDuration.value') as 'unstakingDuration.value' - } - min={1} - register={register} - setValue={setValue} - sizing="md" - step={1} - unit={ - !isCreating - ? t(`unit.${unstakingDuration.units}`, { - count: unstakingDuration.value, - }).toLocaleLowerCase() - : undefined - } - validation={[validatePositive, validateRequired]} - watch={watch} - /> - - {isCreating && ( - <SelectInput - error={errors?.unstakingDuration?.units} - fieldName={ - (fieldNamePrefix + - 'unstakingDuration.units') as 'unstakingDuration.units' - } - register={register} - validation={[validateRequired]} - > - {DurationUnitsValues.map((type, idx) => ( - <option key={idx} value={type}> - {t(`unit.${type}`, { - count: unstakingDuration.value, - }).toLocaleLowerCase()} - </option> - ))} - </SelectInput> - )} - </div> - - <InputErrorMessage - className="-mt-2" - error={errors?.unstakingDuration?.value} - /> - </> - )} - </> - ) -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/README.md deleted file mode 100644 index 74171ed2c..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# UpdateStakingConfig - -Update token staking configuration. - -## 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 - -`updateStakingConfig` - -### Data format - -```json -{ - "unstakingDurationEnabled": <true | false>, - "unstakingDuration": { - "value": "<NUMBER>", - "units": "<seconds | minutes | hours | days | weeks | months | years>" - } -} -``` diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/index.ts deleted file mode 100644 index 8185b4b0e..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/UpdateStakingConfig/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { daoVotingNativeStakedQueries } from '@dao-dao/state/query' -import { ActionBase, GearEmoji } from '@dao-dao/stateless' -import { DurationUnits, UnifiedCosmosMsg } from '@dao-dao/types' -import { - ActionContextType, - ActionKey, - ActionMatch, - ActionOptions, - ProcessedMessage, -} from '@dao-dao/types/actions' -import { - convertDurationToDurationWithUnits, - convertDurationWithUnitsToDuration, - makeExecuteSmartContractMessage, - objectMatchesStructure, -} from '@dao-dao/utils' - -import { - UpdateStakingConfigComponent as Component, - UpdateStakingConfigData, -} from './Component' - -export class UpdateStakingConfigAction extends ActionBase<UpdateStakingConfigData> { - public readonly key = ActionKey.UpdateStakingConfig - public readonly Component = Component - - private stakingContractAddress: string - - constructor(options: ActionOptions) { - // Type-check. - if (options.context.type !== ActionContextType.Dao) { - throw new Error('Invalid context for update staking config action') - } - - super(options, { - Icon: GearEmoji, - label: options.t('title.updateStakingConfig'), - description: options.t('info.updateStakingConfigDescription'), - }) - - this.stakingContractAddress = options.context.dao.votingModule.address - } - - async setup() { - const { unstaking_duration } = await this.options.queryClient.fetchQuery( - daoVotingNativeStakedQueries.getConfig(this.options.queryClient, { - chainId: this.options.chain.chain_id, - contractAddress: this.stakingContractAddress, - }) - ) - - this.defaults = { - unstakingDurationEnabled: !!unstaking_duration, - unstakingDuration: unstaking_duration - ? convertDurationToDurationWithUnits(unstaking_duration) - : { - value: 2, - units: DurationUnits.Weeks, - }, - } - } - - encode({ - unstakingDurationEnabled, - unstakingDuration, - }: UpdateStakingConfigData): UnifiedCosmosMsg { - return makeExecuteSmartContractMessage({ - chainId: this.options.chain.chain_id, - sender: this.options.address, - contractAddress: this.stakingContractAddress, - msg: { - update_config: { - duration: unstakingDurationEnabled - ? convertDurationWithUnitsToDuration(unstakingDuration) - : null, - }, - }, - }) - } - - match([{ decodedMessage }]: ProcessedMessage[]): ActionMatch { - return ( - objectMatchesStructure(decodedMessage, { - wasm: { - execute: { - contract_addr: {}, - funds: {}, - msg: { - update_config: {}, - }, - }, - }, - }) && - decodedMessage.wasm.execute.contract_addr === this.stakingContractAddress - ) - } - - decode([{ decodedMessage }]: ProcessedMessage[]): UpdateStakingConfigData { - return { - unstakingDurationEnabled: - !!decodedMessage.wasm.execute.msg.update_config.duration, - unstakingDuration: decodedMessage.wasm.execute.msg.update_config.duration - ? convertDurationToDurationWithUnits( - decodedMessage.wasm.execute.msg.update_config.duration - ) - : { - value: 2, - units: DurationUnits.Weeks, - }, - } - } -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/index.ts deleted file mode 100644 index 40591c371..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Mint' -export * from './UpdateStakingConfig' diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/MembersTab.tsx deleted file mode 100644 index d61a547b7..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/MembersTab.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useTranslation } from 'react-i18next' - -import { indexerQueries } from '@dao-dao/state/query' -import { MembersTab as StatelessMembersTab } from '@dao-dao/stateless' -import { StatefulDaoMemberCardProps } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' - -import { - ButtonLink, - DaoMemberCard, - EntityDisplay, -} from '../../../../components' -import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useGovernanceTokenInfo } from '../hooks/useGovernanceTokenInfo' - -export const MembersTab = () => { - const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { governanceToken } = useGovernanceTokenInfo() - - const queryClient = useQueryClient() - const members = useQueryLoadingDataWithError( - indexerQueries.queryContract(queryClient, { - chainId, - contractAddress: votingModuleAddress, - formula: 'daoVotingNativeStaked/topStakers', - noFallback: true, - }), - (data) => - data?.map( - ({ - address, - balance, - votingPowerPercent, - }: any): StatefulDaoMemberCardProps => ({ - address, - balanceLabel: t('title.staked'), - balance: { - loading: false, - data: { - amount: convertMicroDenomToDenomWithDecimals( - balance, - governanceToken.decimals - ), - token: governanceToken, - }, - }, - votingPowerPercent: { - loading: false, - data: votingPowerPercent, - }, - }) - ) ?? [] - ) - - return ( - <StatelessMembersTab - ButtonLink={ButtonLink} - DaoMemberCard={DaoMemberCard} - members={members} - topVoters={{ - show: true, - EntityDisplay, - }} - /> - ) -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/ProfileCardMemberInfo.tsx deleted file mode 100644 index 434234449..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/ProfileCardMemberInfo.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { useCallback, useState } from 'react' -import toast from 'react-hot-toast' -import { useTranslation } from 'react-i18next' -import { useRecoilValue } from 'recoil' - -import { - blockHeightSelector, - blocksPerYearSelector, - stakingLoadingAtom, -} from '@dao-dao/state' -import { useCachedLoadable, useDaoInfoContext } from '@dao-dao/stateless' -import { - BaseProfileCardMemberInfoProps, - UnstakingTask, - UnstakingTaskStatus, -} from '@dao-dao/types' -import { - convertExpirationToDate, - convertMicroDenomToDenomWithDecimals, - durationToSeconds, - processError, -} from '@dao-dao/utils' - -import { - DaoVotingNativeStakedHooks, - useAwaitNextBlock, - useWallet, -} from '../../../../hooks' -import { ProfileCardMemberInfoTokens } from '../../../components' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useGovernanceTokenInfo, useStakingInfo } from '../hooks' -import { StakingModal } from './StakingModal' - -export const ProfileCardMemberInfo = ({ - maxGovernanceTokenDeposit, - ...props -}: BaseProfileCardMemberInfoProps) => { - const { t } = useTranslation() - const { name: daoName } = useDaoInfoContext() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { - address: walletAddress, - isWalletConnected, - refreshBalances, - } = useWallet({ - chainId, - }) - - const [showStakingModal, setShowStakingModal] = useState(false) - const [claimingLoading, setClaimingLoading] = useState(false) - const stakingLoading = useRecoilValue(stakingLoadingAtom) - - const { governanceToken, loadingWalletBalance: loadingUnstakedBalance } = - useGovernanceTokenInfo({ - fetchWalletBalance: true, - }) - - const { - unstakingDuration, - refreshTotals, - claimsPending, - claimsAvailable, - sumClaimsAvailable, - loadingWalletStakedValue, - loadingTotalStakedValue, - refreshClaims, - } = useStakingInfo({ - fetchClaims: true, - fetchWalletStakedValue: true, - fetchTotalStakedValue: true, - }) - - const doClaim = DaoVotingNativeStakedHooks.useClaim({ - contractAddress: votingModuleAddress, - sender: walletAddress ?? '', - }) - - const awaitNextBlock = useAwaitNextBlock() - const onClaim = useCallback(async () => { - if (!isWalletConnected) { - return toast.error(t('error.logInToContinue')) - } - if (!sumClaimsAvailable) { - return toast.error(t('error.noClaimsAvailable')) - } - - setClaimingLoading(true) - try { - await doClaim() - - // New balances will not appear until the next block. - await awaitNextBlock() - - refreshBalances() - refreshTotals() - refreshClaims?.() - - toast.success( - `Claimed ${convertMicroDenomToDenomWithDecimals( - sumClaimsAvailable, - governanceToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` - ) - } catch (err) { - console.error(err) - toast.error(processError(err)) - } finally { - setClaimingLoading(false) - } - }, [ - awaitNextBlock, - isWalletConnected, - doClaim, - governanceToken.decimals, - governanceToken.symbol, - refreshBalances, - refreshClaims, - refreshTotals, - sumClaimsAvailable, - t, - ]) - - const blockHeightLoadable = useCachedLoadable( - blockHeightSelector({ - chainId, - }) - ) - const blocksPerYear = useRecoilValue( - blocksPerYearSelector({ - chainId, - }) - ) - - const unstakingTasks: UnstakingTask[] = [ - ...(claimsPending ?? []).map(({ amount, release_at }) => ({ - token: governanceToken, - status: UnstakingTaskStatus.Unstaking, - amount: convertMicroDenomToDenomWithDecimals( - amount, - governanceToken.decimals - ), - date: convertExpirationToDate( - blocksPerYear, - release_at, - blockHeightLoadable.state === 'hasValue' - ? blockHeightLoadable.contents - : 0 - ), - })), - ...(claimsAvailable ?? []).map(({ amount, release_at }) => ({ - token: governanceToken, - status: UnstakingTaskStatus.ReadyToClaim, - amount: convertMicroDenomToDenomWithDecimals( - amount, - governanceToken.decimals - ), - date: convertExpirationToDate( - blocksPerYear, - release_at, - blockHeightLoadable.state === 'hasValue' - ? blockHeightLoadable.contents - : 0 - ), - })), - ] - - return ( - <> - <ProfileCardMemberInfoTokens - claimingLoading={claimingLoading} - daoName={daoName} - loadingTokens={ - !loadingWalletStakedValue || - loadingWalletStakedValue.loading || - !loadingUnstakedBalance || - loadingUnstakedBalance.loading - ? { - loading: true, - } - : { - loading: false, - data: [ - { - token: governanceToken, - staked: convertMicroDenomToDenomWithDecimals( - loadingWalletStakedValue.data, - governanceToken.decimals - ), - unstaked: convertMicroDenomToDenomWithDecimals( - loadingUnstakedBalance.data, - governanceToken.decimals - ), - }, - ], - } - } - loadingVotingPower={ - !loadingWalletStakedValue || - loadingWalletStakedValue.loading || - !loadingTotalStakedValue || - loadingTotalStakedValue.loading - ? { loading: true } - : { - loading: false, - data: - (loadingWalletStakedValue.data / - loadingTotalStakedValue.data) * - 100, - } - } - onClaim={onClaim} - onStake={() => setShowStakingModal(true)} - refreshUnstakingTasks={() => refreshClaims?.()} - stakingLoading={stakingLoading} - unstakingDurationSeconds={ - (unstakingDuration && - durationToSeconds(blocksPerYear, unstakingDuration)) || - undefined - } - unstakingTasks={unstakingTasks} - {...props} - /> - - <StakingModal - maxDeposit={maxGovernanceTokenDeposit} - onClose={() => setShowStakingModal(false)} - visible={showStakingModal} - /> - </> - ) -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/StakingModal.tsx deleted file mode 100644 index 96d7386a6..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/StakingModal.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { coins } from '@cosmjs/stargate' -import { useState } from 'react' -import toast from 'react-hot-toast' -import { useTranslation } from 'react-i18next' -import { useRecoilState, useSetRecoilState } from 'recoil' - -import { - refreshDaoVotingPowerAtom, - refreshFollowingDaosAtom, - stakingLoadingAtom, -} from '@dao-dao/state' -import { - ModalLoader, - StakingMode, - StakingModal as StatelessStakingModal, -} from '@dao-dao/stateless' -import { BaseStakingModalProps } from '@dao-dao/types' -import { - CHAIN_GAS_MULTIPLIER, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, - processError, -} from '@dao-dao/utils' - -import { SuspenseLoader } from '../../../../components' -import { - DaoVotingNativeStakedHooks, - useAwaitNextBlock, - useWallet, -} from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useGovernanceTokenInfo, useStakingInfo } from '../hooks' - -export const StakingModal = (props: BaseStakingModalProps) => ( - <SuspenseLoader - fallback={<ModalLoader onClose={props.onClose} visible={props.visible} />} - > - <InnerStakingModal {...props} /> - </SuspenseLoader> -) - -const InnerStakingModal = ({ - onClose, - visible, - initialMode = StakingMode.Stake, - maxDeposit, -}: BaseStakingModalProps) => { - const { t } = useTranslation() - const { - address: walletAddress, - isWalletConnected, - refreshBalances, - } = useWallet() - const { coreAddress, votingModuleAddress } = useVotingModuleAdapterOptions() - - const [stakingLoading, setStakingLoading] = useRecoilState(stakingLoadingAtom) - - const { governanceToken, loadingWalletBalance: loadingUnstakedBalance } = - useGovernanceTokenInfo({ - fetchWalletBalance: true, - }) - const { - unstakingDuration, - refreshTotals, - sumClaimsAvailable, - loadingWalletStakedValue, - refreshClaims, - } = useStakingInfo({ - fetchClaims: true, - fetchWalletStakedValue: true, - }) - - const [amount, setAmount] = useState(0) - - const doStake = DaoVotingNativeStakedHooks.useStake({ - contractAddress: votingModuleAddress, - sender: walletAddress ?? '', - }) - const doUnstake = DaoVotingNativeStakedHooks.useUnstake({ - contractAddress: votingModuleAddress, - sender: walletAddress ?? '', - }) - const doClaim = DaoVotingNativeStakedHooks.useClaim({ - contractAddress: votingModuleAddress, - sender: walletAddress ?? '', - }) - - const setRefreshDaoVotingPower = useSetRecoilState( - refreshDaoVotingPowerAtom(coreAddress) - ) - const setRefreshFollowedDaos = useSetRecoilState(refreshFollowingDaosAtom) - const refreshDaoVotingPower = () => { - setRefreshDaoVotingPower((id) => id + 1) - setRefreshFollowedDaos((id) => id + 1) - } - - const awaitNextBlock = useAwaitNextBlock() - const onAction = async (mode: StakingMode, amount: number) => { - if (!isWalletConnected) { - toast.error(t('error.logInToContinue')) - return - } - - setStakingLoading(true) - - switch (mode) { - case StakingMode.Stake: { - setStakingLoading(true) - - try { - await doStake( - CHAIN_GAS_MULTIPLIER, - undefined, - coins( - convertDenomToMicroDenomStringWithDecimals( - amount, - governanceToken.decimals - ), - governanceToken.denomOrAddress - ) - ) - - // New balances will not appear until the next block. - await awaitNextBlock() - - refreshBalances() - refreshTotals() - refreshDaoVotingPower() - - setAmount(0) - toast.success( - `Staked ${amount.toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` - ) - - // Close once done. - onClose() - } catch (err) { - console.error(err) - toast.error(processError(err)) - } finally { - setStakingLoading(false) - } - - break - } - case StakingMode.Unstake: { - setStakingLoading(true) - - try { - await doUnstake({ - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - governanceToken.decimals - ), - }) - - // New balances will not appear until the next block. - await awaitNextBlock() - - refreshBalances() - refreshTotals() - refreshClaims?.() - refreshDaoVotingPower() - - setAmount(0) - toast.success( - `Unstaked ${amount.toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` - ) - - // Close once done. - onClose() - } catch (err) { - console.error(err) - toast.error(processError(err)) - } finally { - setStakingLoading(false) - } - - break - } - case StakingMode.Claim: { - if (sumClaimsAvailable === 0) { - return toast.error('No claims available.') - } - - setStakingLoading(true) - try { - await doClaim() - - // New balances will not appear until the next block. - await awaitNextBlock() - - refreshBalances() - refreshTotals() - refreshClaims?.() - - setAmount(0) - - toast.success( - `Claimed ${convertMicroDenomToDenomWithDecimals( - sumClaimsAvailable || 0, - governanceToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` - ) - - // Close once done. - onClose() - } catch (err) { - console.error(err) - toast.error(processError(err)) - } finally { - setStakingLoading(false) - } - - break - } - default: - toast.error('Internal error while staking. Unrecognized mode.') - } - } - - return ( - <StatelessStakingModal - amount={amount} - claimableTokens={sumClaimsAvailable || 0} - error={isWalletConnected ? undefined : t('error.logInToContinue')} - initialMode={initialMode} - loading={stakingLoading} - loadingStakableTokens={ - !loadingUnstakedBalance || loadingUnstakedBalance.loading - ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingUnstakedBalance.data, - governanceToken.decimals - ), - } - } - loadingUnstakableTokens={ - !loadingWalletStakedValue || loadingWalletStakedValue.loading - ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingWalletStakedValue.data, - governanceToken.decimals - ), - } - } - onAction={onAction} - onClose={onClose} - proposalDeposit={ - maxDeposit - ? convertMicroDenomToDenomWithDecimals( - maxDeposit, - governanceToken.decimals - ) - : undefined - } - setAmount={(newAmount) => setAmount(newAmount)} - token={governanceToken} - unstakingDuration={unstakingDuration ?? null} - visible={visible} - /> - ) -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/index.ts deleted file mode 100644 index 8b6d3650b..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './MembersTab' -export * from './ProfileCardMemberInfo' -export * from './StakingModal' diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/index.ts deleted file mode 100644 index ca93746a8..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useGovernanceTokenInfo' -export * from './useMainDaoInfoCards' -export * from './useStakingInfo' diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useGovernanceTokenInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useGovernanceTokenInfo.ts deleted file mode 100644 index ae3dc0be4..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useGovernanceTokenInfo.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { constSelector, useRecoilValue, waitForAll } from 'recoil' - -import { - DaoVotingNativeStakedSelectors, - genericTokenSelector, - nativeDenomBalanceSelector, - nativeSupplySelector, - usdPriceSelector, -} from '@dao-dao/state' -import { useCachedLoading } from '@dao-dao/stateless' -import { TokenType } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' - -import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { - UseGovernanceTokenInfoOptions, - UseGovernanceTokenInfoResponse, -} from '../types' - -export const useGovernanceTokenInfo = ({ - fetchWalletBalance = false, - fetchTreasuryBalance = false, - fetchUsdcPrice = false, -}: UseGovernanceTokenInfoOptions = {}): UseGovernanceTokenInfoResponse => { - const { chainId, coreAddress, votingModuleAddress } = - useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) - - const { denom } = useRecoilValue( - DaoVotingNativeStakedSelectors.getConfigSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], - }) - ) - - const [governanceToken, supply] = useRecoilValue( - waitForAll([ - genericTokenSelector({ - chainId, - type: TokenType.Native, - denomOrAddress: denom, - }), - nativeSupplySelector({ - chainId, - denom, - }), - ]) - ) - - /// Optional - - // Wallet balance - const loadingWalletBalance = useCachedLoading( - fetchWalletBalance && walletAddress - ? nativeDenomBalanceSelector({ - chainId, - walletAddress, - denom, - }) - : constSelector(undefined), - undefined - ) - - // Treasury balance - const loadingTreasuryBalance = useCachedLoading( - fetchTreasuryBalance - ? nativeDenomBalanceSelector({ - chainId, - walletAddress: coreAddress, - denom, - }) - : constSelector(undefined), - undefined - ) - - // Price info - const loadingPrice = useCachedLoading( - fetchUsdcPrice - ? usdPriceSelector({ - type: TokenType.Native, - chainId, - denomOrAddress: denom, - }) - : constSelector(undefined), - undefined - ) - - return { - governanceToken, - supply: convertMicroDenomToDenomWithDecimals( - supply, - governanceToken.decimals - ), - /// Optional - // Wallet balance - loadingWalletBalance: loadingWalletBalance.loading - ? { loading: true } - : !loadingWalletBalance.data - ? undefined - : { - loading: false, - data: Number(loadingWalletBalance.data.amount), - }, - // Treasury balance - loadingTreasuryBalance: loadingTreasuryBalance.loading - ? { loading: true } - : !loadingTreasuryBalance.data - ? undefined - : { - loading: false, - data: Number(loadingTreasuryBalance.data.amount), - }, - // Price - loadingPrice: loadingPrice.loading - ? { loading: true } - : !loadingPrice.data - ? undefined - : { - loading: false, - data: loadingPrice.data, - }, - } -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useMainDaoInfoCards.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useMainDaoInfoCards.tsx deleted file mode 100644 index ebbdc647f..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useMainDaoInfoCards.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useTranslation } from 'react-i18next' - -import { indexerQueries } from '@dao-dao/state' -import { TokenAmountDisplay } from '@dao-dao/stateless' -import { DaoInfoCard } from '@dao-dao/types' -import { - convertDurationToHumanReadableString, - convertMicroDenomToDenomWithDecimals, -} from '@dao-dao/utils' - -import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useGovernanceTokenInfo } from './useGovernanceTokenInfo' -import { useStakingInfo } from './useStakingInfo' - -export const useMainDaoInfoCards = (): DaoInfoCard[] => { - const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { loadingTotalStakedValue, unstakingDuration } = useStakingInfo({ - fetchTotalStakedValue: true, - }) - - if (loadingTotalStakedValue === undefined) { - throw new Error(t('error.loadingData')) - } - - const { - governanceToken: { decimals, symbol }, - supply, - } = useGovernanceTokenInfo() - - const queryClient = useQueryClient() - const loadingMembers = useQueryLoadingDataWithError( - indexerQueries.queryContract(queryClient, { - chainId, - contractAddress: votingModuleAddress, - formula: 'daoVotingNativeStaked/topStakers', - noFallback: true, - }) - ) - - return [ - { - label: t('title.members'), - tooltip: t('info.membersTooltip'), - loading: loadingMembers.loading, - value: loadingMembers.loading - ? undefined - : loadingMembers.errored - ? '<error>' - : loadingMembers.data?.length ?? '<error>', - }, - { - label: t('title.totalSupply'), - tooltip: t('info.totalSupplyTooltip', { - tokenSymbol: symbol, - }), - value: ( - <TokenAmountDisplay - amount={supply} - decimals={decimals} - symbol={symbol} - /> - ), - }, - { - label: t('title.totalStaked'), - tooltip: t('info.totalStakedTooltip', { - tokenSymbol: symbol, - }), - value: ( - <TokenAmountDisplay - amount={ - loadingTotalStakedValue.loading - ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingTotalStakedValue.data, - decimals - ), - } - } - decimals={decimals} - symbol={symbol} - /> - ), - }, - { - label: t('title.unstakingPeriod'), - tooltip: t('info.unstakingPeriodTooltip', { - tokenSymbol: symbol, - }), - value: unstakingDuration - ? convertDurationToHumanReadableString(t, unstakingDuration) - : t('info.none'), - }, - ] -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useStakingInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useStakingInfo.ts deleted file mode 100644 index c6449b7f8..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/hooks/useStakingInfo.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { useCallback } from 'react' -import { constSelector, useRecoilValue, useSetRecoilState } from 'recoil' - -import { - DaoVotingNativeStakedSelectors, - blockHeightSelector, - refreshClaimsIdAtom, - refreshWalletBalancesIdAtom, -} from '@dao-dao/state' -import { useCachedLoadable, useCachedLoading } from '@dao-dao/stateless' -import { claimAvailable } from '@dao-dao/utils' - -import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { UseStakingInfoOptions, UseStakingInfoResponse } from '../types' - -export const useStakingInfo = ({ - fetchClaims = false, - fetchTotalStakedValue = false, - fetchWalletStakedValue = false, -}: UseStakingInfoOptions = {}): UseStakingInfoResponse => { - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) - - const config = useRecoilValue( - DaoVotingNativeStakedSelectors.getConfigSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], - }) - ) - - const setRefreshTotalBalancesId = useSetRecoilState( - refreshWalletBalancesIdAtom(undefined) - ) - // Refresh totals, mostly for total staked power. - const refreshTotals = useCallback( - () => setRefreshTotalBalancesId((id) => id + 1), - [setRefreshTotalBalancesId] - ) - - /// Optional - - // Claims - const blockHeightLoadable = useCachedLoadable( - fetchClaims - ? blockHeightSelector({ - chainId, - }) - : undefined - ) - const blockHeight = - blockHeightLoadable.state === 'hasValue' - ? blockHeightLoadable.contents - : undefined - - const _setClaimsId = useSetRecoilState(refreshClaimsIdAtom(walletAddress)) - const refreshClaims = () => _setClaimsId((id) => id + 1) - - const loadingClaims = useCachedLoading( - fetchClaims && walletAddress - ? DaoVotingNativeStakedSelectors.claimsSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [{ address: walletAddress }], - }) - : constSelector(undefined), - undefined - ) - const claims = loadingClaims.loading - ? [] - : !loadingClaims.data - ? undefined - : loadingClaims.data.claims - - const claimsPending = blockHeight - ? claims?.filter((c) => !claimAvailable(c, blockHeight)) - : undefined - const claimsAvailable = blockHeight - ? claims?.filter((c) => claimAvailable(c, blockHeight)) - : undefined - const sumClaimsAvailable = claimsAvailable?.reduce( - (p, c) => p + Number(c.amount), - 0 - ) - - // Total staked value - const loadingTotalStakedValue = useCachedLoading( - fetchTotalStakedValue - ? DaoVotingNativeStakedSelectors.totalPowerAtHeightSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [{}], - }) - : constSelector(undefined), - undefined - ) - - // Wallet staked value - const loadingWalletStakedValue = useCachedLoading( - fetchWalletStakedValue && walletAddress - ? DaoVotingNativeStakedSelectors.votingPowerAtHeightSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [{ address: walletAddress }], - }) - : constSelector(undefined), - undefined - ) - - return { - stakingContractAddress: votingModuleAddress, - unstakingDuration: config.unstaking_duration ?? undefined, - refreshTotals, - /// Optional - // Claims - blockHeight, - refreshClaims: fetchClaims ? refreshClaims : undefined, - claims, - claimsPending, - claimsAvailable, - sumClaimsAvailable, - // Total staked value - loadingTotalStakedValue: loadingTotalStakedValue.loading - ? { loading: true } - : !loadingTotalStakedValue.data - ? undefined - : { - loading: false, - data: Number(loadingTotalStakedValue.data.power), - }, - // Wallet staked value - loadingWalletStakedValue: loadingWalletStakedValue.loading - ? { loading: true } - : !loadingWalletStakedValue.data - ? undefined - : { - loading: false, - data: Number(loadingWalletStakedValue.data.power), - }, - } -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/index.ts deleted file mode 100644 index 5ee03e09f..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { PeopleAltOutlined, PeopleAltRounded } from '@mui/icons-material' - -import { MainDaoInfoCardsTokenLoader } from '@dao-dao/stateless' -import { - ActionCategoryKey, - ActionKey, - DaoTabId, - VotingModuleAdapter, -} from '@dao-dao/types' -import { - DAO_VOTING_NATIVE_STAKED_CONTRACT_NAMES, - DaoVotingNativeStakedAdapterId, -} from '@dao-dao/utils' - -import { MintAction, UpdateStakingConfigAction } from './actions' -import { MembersTab, ProfileCardMemberInfo, StakingModal } from './components' -import { useMainDaoInfoCards } from './hooks' - -export const DaoVotingNativeStakedAdapter: VotingModuleAdapter = { - id: DaoVotingNativeStakedAdapterId, - contractNames: DAO_VOTING_NATIVE_STAKED_CONTRACT_NAMES, - - load: () => ({ - // Hooks - hooks: { - useMainDaoInfoCards, - useVotingModuleRelevantAddresses: () => [], - }, - - // Components - components: { - MainDaoInfoCardsLoader: MainDaoInfoCardsTokenLoader, - ProfileCardMemberInfo, - StakingModal, - - extraTabs: [ - { - id: DaoTabId.Members, - labelI18nKey: 'title.members', - Component: MembersTab, - Icon: PeopleAltOutlined, - IconFilled: PeopleAltRounded, - }, - ], - }, - - // Functions - fields: { - actions: { - actions: [MintAction, UpdateStakingConfigAction], - categoryMakers: [ - // Add to DAO Governance category. - () => ({ - key: ActionCategoryKey.DaoGovernance, - actionKeys: [ActionKey.Mint, ActionKey.UpdateStakingConfig], - }), - ], - }, - }, - }), -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/types.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/types.ts deleted file mode 100644 index b2ab2d052..000000000 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/types.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - Duration, - DurationWithUnits, - GenericToken, - GenericTokenWithUsdPrice, - LoadingData, -} from '@dao-dao/types' -import { Claim } from '@dao-dao/types/contracts/DaoVotingNativeStaked' - -export interface DaoCreationConfig { - denom: string - _tokenError?: string - unstakingDuration: DurationWithUnits -} - -export interface UseStakingInfoOptions { - fetchClaims?: boolean - fetchTotalStakedValue?: boolean - fetchWalletStakedValue?: boolean -} - -export interface UseStakingInfoResponse { - stakingContractAddress: string - unstakingDuration?: Duration - refreshTotals: () => void - /// Optional - // Claims - blockHeight?: number - refreshClaims?: () => void - claims?: Claim[] - claimsPending?: Claim[] - claimsAvailable?: Claim[] - sumClaimsAvailable?: number - // Total staked value - loadingTotalStakedValue?: LoadingData<number> - // Wallet staked value - loadingWalletStakedValue?: LoadingData<number> -} - -export type UseGovernanceTokenInfoOptions = { - /** - * Optionally fetch wallet balance. Defaults to false. - */ - fetchWalletBalance?: boolean - /** - * Optionally fetch treasury balance. Defaults to false. - */ - fetchTreasuryBalance?: boolean - /** - * Optionally fetch USDC price. Defaults to false. - */ - fetchUsdcPrice?: boolean -} - -export type UseGovernanceTokenInfoResponse = { - /** - * The generic governance token. - */ - governanceToken: GenericToken - /** - * The supply of the governance token converted to the appropriate decimals. - */ - supply: number - - // Optional, defined if options are set to true. - - /** - * Unstaked governance token balance. Only defined if a wallet is connected - * and the option to fetch this is true. - */ - loadingWalletBalance?: LoadingData<number> - /** - * The treasury balance of the governance token. Only defined if the option to - * fetch this is true. - */ - loadingTreasuryBalance?: LoadingData<number> - /** - * The price of the governance token. Only defined if the option to fetch this - * is true. - */ - loadingPrice?: LoadingData<GenericTokenWithUsdPrice> -} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/Component.tsx index 9e2fa6f60..b6fdbdd12 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/Component.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/Component.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { FormSwitchCard, InputErrorMessage, - NumberInput, + NumericInput, SelectInput, } from '@dao-dao/stateless' import { @@ -26,7 +26,7 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ isCreating, }) => { const { t } = useTranslation() - const { register, watch, setValue } = + const { register, watch, setValue, getValues } = useFormContext<UpdateStakingConfigData>() const unstakingDurationEnabled = watch( @@ -57,14 +57,16 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ {unstakingDurationEnabled && ( <> <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.unstakingDuration?.value} fieldName={ (fieldNamePrefix + 'unstakingDuration.value') as 'unstakingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="md" @@ -77,7 +79,6 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ : undefined } validation={[validatePositive, validateRequired]} - watch={watch} /> {isCreating && ( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/README.md index 74171ed2c..79c2dc74c 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/actions/UpdateStakingConfig/README.md @@ -17,7 +17,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). { "unstakingDurationEnabled": <true | false>, "unstakingDuration": { - "value": "<NUMBER>", + "value": <NUMBER>, "units": "<seconds | minutes | hours | days | weeks | months | years>" } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/MembersTab.tsx index e7f33143d..ed3ae5be6 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/MembersTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/MembersTab.tsx @@ -2,7 +2,10 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { daoVotingOnftStakedExtraQueries } from '@dao-dao/state' -import { MembersTab as StatelessMembersTab } from '@dao-dao/stateless' +import { + MembersTab as StatelessMembersTab, + useVotingModule, +} from '@dao-dao/stateless' import { StatefulDaoMemberCardProps } from '@dao-dao/types' import { @@ -14,18 +17,17 @@ import { useDaoGovernanceToken, useQueryLoadingDataWithError, } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' export const MembersTab = () => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const token = useDaoGovernanceToken() ?? undefined const queryClient = useQueryClient() const members = useQueryLoadingDataWithError( daoVotingOnftStakedExtraQueries.topStakers(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }), (data) => data?.map( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/ProfileCardMemberInfo.tsx index 42db582b0..db0cd5f85 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/ProfileCardMemberInfo.tsx @@ -3,16 +3,13 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { blockHeightSelector, blocksPerYearSelector, stakingLoadingAtom, } from '@dao-dao/state' -import { - useCachedLoadable, - useChain, - useDaoInfoContext, -} from '@dao-dao/stateless' +import { useCachedLoadable, useChain, useDao } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps, UnstakingTask, @@ -37,7 +34,7 @@ export const ProfileCardMemberInfo = ({ }: BaseProfileCardMemberInfoProps) => { const { t } = useTranslation() const { chain_id: chainId } = useChain() - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const { address: walletAddress, isWalletConnected, @@ -100,9 +97,10 @@ export const ProfileCardMemberInfo = ({ refreshClaims?.() toast.success( - `Claimed ${sumClaimsAvailable.toLocaleString()} $${ - collectionInfo.symbol - }` + t('success.claimedTokens', { + amount: sumClaimsAvailable.toLocaleString(), + tokenSymbol: collectionInfo.symbol, + }) ) } catch (err) { console.error(err) @@ -138,7 +136,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsPending ?? []).map(({ release_at }) => ({ token, status: UnstakingTaskStatus.Unstaking, - amount: Number(1), + amount: HugeDecimal.one, date: convertExpirationToDate( blocksPerYear, release_at, @@ -150,7 +148,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsAvailable ?? []).map(({ release_at }) => ({ token, status: UnstakingTaskStatus.ReadyToClaim, - amount: Number(1), + amount: HugeDecimal.one, date: convertExpirationToDate( blocksPerYear, release_at, @@ -193,10 +191,10 @@ export const ProfileCardMemberInfo = ({ ? { loading: true } : { loading: false, - data: - (loadingWalletStakedValue.data / - loadingTotalStakedValue.data) * - 100, + data: loadingWalletStakedValue.data + .div(loadingTotalStakedValue.data) + .times(100) + .toNumber(), } } onClaim={onClaim} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/StakingModal.tsx index 9676295c0..56e89cf29 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/StakingModal.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/components/StakingModal.tsx @@ -5,16 +5,12 @@ import { useTranslation } from 'react-i18next' import { useRecoilState } from 'recoil' import { stakingLoadingAtom } from '@dao-dao/state' -import { - ModalLoader, - SegmentedControls, - StakingMode, - useDaoContext, -} from '@dao-dao/stateless' +import { ModalLoader, SegmentedControls, useDao } from '@dao-dao/stateless' import { BaseStakingModalProps, LazyNftCardInfo, LoadingDataWithError, + StakingMode, } from '@dao-dao/types' import { MsgExecuteContract } from '@dao-dao/types/protobuf/codegen/cosmwasm/wasm/v1/tx' import { MsgTransferONFT } from '@dao-dao/types/protobuf/codegen/OmniFlix/onft/v1beta1/tx' @@ -47,7 +43,7 @@ const InnerStakingModal = ({ initialMode = StakingMode.Stake, }: BaseStakingModalProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { address: walletAddress, isWalletConnected, @@ -84,7 +80,7 @@ const InnerStakingModal = ({ const hasStake = loadingWalletStakedValue !== undefined && !loadingWalletStakedValue.loading && - loadingWalletStakedValue.data > 0 + loadingWalletStakedValue.data.isPositive() const walletStakedBalanceLoading = useQueryLoadingDataWithError( dao.votingModule.getVotingPowerQuery(walletAddress) @@ -166,7 +162,10 @@ const InnerStakingModal = ({ refreshTotals() toast.success( - `Staked ${stakeTokenIds.length} $${collectionInfo.symbol}` + t('success.stakedTokens', { + amount: stakeTokenIds.length, + tokenSymbol: collectionInfo.symbol, + }) ) setStakeTokenIds([]) @@ -210,7 +209,10 @@ const InnerStakingModal = ({ refreshClaims?.() toast.success( - `Unstaked ${unstakeTokenIds.length} $${collectionInfo.symbol}` + t('success.unstakedTokens', { + amount: unstakeTokenIds.length, + tokenSymbol: collectionInfo.symbol, + }) ) setUnstakeTokenIds([]) diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useGovernanceCollectionInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useGovernanceCollectionInfo.ts index f9f1a81ad..398c3ce23 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useGovernanceCollectionInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useGovernanceCollectionInfo.ts @@ -4,12 +4,13 @@ import { useSuspenseQuery, } from '@tanstack/react-query' +import { HugeDecimal } from '@dao-dao/math' import { daoVotingOnftStakedQueries, omniflixQueries } from '@dao-dao/state' +import { useVotingModule } from '@dao-dao/stateless' import { TokenType } from '@dao-dao/types' import { useQueryLoadingDataWithError } from '../../../../hooks' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseGovernanceCollectionInfoOptions, UseGovernanceCollectionInfoResponse, @@ -19,19 +20,16 @@ export const useGovernanceCollectionInfo = ({ fetchWalletBalance = false, fetchTreasuryBalance = false, }: UseGovernanceCollectionInfoOptions = {}): UseGovernanceCollectionInfoResponse => { - const { chainId, coreAddress, votingModuleAddress } = - useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) + const votingModule = useVotingModule() + const { address: walletAddress } = useWallet() const queryClient = useQueryClient() const { data: { onft_collection_id }, } = useSuspenseQuery( daoVotingOnftStakedQueries.config(queryClient, { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, }) ) @@ -43,11 +41,11 @@ export const useGovernanceCollectionInfo = ({ ] = useSuspenseQueries({ queries: [ omniflixQueries.onftCollectionInfo({ - chainId, + chainId: votingModule.chainId, id: onft_collection_id, }), omniflixQueries.onftCollectionSupply({ - chainId, + chainId: votingModule.chainId, id: onft_collection_id, }), ], @@ -60,7 +58,7 @@ export const useGovernanceCollectionInfo = ({ omniflixQueries.onftCollectionSupply( fetchWalletBalance && walletAddress ? { - chainId, + chainId: votingModule.chainId, id: onft_collection_id, owner: walletAddress, } @@ -73,31 +71,31 @@ export const useGovernanceCollectionInfo = ({ omniflixQueries.onftCollectionSupply( fetchTreasuryBalance ? { - chainId, + chainId: votingModule.chainId, id: onft_collection_id, - owner: coreAddress, + owner: votingModule.dao.coreAddress, } : undefined ) ) return { - stakingContractAddress: votingModuleAddress, + stakingContractAddress: votingModule.address, collectionAddress: onft_collection_id, collectionInfo: { name, symbol, - totalSupply, + totalSupply: HugeDecimal.from(totalSupply), }, token: { - chainId, + chainId: votingModule.chainId, type: TokenType.Onft, denomOrAddress: onft_collection_id, symbol, decimals: 0, imageUrl: previewUri, source: { - chainId, + chainId: votingModule.chainId, type: TokenType.Onft, denomOrAddress: onft_collection_id, }, @@ -111,7 +109,7 @@ export const useGovernanceCollectionInfo = ({ ? { loading: true } : { loading: false, - data: loadingWalletBalance.data, + data: HugeDecimal.from(loadingWalletBalance.data), }, // Treasury balance loadingTreasuryBalance: @@ -121,7 +119,7 @@ export const useGovernanceCollectionInfo = ({ ? { loading: true } : { loading: false, - data: loadingTreasuryBalance.data, + data: HugeDecimal.from(loadingTreasuryBalance.data), }, } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useMainDaoInfoCards.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useMainDaoInfoCards.tsx index 0dab5d00d..65e00afb6 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useMainDaoInfoCards.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useMainDaoInfoCards.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { daoVotingOnftStakedExtraQueries } from '@dao-dao/state' -import { TokenAmountDisplay } from '@dao-dao/stateless' +import { TokenAmountDisplay, useVotingModule } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' import { convertDurationToHumanReadableString, @@ -10,13 +10,12 @@ import { } from '@dao-dao/utils' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceCollectionInfo } from './useGovernanceCollectionInfo' import { useStakingInfo } from './useStakingInfo' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { loadingTotalStakedValue, unstakingDuration } = useStakingInfo({ fetchTotalStakedValue: true, @@ -33,8 +32,8 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { const queryClient = useQueryClient() const loadingMembers = useQueryLoadingDataWithError( daoVotingOnftStakedExtraQueries.topStakers(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }) ) @@ -66,7 +65,7 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { value: loadingTotalStakedValue.loading ? '...' : formatPercentOf100( - (loadingTotalStakedValue.data / totalSupply) * 100 + loadingTotalStakedValue.data.div(totalSupply).times(100).toNumber() ), }, { diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useStakingInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useStakingInfo.ts index 1ef6ea285..1fb26790d 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useStakingInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/hooks/useStakingInfo.ts @@ -2,6 +2,7 @@ import { useQueryClient, useSuspenseQueries } from '@tanstack/react-query' import { useCallback } from 'react' import { useSetRecoilState, waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { blockHeightSelector, contractQueries, @@ -14,7 +15,7 @@ import { import { useCachedLoadable, useCachedLoadingWithError, - useDaoContext, + useDao, } from '@dao-dao/stateless' import { NftClaim } from '@dao-dao/types/contracts/DaoVotingOnftStaked' import { claimAvailable, parseContractVersion } from '@dao-dao/utils' @@ -24,7 +25,6 @@ import { useQueryLoadingDataWithError, } from '../../../../hooks' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseStakingInfoOptions, UseStakingInfoResponse } from '../types' import { useGovernanceCollectionInfo } from './useGovernanceCollectionInfo' @@ -34,24 +34,22 @@ export const useStakingInfo = ({ fetchWalletStakedValue = false, fetchWalletUnstakedNfts = false, }: UseStakingInfoOptions = {}): UseStakingInfoResponse => { - const { dao } = useDaoContext() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) - + const dao = useDao() + const { address: walletAddress } = useWallet() const { collectionAddress } = useGovernanceCollectionInfo() - const queryClient = useQueryClient() + + const { votingModule } = dao + const [stakingContractVersion, unstakingDuration] = useSuspenseQueries({ queries: [ contractQueries.info(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }), daoVotingOnftStakedQueries.config(queryClient, { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, }), ], combine: ([ @@ -86,7 +84,7 @@ export const useStakingInfo = ({ }) queryClient.invalidateQueries({ queryKey: omniflixQueries.onftCollectionSupply({ - chainId, + chainId: votingModule.chainId, id: collectionAddress, }).queryKey, }) @@ -95,7 +93,7 @@ export const useStakingInfo = ({ 'omniflix', 'paginatedOnfts', { - chainId, + chainId: votingModule.chainId, id: collectionAddress, }, ], @@ -105,7 +103,7 @@ export const useStakingInfo = ({ 'omniflix', 'allOnfts', { - chainId, + chainId: votingModule.chainId, id: collectionAddress, }, ], @@ -115,8 +113,8 @@ export const useStakingInfo = ({ 'indexer', 'query', { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, formula: 'daoVotingOnftStaked/topStakers', }, ], @@ -128,8 +126,8 @@ export const useStakingInfo = ({ 'indexer', 'query', { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, formula: 'daoVotingOnftStaked/stakedNfts', args: { address: walletAddress, @@ -140,20 +138,19 @@ export const useStakingInfo = ({ // Then invalidate contract query that uses indexer query. queryClient.invalidateQueries({ queryKey: daoVotingOnftStakedQueryKeys.stakedNfts( - chainId, - votingModuleAddress, + votingModule.chainId, + votingModule.address, { address: walletAddress, } ), }) }, [ - chainId, + votingModule, dao, collectionAddress, queryClient, setRefreshDaoVotingPower, - votingModuleAddress, walletAddress, ]) @@ -163,7 +160,7 @@ export const useStakingInfo = ({ const blockHeightLoadable = useCachedLoadable( fetchClaims ? blockHeightSelector({ - chainId, + chainId: votingModule.chainId, }) : undefined ) @@ -177,8 +174,8 @@ export const useStakingInfo = ({ 'indexer', 'query', { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, formula: 'daoVotingOnftStaked/nftClaims', args: { address: walletAddress, @@ -189,19 +186,19 @@ export const useStakingInfo = ({ // Then invalidate contract query that uses indexer query. queryClient.invalidateQueries({ queryKey: daoVotingOnftStakedQueryKeys.nftClaims( - chainId, - votingModuleAddress, + votingModule.chainId, + votingModule.address, { address: walletAddress, } ), }) - }, [chainId, queryClient, votingModuleAddress, walletAddress]) + }, [votingModule, queryClient, walletAddress]) const loadingClaims = useQueryLoadingData( daoVotingOnftStakedQueries.nftClaims(queryClient, { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, args: { address: walletAddress || '', }, @@ -226,7 +223,7 @@ export const useStakingInfo = ({ const claimsAvailable = blockHeight ? nftClaims?.filter((c) => claimAvailable(c, blockHeight)) : undefined - const sumClaimsAvailable = claimsAvailable?.length + const sumClaimsAvailable = HugeDecimal.from(claimsAvailable?.length || 0) // Total staked value const loadingTotalStakedValue = useQueryLoadingDataWithError({ @@ -237,8 +234,8 @@ export const useStakingInfo = ({ // Wallet staked value const loadingWalletStakedNftIds = useQueryLoadingDataWithError( daoVotingOnftStakedQueries.stakedNfts(queryClient, { - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, args: { address: walletAddress ?? '', }, @@ -253,7 +250,7 @@ export const useStakingInfo = ({ ? waitForAll( loadingWalletStakedNftIds.data.map((tokenId) => nftCardInfoSelector({ - chainId, + chainId: votingModule.chainId, collection: collectionAddress, tokenId, }) @@ -264,7 +261,7 @@ export const useStakingInfo = ({ const loadingWalletUnstakedOnfts = useQueryLoadingDataWithError({ ...omniflixQueries.allOnfts(queryClient, { - chainId, + chainId: votingModule.chainId, id: collectionAddress, owner: walletAddress ?? '', }), @@ -276,7 +273,7 @@ export const useStakingInfo = ({ ? waitForAll( loadingWalletUnstakedOnfts.data.map(({ id }) => nftCardInfoSelector({ - chainId, + chainId: votingModule.chainId, collection: collectionAddress, tokenId: id, }) @@ -287,7 +284,7 @@ export const useStakingInfo = ({ return { stakingContractVersion, - stakingContractAddress: votingModuleAddress, + stakingContractAddress: votingModule.address, unstakingDuration, refreshTotals, /// Optional @@ -309,7 +306,7 @@ export const useStakingInfo = ({ ? { loading: true } : { loading: false, - data: Number(loadingTotalStakedValue.data.power), + data: HugeDecimal.from(loadingTotalStakedValue.data.power), }, // Wallet staked value loadingWalletStakedValue: @@ -319,7 +316,7 @@ export const useStakingInfo = ({ ? { loading: true } : { loading: false, - data: loadingWalletStakedNftIds.data.length, + data: HugeDecimal.from(loadingWalletStakedNftIds.data.length), }, loadingWalletStakedNfts, loadingWalletUnstakedNfts, diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/types.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/types.ts index 67b25e23b..a5e89eb65 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/types.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingOnftStaked/types.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { ContractVersion, Duration, @@ -27,11 +28,11 @@ export interface UseStakingInfoResponse { claims?: NftClaim[] claimsPending?: NftClaim[] claimsAvailable?: NftClaim[] - sumClaimsAvailable?: number + sumClaimsAvailable?: HugeDecimal // Total staked value - loadingTotalStakedValue?: LoadingData<number> + loadingTotalStakedValue?: LoadingData<HugeDecimal> // Wallet staked value - loadingWalletStakedValue?: LoadingData<number> + loadingWalletStakedValue?: LoadingData<HugeDecimal> loadingWalletStakedNfts?: LoadingDataWithError<NftCardInfo[]> loadingWalletUnstakedNfts?: LoadingDataWithError<NftCardInfo[]> } @@ -48,14 +49,14 @@ export interface UseGovernanceCollectionInfoResponse { collectionInfo: { name: string symbol: string - totalSupply: number + totalSupply: HugeDecimal } token: GenericToken /// Optional // Wallet balance - loadingWalletBalance?: LoadingData<number> + loadingWalletBalance?: LoadingData<HugeDecimal> // Treasury balance - loadingTreasuryBalance?: LoadingData<number> + loadingTreasuryBalance?: LoadingData<HugeDecimal> // Price // loadingPrice?: LoadingData<GenericTokenWithUsdPrice> } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/MembersTab.tsx index aa346a36b..10fc4ef4a 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/MembersTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/MembersTab.tsx @@ -2,7 +2,10 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { daoVotingSgCommunityNftExtraQueries } from '@dao-dao/state' -import { MembersTab as StatelessMembersTab } from '@dao-dao/stateless' +import { + MembersTab as StatelessMembersTab, + useVotingModule, +} from '@dao-dao/stateless' import { StatefulDaoMemberCardProps } from '@dao-dao/types' import { @@ -11,17 +14,16 @@ import { EntityDisplay, } from '../../../../components' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' export const MembersTab = () => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const queryClient = useQueryClient() const members = useQueryLoadingDataWithError( daoVotingSgCommunityNftExtraQueries.allVoters(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }), (data) => data?.map( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/ProfileCardMemberInfo.tsx index 00291b00e..143925dd8 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/components/ProfileCardMemberInfo.tsx @@ -10,7 +10,7 @@ import { daoVotingSgCommunityNftQueries, indexerQueries, } from '@dao-dao/state/query' -import { Button, useDaoContext } from '@dao-dao/stateless' +import { Button, useDao } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps, LoadingDataWithError, @@ -33,7 +33,7 @@ export const ProfileCardMemberInfo = ({ cantVoteOnProposal, }: BaseProfileCardMemberInfoProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { address: walletAddress, isWalletConnected, diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/hooks/useMainDaoInfoCards.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/hooks/useMainDaoInfoCards.ts index 49ed2e8a8..b6e1ff79b 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/hooks/useMainDaoInfoCards.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingSgCommunityNft/hooks/useMainDaoInfoCards.ts @@ -2,20 +2,20 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { daoVotingSgCommunityNftExtraQueries } from '@dao-dao/state' +import { useVotingModule } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const queryClient = useQueryClient() const loadingMembers = useQueryLoadingDataWithError( daoVotingSgCommunityNftExtraQueries.allVoters(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }) ) diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/README.md index e7d3e936b..bb8bed8f2 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/README.md @@ -4,12 +4,18 @@ This is the voting module adapter for the [`dao-voting-token-staked`](https://github.com/DA0-DA0/dao-contracts/tree/95e4f73a170705deeb3689341c51f3a3b126e3e9/contracts/voting/dao-voting-token-staked) contract, which determines DAO voting power based on the staked balance of a native token (IBC, factory, or otherwise), such as `JUNO` or -`factory/junoContract/subdenom`. This is an alternative to the +`factory/junoContract/subdenom`. + +This is an alternative to the [CW20](https://docs.cosmwasm.com/cw-plus/0.9.0/cw20/spec) governance token-based DAO structure, where members are still free to exchange their unstaked tokens with other parties at any time. However, it uses a native token instead of a [CW20](https://docs.cosmwasm.com/cw-plus/0.9.0/cw20/spec) token. +This also supports the deprecated +[`dao-voting-native-staked`](https://github.com/DA0-DA0/dao-contracts/tree/7f89ad1604e8022f202aef729853b0c8c7196988/contracts/voting/dao-voting-native-staked) +contract. + ## Layout | Location | Summary | diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/MintComponent.stories.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/MintComponent.stories.tsx similarity index 92% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/MintComponent.stories.tsx rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/MintComponent.stories.tsx index 141d86e1a..e5886bacb 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/MintComponent.stories.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/MintComponent.stories.tsx @@ -7,11 +7,11 @@ 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 / DaoVotingNativeStakedMint', component: MintComponent, decorators: [ makeReactHookFormDecorator<MintData>({ - amount: 100000, + amount: '100000', }), ], } as ComponentMeta<typeof MintComponent> diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/MintComponent.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/MintComponent.tsx similarity index 65% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/MintComponent.tsx rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/MintComponent.tsx index 5051a6837..31504066d 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/MintComponent.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/MintComponent.tsx @@ -1,16 +1,13 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { InputErrorMessage, NumberInput } from '@dao-dao/stateless' +import { HugeDecimal } from '@dao-dao/math' +import { InputErrorMessage, NumericInput } from '@dao-dao/stateless' import { ActionComponent, GenericToken } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - validatePositive, - validateRequired, -} from '@dao-dao/utils' +import { validatePositive, validateRequired } from '@dao-dao/utils' export type MintData = { - amount: number + amount: string } export type MintOptions = { @@ -24,23 +21,23 @@ export const MintComponent: ActionComponent<MintOptions> = ({ options: { govToken }, }) => { const { t } = useTranslation() - const { register, watch, setValue } = useFormContext() + const { register, setValue, getValues } = useFormContext<MintData>() return ( <> - <NumberInput + <NumericInput containerClassName="w-full" disabled={!isCreating} error={errors?.amount} - fieldName={fieldNamePrefix + 'amount'} - min={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)} + fieldName={(fieldNamePrefix + 'amount') as 'amount'} + getValues={getValues} + min={HugeDecimal.one.toHumanReadableNumber(govToken.decimals)} register={register} setValue={setValue} sizing="none" - step={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)} + step={HugeDecimal.one.toHumanReadableNumber(govToken.decimals)} unit={'$' + govToken.symbol} validation={[validateRequired, validatePositive]} - watch={watch} /> {errors?.amount && ( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/README.md similarity index 78% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/README.md rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/README.md index ebb1e7824..b037b7d41 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/README.md @@ -1,6 +1,6 @@ # Mint -Mint new governance tokens. +Mint new governance tokens for the dao-voting-native-staked module. ## Bulk import format @@ -15,7 +15,6 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). ```json { - "to": "<RECIPIENT ADDRESS>", "amount": "<AMOUNT>" } ``` diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/index.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/index.tsx similarity index 85% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/index.tsx rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/index.tsx index 97a7ee39a..17dd8e04e 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingNativeStaked/actions/Mint/index.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingNativeStakedMint/index.tsx @@ -1,5 +1,6 @@ import { coin } from '@cosmjs/stargate' +import { HugeDecimal } from '@dao-dao/math' import { ActionBase, HerbEmoji } from '@dao-dao/stateless' import { GenericToken, @@ -15,11 +16,7 @@ import { ProcessedMessage, } from '@dao-dao/types/actions' import { MsgMint } from '@dao-dao/types/protobuf/codegen/osmosis/tokenfactory/v1beta1/tx' -import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, - isDecodedStargateMsg, -} from '@dao-dao/utils' +import { isDecodedStargateMsg } from '@dao-dao/utils' import { useGovernanceTokenInfo } from '../../hooks' import { @@ -40,12 +37,12 @@ const Component: ActionComponent = (props) => { ) } -export class MintAction extends ActionBase<MintData> { +export class DaoVotingNativeStakedMintAction extends ActionBase<MintData> { public readonly key = ActionKey.Mint public readonly Component = Component protected _defaults: MintData = { - amount: 1, + amount: '1', } private governanceToken?: GenericToken @@ -83,10 +80,10 @@ export class MintAction extends ActionBase<MintData> { value: MsgMint.fromPartial({ sender: this.options.address, amount: coin( - convertDenomToMicroDenomStringWithDecimals( + HugeDecimal.fromHumanReadable( amount, this.governanceToken.decimals - ), + ).toString(), this.governanceToken.denomOrAddress ), }), @@ -109,10 +106,9 @@ export class MintAction extends ActionBase<MintData> { } return { - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.stargate.value.amount.amount, - this.governanceToken.decimals - ), + amount: HugeDecimal.from( + decodedMessage.stargate.value.amount.amount + ).toHumanReadableString(this.governanceToken.decimals), } } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/BitSongFantokenMintAction.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/BitSongFantokenMintAction.ts similarity index 81% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/BitSongFantokenMintAction.ts rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/BitSongFantokenMintAction.ts index 36734cb85..e824c29aa 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/BitSongFantokenMintAction.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/BitSongFantokenMintAction.ts @@ -1,5 +1,4 @@ -import { coin } from '@cosmjs/amino' - +import { HugeDecimal } from '@dao-dao/math' import { tokenQueries } from '@dao-dao/state/query' import { ActionBase, HerbEmoji } from '@dao-dao/stateless' import { @@ -15,11 +14,7 @@ import { ProcessedMessage, } from '@dao-dao/types/actions' import { MsgMint } from '@dao-dao/types/protobuf/codegen/bitsong/fantoken/v1beta1/tx' -import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, - isDecodedStargateMsg, -} from '@dao-dao/utils' +import { isDecodedStargateMsg } from '@dao-dao/utils' import { Component } from './Component' import { MintData } from './MintComponent' @@ -71,26 +66,24 @@ export class BitSongFantokenMintAction extends ActionBase<MintData> { this.defaults = { recipient: this.options.address, - amount: 1, + amount: '1', } } - encode({ recipient, amount: _amount }: MintData): UnifiedCosmosMsg { + encode({ recipient, amount }: MintData): UnifiedCosmosMsg { if (!this.governanceToken) { throw new Error('Action not ready') } - const amount = convertDenomToMicroDenomStringWithDecimals( - _amount, - this.governanceToken.decimals - ) - return makeStargateMessage({ stargate: { typeUrl: MsgMint.typeUrl, value: MsgMint.fromPartial({ recipient, - coin: coin(amount, this.governanceToken.denomOrAddress), + coin: HugeDecimal.fromHumanReadable( + amount, + this.governanceToken.decimals + ).toCoin(this.governanceToken.denomOrAddress), minter: this.options.address, }), }, @@ -115,10 +108,9 @@ export class BitSongFantokenMintAction extends ActionBase<MintData> { return { recipient: decodedMessage.stargate.value.recipient, - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.stargate.value.coin.amount, - this.governanceToken.decimals - ), + amount: HugeDecimal.from( + decodedMessage.stargate.value.coin.amount + ).toHumanReadableString(this.governanceToken.decimals), } } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/Component.tsx similarity index 100% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/Component.tsx rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/Component.tsx diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintAction.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintAction.ts similarity index 91% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintAction.ts rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintAction.ts index 4ece07468..b711cf560 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintAction.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintAction.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { daoVotingTokenStakedExtraQueries } from '@dao-dao/state/query' import { ActionBase, HerbEmoji } from '@dao-dao/stateless' import { GenericToken, UnifiedCosmosMsg } from '@dao-dao/types' @@ -9,8 +10,6 @@ import { ProcessedMessage, } from '@dao-dao/types/actions' import { - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, makeExecuteSmartContractMessage, objectMatchesStructure, } from '@dao-dao/utils' @@ -21,7 +20,7 @@ import { MintData } from './MintComponent' /** * A mint action for tokenfactory tokens. */ -export class MintAction extends ActionBase<MintData> { +export class DaoVotingTokenStakedMintAction extends ActionBase<MintData> { public readonly key = ActionKey.Mint public readonly Component = Component @@ -39,7 +38,7 @@ export class MintAction extends ActionBase<MintData> { this.defaults = { recipient: options.address, - amount: 1, + amount: '1', } // Fire async init immediately since we may hide this action. @@ -78,10 +77,10 @@ export class MintAction extends ActionBase<MintData> { throw new Error('Action not ready') } - const amount = convertDenomToMicroDenomStringWithDecimals( + const amount = HugeDecimal.fromHumanReadable( _amount, this.governanceToken.decimals - ) + ).toString() return [ // Set DAO minter allowance to the amount we're about to mint. @@ -160,10 +159,9 @@ export class MintAction extends ActionBase<MintData> { return { recipient: decodedMessage.wasm.execute.msg.mint.to_address, - amount: convertMicroDenomToDenomWithDecimals( - decodedMessage.wasm.execute.msg.mint.amount, - this.governanceToken.decimals - ), + amount: HugeDecimal.from( + decodedMessage.wasm.execute.msg.mint.amount + ).toHumanReadableString(this.governanceToken.decimals), } } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintComponent.stories.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintComponent.stories.tsx similarity index 92% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintComponent.stories.tsx rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintComponent.stories.tsx index 3a89a311d..7bcc9d1b9 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintComponent.stories.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintComponent.stories.tsx @@ -8,12 +8,12 @@ import { MintComponent, MintData } from './MintComponent' export default { title: - 'DAO DAO / packages / stateful / voting-module-adapter / adapters / DaoVotingTokenStaked / actions / Mint', + 'DAO DAO / packages / stateful / voting-module-adapter / adapters / DaoVotingTokenStaked / actions / DaoVotingTokenStakedMint', component: MintComponent, decorators: [ makeReactHookFormDecorator<MintData>({ recipient: 'address', - amount: 100000, + amount: '100000', }), ], } as ComponentMeta<typeof MintComponent> diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintComponent.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintComponent.tsx similarity index 88% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintComponent.tsx rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintComponent.tsx index 8480a9025..565305541 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/MintComponent.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/MintComponent.tsx @@ -7,9 +7,10 @@ import { ComponentType } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { InputErrorMessage, - NumberInput, + NumericInput, StatusCard, useChain, useDetectWrap, @@ -20,7 +21,6 @@ import { GenericToken, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, makeValidateAddress, validatePositive, validateRequired, @@ -28,7 +28,7 @@ import { export type MintData = { recipient: string - amount: number + amount: string } export type MintOptions = { @@ -43,7 +43,7 @@ export const MintComponent: ActionComponent<MintOptions> = ({ options: { govToken, AddressInput }, }) => { const { t } = useTranslation() - const { register, watch, setValue } = useFormContext<MintData>() + const { register, setValue, getValues } = useFormContext<MintData>() const { bech32_prefix: bech32Prefix } = useChain() const { containerRef, childRef, wrapped } = useDetectWrap() @@ -61,17 +61,17 @@ export const MintComponent: ActionComponent<MintOptions> = ({ className="flex min-w-0 flex-row flex-wrap items-stretch justify-between gap-x-3 gap-y-1" ref={containerRef} > - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.amount} fieldName={(fieldNamePrefix + 'amount') as 'amount'} - min={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)} + getValues={getValues} + min={HugeDecimal.one.toHumanReadableNumber(govToken.decimals)} register={register} setValue={setValue} - step={convertMicroDenomToDenomWithDecimals(1, govToken.decimals)} + step={HugeDecimal.one.toHumanReadableNumber(govToken.decimals)} unit={'$' + govToken.symbol} validation={[validateRequired, validatePositive]} - watch={watch} /> <div diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/README.md similarity index 81% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/README.md rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/README.md index 101c63406..198ec9f71 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/README.md @@ -1,6 +1,6 @@ # Mint -Mint new governance tokens. +Mint new governance tokens for the dao-voting-token-staked module. ## Bulk import format diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/index.ts similarity index 100% rename from packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/Mint/index.ts rename to packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/DaoVotingTokenStakedMint/index.ts diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/Component.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/Component.tsx index 9e2fa6f60..b6fdbdd12 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/Component.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/Component.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { FormSwitchCard, InputErrorMessage, - NumberInput, + NumericInput, SelectInput, } from '@dao-dao/stateless' import { @@ -26,7 +26,7 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ isCreating, }) => { const { t } = useTranslation() - const { register, watch, setValue } = + const { register, watch, setValue, getValues } = useFormContext<UpdateStakingConfigData>() const unstakingDurationEnabled = watch( @@ -57,14 +57,16 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ {unstakingDurationEnabled && ( <> <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput disabled={!isCreating} error={errors?.unstakingDuration?.value} fieldName={ (fieldNamePrefix + 'unstakingDuration.value') as 'unstakingDuration.value' } + getValues={getValues} min={1} + numericValue register={register} setValue={setValue} sizing="md" @@ -77,7 +79,6 @@ export const UpdateStakingConfigComponent: ActionComponent = ({ : undefined } validation={[validatePositive, validateRequired]} - watch={watch} /> {isCreating && ( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/README.md b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/README.md index 74171ed2c..79c2dc74c 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/README.md +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/UpdateStakingConfig/README.md @@ -17,7 +17,7 @@ guide](https://github.com/DA0-DA0/dao-dao-ui/wiki/Bulk-importing-actions). { "unstakingDurationEnabled": <true | false>, "unstakingDuration": { - "value": "<NUMBER>", + "value": <NUMBER>, "units": "<seconds | minutes | hours | days | weeks | months | years>" } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/index.ts index 40591c371..51fb505b1 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/index.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/actions/index.ts @@ -1,2 +1,3 @@ -export * from './Mint' +export * from './DaoVotingNativeStakedMint' +export * from './DaoVotingTokenStakedMint' export * from './UpdateStakingConfig' diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/MembersTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/MembersTab.tsx index 8d161249e..ce8d60f40 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/MembersTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/MembersTab.tsx @@ -1,31 +1,37 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { indexerQueries } from '@dao-dao/state/query' -import { MembersTab as StatelessMembersTab } from '@dao-dao/stateless' +import { + MembersTab as StatelessMembersTab, + useVotingModule, +} from '@dao-dao/stateless' import { StatefulDaoMemberCardProps } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' +import { TokenStakedVotingModule } from '../../../../clients' import { ButtonLink, DaoMemberCard, EntityDisplay, } from '../../../../components' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo } from '../hooks/useGovernanceTokenInfo' export const MembersTab = () => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { governanceToken } = useGovernanceTokenInfo() const queryClient = useQueryClient() const members = useQueryLoadingDataWithError( indexerQueries.queryContract(queryClient, { - chainId, - contractAddress: votingModuleAddress, - formula: 'daoVotingTokenStaked/topStakers', + chainId: votingModule.chainId, + contractAddress: votingModule.address, + formula: + votingModule instanceof TokenStakedVotingModule + ? 'daoVotingTokenStaked/topStakers' + : 'daoVotingNativeStaked/topStakers', noFallback: true, }), (data) => @@ -40,10 +46,7 @@ export const MembersTab = () => { balance: { loading: false, data: { - amount: convertMicroDenomToDenomWithDecimals( - balance, - governanceToken.decimals - ), + amount: HugeDecimal.from(balance), token: governanceToken, }, }, diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/ProfileCardMemberInfo.tsx index b787ce088..560c3574f 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/ProfileCardMemberInfo.tsx @@ -3,12 +3,13 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { useRecoilValue } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { blockHeightSelector, blocksPerYearSelector, stakingLoadingAtom, } from '@dao-dao/state' -import { useCachedLoadable, useDaoInfoContext } from '@dao-dao/stateless' +import { useCachedLoadable, useDao, useVotingModule } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps, UnstakingTask, @@ -16,7 +17,6 @@ import { } from '@dao-dao/types' import { convertExpirationToDate, - convertMicroDenomToDenomWithDecimals, durationToSeconds, processError, } from '@dao-dao/utils' @@ -27,7 +27,6 @@ import { useWallet, } from '../../../../hooks' import { ProfileCardMemberInfoTokens } from '../../../components' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo, useStakingInfo } from '../hooks' import { StakingModal } from './StakingModal' @@ -36,15 +35,13 @@ export const ProfileCardMemberInfo = ({ ...props }: BaseProfileCardMemberInfoProps) => { const { t } = useTranslation() - const { name: daoName } = useDaoInfoContext() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const { name: daoName } = useDao() + const votingModule = useVotingModule() const { address: walletAddress, isWalletConnected, refreshBalances, - } = useWallet({ - chainId, - }) + } = useWallet() const [showStakingModal, setShowStakingModal] = useState(false) const [claimingLoading, setClaimingLoading] = useState(false) @@ -60,7 +57,7 @@ export const ProfileCardMemberInfo = ({ refreshTotals, claimsPending, claimsAvailable, - sumClaimsAvailable, + sumClaimsAvailable = HugeDecimal.zero, loadingWalletStakedValue, loadingTotalStakedValue, refreshClaims, @@ -71,7 +68,7 @@ export const ProfileCardMemberInfo = ({ }) const doClaim = DaoVotingTokenStakedHooks.useClaim({ - contractAddress: votingModuleAddress, + contractAddress: votingModule.address, sender: walletAddress ?? '', }) @@ -80,7 +77,7 @@ export const ProfileCardMemberInfo = ({ if (!isWalletConnected) { return toast.error(t('error.logInToContinue')) } - if (!sumClaimsAvailable) { + if (sumClaimsAvailable.isZero()) { return toast.error(t('error.noClaimsAvailable')) } @@ -96,12 +93,12 @@ export const ProfileCardMemberInfo = ({ refreshClaims?.() toast.success( - `Claimed ${convertMicroDenomToDenomWithDecimals( - sumClaimsAvailable, - governanceToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.claimedTokens', { + amount: sumClaimsAvailable.toInternationalizedHumanReadableString({ + decimals: governanceToken.decimals, + }), + tokenSymbol: governanceToken.symbol, + }) ) } catch (err) { console.error(err) @@ -124,12 +121,12 @@ export const ProfileCardMemberInfo = ({ const blockHeightLoadable = useCachedLoadable( blockHeightSelector({ - chainId, + chainId: votingModule.chainId, }) ) const blocksPerYear = useRecoilValue( blocksPerYearSelector({ - chainId, + chainId: votingModule.chainId, }) ) @@ -137,10 +134,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsPending ?? []).map(({ amount, release_at }) => ({ token: governanceToken, status: UnstakingTaskStatus.Unstaking, - amount: convertMicroDenomToDenomWithDecimals( - amount, - governanceToken.decimals - ), + amount: HugeDecimal.from(amount), date: convertExpirationToDate( blocksPerYear, release_at, @@ -152,10 +146,7 @@ export const ProfileCardMemberInfo = ({ ...(claimsAvailable ?? []).map(({ amount, release_at }) => ({ token: governanceToken, status: UnstakingTaskStatus.ReadyToClaim, - amount: convertMicroDenomToDenomWithDecimals( - amount, - governanceToken.decimals - ), + amount: HugeDecimal.from(amount), date: convertExpirationToDate( blocksPerYear, release_at, @@ -184,14 +175,8 @@ export const ProfileCardMemberInfo = ({ data: [ { token: governanceToken, - staked: convertMicroDenomToDenomWithDecimals( - loadingWalletStakedValue.data, - governanceToken.decimals - ), - unstaked: convertMicroDenomToDenomWithDecimals( - loadingUnstakedBalance.data, - governanceToken.decimals - ), + staked: loadingWalletStakedValue.data, + unstaked: loadingUnstakedBalance.data, }, ], } @@ -204,10 +189,10 @@ export const ProfileCardMemberInfo = ({ ? { loading: true } : { loading: false, - data: - (loadingWalletStakedValue.data / - loadingTotalStakedValue.data) * - 100, + data: loadingWalletStakedValue.data + .div(loadingTotalStakedValue.data) + .times(100) + .toNumber(), } } onClaim={onClaim} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/StakingModal.tsx index d1bf20420..a6cee4f04 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/StakingModal.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/components/StakingModal.tsx @@ -1,9 +1,9 @@ -import { coins } from '@cosmjs/stargate' import { useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { useRecoilState, useSetRecoilState } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { refreshDaoVotingPowerAtom, refreshFollowingDaosAtom, @@ -11,16 +11,11 @@ import { } from '@dao-dao/state' import { ModalLoader, - StakingMode, StakingModal as StatelessStakingModal, + useVotingModule, } from '@dao-dao/stateless' -import { BaseStakingModalProps } from '@dao-dao/types' -import { - CHAIN_GAS_MULTIPLIER, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, - processError, -} from '@dao-dao/utils' +import { BaseStakingModalProps, StakingMode } from '@dao-dao/types' +import { CHAIN_GAS_MULTIPLIER, processError } from '@dao-dao/utils' import { SuspenseLoader } from '../../../../components' import { @@ -28,7 +23,6 @@ import { useAwaitNextBlock, useWallet, } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo, useStakingInfo } from '../hooks' export const StakingModal = (props: BaseStakingModalProps) => ( @@ -51,7 +45,7 @@ const InnerStakingModal = ({ isWalletConnected, refreshBalances, } = useWallet() - const { coreAddress, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const [stakingLoading, setStakingLoading] = useRecoilState(stakingLoadingAtom) @@ -62,7 +56,7 @@ const InnerStakingModal = ({ const { unstakingDuration, refreshTotals, - sumClaimsAvailable, + sumClaimsAvailable = HugeDecimal.zero, loadingWalletStakedValue, refreshClaims, } = useStakingInfo({ @@ -70,23 +64,23 @@ const InnerStakingModal = ({ fetchWalletStakedValue: true, }) - const [amount, setAmount] = useState(0) + const [amount, setAmount] = useState(HugeDecimal.zero) const doStake = DaoVotingTokenStakedHooks.useStake({ - contractAddress: votingModuleAddress, + contractAddress: votingModule.address, sender: walletAddress ?? '', }) const doUnstake = DaoVotingTokenStakedHooks.useUnstake({ - contractAddress: votingModuleAddress, + contractAddress: votingModule.address, sender: walletAddress ?? '', }) const doClaim = DaoVotingTokenStakedHooks.useClaim({ - contractAddress: votingModuleAddress, + contractAddress: votingModule.address, sender: walletAddress ?? '', }) const setRefreshDaoVotingPower = useSetRecoilState( - refreshDaoVotingPowerAtom(coreAddress) + refreshDaoVotingPowerAtom(votingModule.dao.coreAddress) ) const setRefreshFollowedDaos = useSetRecoilState(refreshFollowingDaosAtom) const refreshDaoVotingPower = () => { @@ -95,7 +89,7 @@ const InnerStakingModal = ({ } const awaitNextBlock = useAwaitNextBlock() - const onAction = async (mode: StakingMode, amount: number) => { + const onAction = async (mode: StakingMode, amount: HugeDecimal) => { if (!isWalletConnected) { toast.error(t('error.logInToContinue')) return @@ -111,13 +105,7 @@ const InnerStakingModal = ({ await doStake( CHAIN_GAS_MULTIPLIER, undefined, - coins( - convertDenomToMicroDenomStringWithDecimals( - amount, - governanceToken.decimals - ), - governanceToken.denomOrAddress - ) + amount.toCoins(governanceToken.denomOrAddress) ) // New balances will not appear until the next block. @@ -127,11 +115,14 @@ const InnerStakingModal = ({ refreshTotals() refreshDaoVotingPower() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Staked ${amount.toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.stakedTokens', { + amount: amount.toInternationalizedHumanReadableString({ + decimals: governanceToken.decimals, + }), + tokenSymbol: governanceToken.symbol, + }) ) // Close once done. @@ -150,10 +141,7 @@ const InnerStakingModal = ({ try { await doUnstake({ - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - governanceToken.decimals - ), + amount: amount.toFixed(0), }) // New balances will not appear until the next block. @@ -164,11 +152,14 @@ const InnerStakingModal = ({ refreshClaims?.() refreshDaoVotingPower() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Unstaked ${amount.toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.unstakedTokens', { + amount: amount.toInternationalizedHumanReadableString({ + decimals: governanceToken.decimals, + }), + tokenSymbol: governanceToken.symbol, + }) ) // Close once done. @@ -183,8 +174,9 @@ const InnerStakingModal = ({ break } case StakingMode.Claim: { - if (sumClaimsAvailable === 0) { - return toast.error('No claims available.') + if (sumClaimsAvailable.isZero()) { + toast.error(t('error.noClaimsAvailable')) + return } setStakingLoading(true) @@ -198,15 +190,17 @@ const InnerStakingModal = ({ refreshTotals() refreshClaims?.() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Claimed ${convertMicroDenomToDenomWithDecimals( - sumClaimsAvailable || 0, - governanceToken.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: governanceToken.decimals, - })} $${governanceToken.symbol}` + t('success.claimedTokens', { + amount: sumClaimsAvailable.toInternationalizedHumanReadableString( + { + decimals: governanceToken.decimals, + } + ), + tokenSymbol: governanceToken.symbol, + }) ) // Close once done. @@ -228,7 +222,7 @@ const InnerStakingModal = ({ return ( <StatelessStakingModal amount={amount} - claimableTokens={sumClaimsAvailable || 0} + claimableTokens={sumClaimsAvailable} error={isWalletConnected ? undefined : t('error.logInToContinue')} initialMode={initialMode} loading={stakingLoading} @@ -237,10 +231,7 @@ const InnerStakingModal = ({ ? { loading: true } : { loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingUnstakedBalance.data, - governanceToken.decimals - ), + data: HugeDecimal.from(loadingUnstakedBalance.data), } } loadingUnstakableTokens={ @@ -248,22 +239,12 @@ const InnerStakingModal = ({ ? { loading: true } : { loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingWalletStakedValue.data, - governanceToken.decimals - ), + data: HugeDecimal.from(loadingWalletStakedValue.data), } } onAction={onAction} onClose={onClose} - proposalDeposit={ - maxDeposit - ? convertMicroDenomToDenomWithDecimals( - maxDeposit, - governanceToken.decimals - ) - : undefined - } + proposalDeposit={maxDeposit ? HugeDecimal.from(maxDeposit) : undefined} setAmount={(newAmount) => setAmount(newAmount)} token={governanceToken} unstakingDuration={unstakingDuration ?? null} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useGovernanceTokenInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useGovernanceTokenInfo.ts index eccc8a861..443d9429e 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useGovernanceTokenInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useGovernanceTokenInfo.ts @@ -1,18 +1,19 @@ import { constSelector, useRecoilValue, waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { + DaoVotingNativeStakedSelectors, DaoVotingTokenStakedSelectors, genericTokenSelector, nativeDenomBalanceSelector, nativeSupplySelector, usdPriceSelector, } from '@dao-dao/state' -import { useCachedLoading } from '@dao-dao/stateless' +import { useCachedLoading, useVotingModule } from '@dao-dao/stateless' import { TokenType } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' +import { TokenStakedVotingModule } from '../../../../clients' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseGovernanceTokenInfoOptions, UseGovernanceTokenInfoResponse, @@ -23,37 +24,46 @@ export const useGovernanceTokenInfo = ({ fetchTreasuryBalance = false, fetchUsdcPrice = false, }: UseGovernanceTokenInfoOptions = {}): UseGovernanceTokenInfoResponse => { - const { chainId, coreAddress, votingModuleAddress } = - useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { address: walletAddress } = useWallet({ - chainId, + chainId: votingModule.chainId, }) + const isTokenStaked = votingModule instanceof TokenStakedVotingModule + const { denom } = useRecoilValue( - DaoVotingTokenStakedSelectors.denomSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], - }) + isTokenStaked + ? DaoVotingTokenStakedSelectors.denomSelector({ + chainId: votingModule.chainId, + contractAddress: votingModule.address, + params: [], + }) + : DaoVotingNativeStakedSelectors.getConfigSelector({ + chainId: votingModule.chainId, + contractAddress: votingModule.address, + params: [], + }) ) const [governanceToken, supply, tokenFactoryIssuerAddress] = useRecoilValue( waitForAll([ genericTokenSelector({ - chainId, + chainId: votingModule.chainId, type: TokenType.Native, denomOrAddress: denom, }), nativeSupplySelector({ - chainId, + chainId: votingModule.chainId, denom, }), - DaoVotingTokenStakedSelectors.validatedTokenfactoryIssuerContractSelector( - { - contractAddress: votingModuleAddress, - chainId, - } - ), + isTokenStaked + ? DaoVotingTokenStakedSelectors.validatedTokenfactoryIssuerContractSelector( + { + chainId: votingModule.chainId, + contractAddress: votingModule.address, + } + ) + : constSelector(undefined), ]) ) @@ -63,7 +73,7 @@ export const useGovernanceTokenInfo = ({ const loadingWalletBalance = useCachedLoading( fetchWalletBalance && walletAddress ? nativeDenomBalanceSelector({ - chainId, + chainId: votingModule.chainId, walletAddress, denom, }) @@ -75,8 +85,8 @@ export const useGovernanceTokenInfo = ({ const loadingTreasuryBalance = useCachedLoading( fetchTreasuryBalance ? nativeDenomBalanceSelector({ - chainId, - walletAddress: coreAddress, + chainId: votingModule.chainId, + walletAddress: votingModule.dao.coreAddress, denom, }) : constSelector(undefined), @@ -88,7 +98,7 @@ export const useGovernanceTokenInfo = ({ fetchUsdcPrice ? usdPriceSelector({ type: TokenType.Native, - chainId, + chainId: votingModule.chainId, denomOrAddress: denom, }) : constSelector(undefined), @@ -98,10 +108,7 @@ export const useGovernanceTokenInfo = ({ return { tokenFactoryIssuerAddress, governanceToken, - supply: convertMicroDenomToDenomWithDecimals( - supply, - governanceToken.decimals - ), + supply: HugeDecimal.from(supply), /// Optional // Wallet balance loadingWalletBalance: loadingWalletBalance.loading @@ -110,7 +117,7 @@ export const useGovernanceTokenInfo = ({ ? undefined : { loading: false, - data: Number(loadingWalletBalance.data.amount), + data: HugeDecimal.from(loadingWalletBalance.data.amount), }, // Treasury balance loadingTreasuryBalance: loadingTreasuryBalance.loading @@ -119,7 +126,7 @@ export const useGovernanceTokenInfo = ({ ? undefined : { loading: false, - data: Number(loadingTreasuryBalance.data.amount), + data: HugeDecimal.from(loadingTreasuryBalance.data.amount), }, // Price loadingPrice: loadingPrice.loading diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useMainDaoInfoCards.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useMainDaoInfoCards.tsx index 6650a514f..4e3d22b57 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useMainDaoInfoCards.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useMainDaoInfoCards.tsx @@ -1,23 +1,23 @@ import { useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { indexerQueries } from '@dao-dao/state' -import { TokenAmountDisplay } from '@dao-dao/stateless' +import { TokenAmountDisplay, useVotingModule } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' import { convertDurationToHumanReadableString, - convertMicroDenomToDenomWithDecimals, isSecretNetwork, } from '@dao-dao/utils' +import { TokenStakedVotingModule } from '../../../../clients' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { useGovernanceTokenInfo } from './useGovernanceTokenInfo' import { useStakingInfo } from './useStakingInfo' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() const { loadingTotalStakedValue, unstakingDuration } = useStakingInfo({ fetchTotalStakedValue: true, }) @@ -34,16 +34,19 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { const queryClient = useQueryClient() const loadingMembers = useQueryLoadingDataWithError( indexerQueries.queryContract(queryClient, { - chainId, - contractAddress: votingModuleAddress, - formula: 'daoVotingTokenStaked/topStakers', + chainId: votingModule.chainId, + contractAddress: votingModule.address, + formula: + votingModule instanceof TokenStakedVotingModule + ? 'daoVotingTokenStaked/topStakers' + : 'daoVotingNativeStaked/topStakers', noFallback: true, }) ) return [ // Can't view members on Secret Network. - ...(isSecretNetwork(chainId) + ...(isSecretNetwork(votingModule.chainId) ? [] : [ { @@ -80,13 +83,7 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { amount={ loadingTotalStakedValue.loading ? { loading: true } - : { - loading: false, - data: convertMicroDenomToDenomWithDecimals( - loadingTotalStakedValue.data, - decimals - ), - } + : HugeDecimal.from(loadingTotalStakedValue.data) } decimals={decimals} symbol={symbol} diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useStakingInfo.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useStakingInfo.ts index ba62dd72f..ca174be0c 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useStakingInfo.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/hooks/useStakingInfo.ts @@ -1,17 +1,25 @@ +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useCallback } from 'react' -import { constSelector, useRecoilValue, useSetRecoilState } from 'recoil' +import { constSelector, useSetRecoilState } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { + DaoVotingNativeStakedSelectors, DaoVotingTokenStakedSelectors, blockHeightSelector, + daoVotingTokenStakedQueries, refreshClaimsIdAtom, refreshWalletBalancesIdAtom, } from '@dao-dao/state' -import { useCachedLoadable, useCachedLoading } from '@dao-dao/stateless' +import { + useCachedLoadable, + useCachedLoading, + useVotingModule, +} from '@dao-dao/stateless' import { claimAvailable } from '@dao-dao/utils' +import { TokenStakedVotingModule } from '../../../../clients' import { useWallet } from '../../../../hooks/useWallet' -import { useVotingModuleAdapterOptions } from '../../../react/context' import { UseStakingInfoOptions, UseStakingInfoResponse } from '../types' export const useStakingInfo = ({ @@ -19,16 +27,14 @@ export const useStakingInfo = ({ fetchTotalStakedValue = false, fetchWalletStakedValue = false, }: UseStakingInfoOptions = {}): UseStakingInfoResponse => { - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() - const { address: walletAddress } = useWallet({ - chainId, - }) + const votingModule = useVotingModule() + const { address: walletAddress } = useWallet() + const queryClient = useQueryClient() - const config = useRecoilValue( - DaoVotingTokenStakedSelectors.getConfigSelector({ - chainId, - contractAddress: votingModuleAddress, - params: [], + const { data: config } = useSuspenseQuery( + daoVotingTokenStakedQueries.getConfig(queryClient, { + chainId: votingModule.chainId, + contractAddress: votingModule.address, }) ) @@ -47,7 +53,7 @@ export const useStakingInfo = ({ const blockHeightLoadable = useCachedLoadable( fetchClaims ? blockHeightSelector({ - chainId, + chainId: votingModule.chainId, }) : undefined ) @@ -62,8 +68,8 @@ export const useStakingInfo = ({ const loadingClaims = useCachedLoading( fetchClaims && walletAddress ? DaoVotingTokenStakedSelectors.claimsSelector({ - chainId, - contractAddress: votingModuleAddress, + chainId: votingModule.chainId, + contractAddress: votingModule.address, params: [{ address: walletAddress }], }) : constSelector(undefined), @@ -82,16 +88,19 @@ export const useStakingInfo = ({ ? claims?.filter((c) => claimAvailable(c, blockHeight)) : undefined const sumClaimsAvailable = claimsAvailable?.reduce( - (p, c) => p + Number(c.amount), - 0 + (sum, c) => sum.plus(c.amount), + HugeDecimal.zero ) // Total staked value const loadingTotalStakedValue = useCachedLoading( fetchTotalStakedValue - ? DaoVotingTokenStakedSelectors.totalPowerAtHeightSelector({ - chainId, - contractAddress: votingModuleAddress, + ? (votingModule instanceof TokenStakedVotingModule + ? DaoVotingTokenStakedSelectors + : DaoVotingNativeStakedSelectors + ).totalPowerAtHeightSelector({ + chainId: votingModule.chainId, + contractAddress: votingModule.address, params: [{}], }) : constSelector(undefined), @@ -101,9 +110,12 @@ export const useStakingInfo = ({ // Wallet staked value const loadingWalletStakedValue = useCachedLoading( fetchWalletStakedValue && walletAddress - ? DaoVotingTokenStakedSelectors.votingPowerAtHeightSelector({ - chainId, - contractAddress: votingModuleAddress, + ? (votingModule instanceof TokenStakedVotingModule + ? DaoVotingTokenStakedSelectors + : DaoVotingNativeStakedSelectors + ).votingPowerAtHeightSelector({ + chainId: votingModule.chainId, + contractAddress: votingModule.address, params: [{ address: walletAddress }], }) : constSelector(undefined), @@ -111,7 +123,7 @@ export const useStakingInfo = ({ ) return { - stakingContractAddress: votingModuleAddress, + stakingContractAddress: votingModule.address, unstakingDuration: config.unstaking_duration ?? undefined, refreshTotals, /// Optional @@ -129,7 +141,7 @@ export const useStakingInfo = ({ ? undefined : { loading: false, - data: Number(loadingTotalStakedValue.data.power), + data: HugeDecimal.from(loadingTotalStakedValue.data.power), }, // Wallet staked value loadingWalletStakedValue: loadingWalletStakedValue.loading @@ -138,7 +150,7 @@ export const useStakingInfo = ({ ? undefined : { loading: false, - data: Number(loadingWalletStakedValue.data.power), + data: HugeDecimal.from(loadingWalletStakedValue.data.power), }, } } diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/index.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/index.ts index 45931a716..5e6fac34c 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/index.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/index.ts @@ -9,14 +9,17 @@ import { VotingModuleAdapter, } from '@dao-dao/types' import { + DAO_VOTING_NATIVE_STAKED_CONTRACT_NAMES, DAO_VOTING_TOKEN_STAKED_CONTRACT_NAMES, DaoVotingTokenStakedAdapterId, isSecretNetwork, } from '@dao-dao/utils' +import { TokenStakedVotingModule } from '../../../clients' import { BitSongFantokenMintAction, - MintAction, + DaoVotingNativeStakedMintAction, + DaoVotingTokenStakedMintAction, UpdateStakingConfigAction, } from './actions' import { MembersTab, ProfileCardMemberInfo, StakingModal } from './components' @@ -24,9 +27,12 @@ import { useMainDaoInfoCards } from './hooks' export const DaoVotingTokenStakedAdapter: VotingModuleAdapter = { id: DaoVotingTokenStakedAdapterId, - contractNames: DAO_VOTING_TOKEN_STAKED_CONTRACT_NAMES, + contractNames: [ + ...DAO_VOTING_TOKEN_STAKED_CONTRACT_NAMES, + ...DAO_VOTING_NATIVE_STAKED_CONTRACT_NAMES, + ], - load: ({ chainId }) => ({ + load: (votingModule) => ({ // Hooks hooks: { useMainDaoInfoCards, @@ -40,7 +46,7 @@ export const DaoVotingTokenStakedAdapter: VotingModuleAdapter = { StakingModal, // Can't view members on Secret Network. - extraTabs: isSecretNetwork(chainId) + extraTabs: isSecretNetwork(votingModule.chainId) ? undefined : [ { @@ -57,10 +63,14 @@ export const DaoVotingTokenStakedAdapter: VotingModuleAdapter = { fields: { actions: { actions: [ - ...(chainId === ChainId.BitsongMainnet || - chainId === ChainId.BitsongTestnet + ...(votingModule.chainId === ChainId.BitsongMainnet || + votingModule.chainId === ChainId.BitsongTestnet ? [BitSongFantokenMintAction] - : [MintAction]), + : [ + votingModule instanceof TokenStakedVotingModule + ? DaoVotingTokenStakedMintAction + : DaoVotingNativeStakedMintAction, + ]), UpdateStakingConfigAction, ], categoryMakers: [ diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/types.ts b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/types.ts index 54c794113..3d1e7548e 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/types.ts +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingTokenStaked/types.ts @@ -1,3 +1,4 @@ +import { HugeDecimal } from '@dao-dao/math' import { Duration, DurationWithUnits, @@ -30,11 +31,11 @@ export interface UseStakingInfoResponse { claims?: Claim[] claimsPending?: Claim[] claimsAvailable?: Claim[] - sumClaimsAvailable?: number + sumClaimsAvailable?: HugeDecimal // Total staked value - loadingTotalStakedValue?: LoadingData<number> + loadingTotalStakedValue?: LoadingData<HugeDecimal> // Wallet staked value - loadingWalletStakedValue?: LoadingData<number> + loadingWalletStakedValue?: LoadingData<HugeDecimal> } export type UseGovernanceTokenInfoOptions = { @@ -55,7 +56,8 @@ export type UseGovernanceTokenInfoOptions = { export type UseGovernanceTokenInfoResponse = { /** * The token factory issuer contract address, if the governance token is a - * token factory denom and a token factory issuer contract exists. + * token factory denom and a token factory issuer contract exists. This will + * always be undefined for the deprecated dao-voting-native-staked module. */ tokenFactoryIssuerAddress: string | undefined /** @@ -65,7 +67,7 @@ export type UseGovernanceTokenInfoResponse = { /** * The supply of the governance token converted to the appropriate decimals. */ - supply: number + supply: HugeDecimal // Optional, defined if options are set to true. @@ -73,12 +75,12 @@ export type UseGovernanceTokenInfoResponse = { * Unstaked governance token balance. Only defined if a wallet is connected * and the option to fetch this is true. */ - loadingWalletBalance?: LoadingData<number> + loadingWalletBalance?: LoadingData<HugeDecimal> /** * The treasury balance of the governance token. Only defined if the option to * fetch this is true. */ - loadingTreasuryBalance?: LoadingData<number> + loadingTreasuryBalance?: LoadingData<HugeDecimal> /** * The price of the governance token. Only defined if the option to fetch this * is true. diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/MainDaoInfoCardsLoader.tsx b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/MainDaoInfoCardsLoader.tsx index fdd265aa0..4c713561c 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/MainDaoInfoCardsLoader.tsx +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/MainDaoInfoCardsLoader.tsx @@ -1,11 +1,11 @@ import { useTranslation } from 'react-i18next' -import { DaoInfoCards, useDaoInfoContext } from '@dao-dao/stateless' +import { DaoInfoCards, useDao } from '@dao-dao/stateless' import { formatDate } from '@dao-dao/utils' export const MainDaoInfoCardsLoader = () => { const { t } = useTranslation() - const { activeThreshold, created } = useDaoInfoContext() + const { activeThreshold, created } = useDao().info return ( <DaoInfoCards diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/ProfileCardMemberInfo.tsx index f32695449..69c75a9c7 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/ProfileCardMemberInfo.tsx @@ -2,38 +2,33 @@ import { useQueries } from '@tanstack/react-query' import { useState } from 'react' import { useRecoilValue, waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenBalanceSelector, neutronVaultQueries, neutronVotingRegistryQueries, stakingLoadingAtom, } from '@dao-dao/state' -import { - useCachedLoadingWithError, - useDaoInfoContext, -} from '@dao-dao/stateless' +import { useCachedLoadingWithError, useDao } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - makeCombineQueryResultsIntoLoadingDataWithError, -} from '@dao-dao/utils' +import { makeCombineQueryResultsIntoLoadingDataWithError } from '@dao-dao/utils' import { useQueryLoadingDataWithError, useWallet } from '../../../../hooks' import { ProfileCardMemberInfoTokens } from '../../../components' -import { useVotingModule } from '../hooks' +import { useVotingModuleInfo } from '../hooks' import { StakingModal } from './StakingModal' export const ProfileCardMemberInfo = ({ maxGovernanceTokenDeposit, ...props }: BaseProfileCardMemberInfoProps) => { - const { name: daoName, chainId } = useDaoInfoContext() + const { name: daoName, chainId } = useDao() const { address } = useWallet() const [showStakingModal, setShowStakingModal] = useState(false) const stakingLoading = useRecoilValue(stakingLoadingAtom) - const { votingRegistryAddress, loadingVaults } = useVotingModule() + const { votingRegistryAddress, loadingVaults } = useVotingModuleInfo() const realVaults = loadingVaults.loading || loadingVaults.errored ? [] @@ -117,13 +112,11 @@ export const ProfileCardMemberInfo = ({ loading: false, data: realVaults.map(({ bondToken }, index) => ({ token: bondToken, - staked: convertMicroDenomToDenomWithDecimals( - loadingStakedTokens.data[index].unbondable_abount, - bondToken.decimals + staked: HugeDecimal.from( + loadingStakedTokens.data[index].unbondable_abount ), - unstaked: convertMicroDenomToDenomWithDecimals( - loadingUnstakedTokens.data[index].balance, - bondToken.decimals + unstaked: HugeDecimal.from( + loadingUnstakedTokens.data[index].balance ), })), } diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/StakingModal.tsx index 15003379b..5f3d0986d 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/StakingModal.tsx +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/StakingModal.tsx @@ -1,10 +1,10 @@ -import { coins } from '@cosmjs/stargate' import { useQueries } from '@tanstack/react-query' import { useCallback, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { useRecoilState, useSetRecoilState, waitForAll } from 'recoil' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenBalanceSelector, neutronVaultQueries, @@ -15,15 +15,17 @@ import { } from '@dao-dao/state' import { ModalLoader, - StakingMode, StakingModal as StatelessStakingModal, useCachedLoadingWithError, + useVotingModule, } from '@dao-dao/stateless' -import { BaseStakingModalProps, TokenInputOption } from '@dao-dao/types' +import { + BaseStakingModalProps, + StakingMode, + TokenInputOption, +} from '@dao-dao/types' import { CHAIN_GAS_MULTIPLIER, - convertDenomToMicroDenomStringWithDecimals, - convertMicroDenomToDenomWithDecimals, makeCombineQueryResultsIntoLoadingDataWithError, processError, tokensEqual, @@ -35,8 +37,7 @@ import { useAwaitNextBlock, useWallet, } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' -import { useVotingModule } from '../hooks' +import { useVotingModuleInfo } from '../hooks' export const StakingModal = (props: BaseStakingModalProps) => ( <SuspenseLoader @@ -53,9 +54,9 @@ const InnerStakingModal = ({ }: BaseStakingModalProps) => { const { t } = useTranslation() const { address = '', isWalletConnected, refreshBalances } = useWallet() - const { coreAddress, chainId } = useVotingModuleAdapterOptions() + const votingModule = useVotingModule() - const { loadingVaults } = useVotingModule() + const { loadingVaults } = useVotingModuleInfo() const realVaults = loadingVaults.loading || loadingVaults.errored ? [] @@ -74,7 +75,7 @@ const InnerStakingModal = ({ ? [] : realVaults.map(({ address: contractAddress }) => neutronVaultQueries.bondingStatus({ - chainId, + chainId: votingModule.chainId, contractAddress, args: { address, @@ -112,7 +113,7 @@ const InnerStakingModal = ({ const [stakingLoading, setStakingLoading] = useRecoilState(stakingLoadingAtom) const [selectedVaultIndex, setSelectedVaultIndex] = useState(0) - const [amount, setAmount] = useState(0) + const [amount, setAmount] = useState(HugeDecimal.zero) const selectedVault = loadingVaults.loading || loadingVaults.errored @@ -137,7 +138,7 @@ const InnerStakingModal = ({ }) const setRefreshDaoVotingPower = useSetRecoilState( - refreshDaoVotingPowerAtom(coreAddress) + refreshDaoVotingPowerAtom(votingModule.dao.coreAddress) ) const setRefreshFollowedDaos = useSetRecoilState(refreshFollowingDaosAtom) const refreshDaoVotingPower = () => { @@ -146,7 +147,7 @@ const InnerStakingModal = ({ } const awaitNextBlock = useAwaitNextBlock() - const onAction = async (mode: StakingMode, amount: number) => { + const onAction = async (mode: StakingMode, amount: HugeDecimal) => { if (!selectedVault) { toast.error(t('error.loadingData')) return @@ -166,13 +167,7 @@ const InnerStakingModal = ({ await doStake( CHAIN_GAS_MULTIPLIER, undefined, - coins( - convertDenomToMicroDenomStringWithDecimals( - amount, - selectedVault.bondToken.decimals - ), - selectedVault.bondToken.denomOrAddress - ) + amount.toCoins(selectedVault.bondToken.denomOrAddress) ) // New balances will not appear until the next block. @@ -182,11 +177,14 @@ const InnerStakingModal = ({ refreshTotals() refreshDaoVotingPower() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Staked ${amount.toLocaleString(undefined, { - maximumFractionDigits: selectedVault.bondToken.decimals, - })} $${selectedVault.bondToken.symbol}` + t('success.stakedTokens', { + amount: amount.toInternationalizedHumanReadableString({ + decimals: selectedVault.bondToken.decimals, + }), + tokenSymbol: selectedVault.bondToken.symbol, + }) ) // Close once done. @@ -205,10 +203,7 @@ const InnerStakingModal = ({ try { await doUnstake({ - amount: convertDenomToMicroDenomStringWithDecimals( - amount, - selectedVault.bondToken.decimals - ), + amount: amount.toFixed(0), }) // New balances will not appear until the next block. @@ -218,11 +213,14 @@ const InnerStakingModal = ({ refreshTotals() refreshDaoVotingPower() - setAmount(0) + setAmount(HugeDecimal.zero) toast.success( - `Unstaked ${amount.toLocaleString(undefined, { - maximumFractionDigits: selectedVault.bondToken.decimals, - })} $${selectedVault.bondToken.symbol}` + t('success.unstakedTokens', { + amount: amount.toInternationalizedHumanReadableString({ + decimals: selectedVault.bondToken.decimals, + }), + tokenSymbol: selectedVault.bondToken.symbol, + }) ) // Close once done. @@ -251,7 +249,7 @@ const InnerStakingModal = ({ return ( <StatelessStakingModal amount={amount} - claimableTokens={0} + claimableTokens={HugeDecimal.zero} error={isWalletConnected ? undefined : t('error.logInToContinue')} initialMode={initialMode} loading={stakingLoading} @@ -262,10 +260,7 @@ const InnerStakingModal = ({ ? { loading: true } : { loading: false, - data: convertMicroDenomToDenomWithDecimals( - selectedVaultUnstakedTokens, - selectedVault.bondToken.decimals - ), + data: HugeDecimal.from(selectedVaultUnstakedTokens), } } loadingUnstakableTokens={ @@ -275,10 +270,7 @@ const InnerStakingModal = ({ ? { loading: true } : { loading: false, - data: convertMicroDenomToDenomWithDecimals( - selectedVaultStakedTokens, - selectedVault.bondToken.decimals - ), + data: HugeDecimal.from(selectedVaultStakedTokens), } } onAction={onAction} diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/VaultsTab.tsx b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/VaultsTab.tsx index eb8fc945d..1e067fe49 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/VaultsTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/components/VaultsTab.tsx @@ -3,11 +3,11 @@ import { NeutronVotingVaultsTab, useChain } from '@dao-dao/stateless' import { DaoVotingVaultCard } from '../../../../components' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModule } from '../hooks' +import { useVotingModuleInfo } from '../hooks' export const VaultsTab = () => { const { chain_id: chainId } = useChain() - const { votingRegistryAddress, loadingVaults } = useVotingModule() + const { votingRegistryAddress, loadingVaults } = useVotingModuleInfo() const loadingTotalVotingPower = useQueryLoadingDataWithError( neutronVotingRegistryQueries.totalPowerAtHeight({ chainId, diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/index.ts b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/index.ts index 3055dca9b..bcabdf232 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/index.ts +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/index.ts @@ -1,2 +1,2 @@ export * from './useMainDaoInfoCards' -export * from './useVotingModule' +export * from './useVotingModuleInfo' diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useMainDaoInfoCards.tsx b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useMainDaoInfoCards.tsx index 40fa9b71c..96bae5a04 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useMainDaoInfoCards.tsx +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useMainDaoInfoCards.tsx @@ -2,14 +2,13 @@ import { useTranslation } from 'react-i18next' import { TokenAmountDisplay } from '@dao-dao/stateless' import { DaoInfoCard } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' -import { useVotingModule } from './useVotingModule' +import { useVotingModuleInfo } from './useVotingModuleInfo' export const useMainDaoInfoCards = (): DaoInfoCard[] => { const { t } = useTranslation() - const { loadingVaults } = useVotingModule() + const { loadingVaults } = useVotingModuleInfo() return loadingVaults.loading || loadingVaults.errored ? [] @@ -24,10 +23,7 @@ export const useMainDaoInfoCards = (): DaoInfoCard[] => { }), value: ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - totalPower, - info.bondToken.decimals - )} + amount={totalPower} decimals={info.bondToken.decimals} symbol={info.bondToken.symbol} /> diff --git a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useVotingModule.ts b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useVotingModuleInfo.ts similarity index 64% rename from packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useVotingModule.ts rename to packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useVotingModuleInfo.ts index 7811d37a4..fee8423f1 100644 --- a/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useVotingModule.ts +++ b/packages/stateful/voting-module-adapter/adapters/NeutronVotingRegistry/hooks/useVotingModuleInfo.ts @@ -1,31 +1,31 @@ import { useQueryClient } from '@tanstack/react-query' import { neutronVotingRegistryExtraQueries } from '@dao-dao/state' +import { useVotingModule } from '@dao-dao/stateless' import { LoadingDataWithError, VotingVaultWithInfo } from '@dao-dao/types' import { useQueryLoadingDataWithError } from '../../../../hooks' -import { useVotingModuleAdapterOptions } from '../../../react/context' export type LoadingVaults = LoadingDataWithError<VotingVaultWithInfo[]> -export type UseVotingModuleReturn = { +export type UseVotingModuleInfoReturn = { votingRegistryAddress: string loadingVaults: LoadingVaults } -export const useVotingModule = (): UseVotingModuleReturn => { - const { chainId, votingModuleAddress } = useVotingModuleAdapterOptions() +export const useVotingModuleInfo = (): UseVotingModuleInfoReturn => { + const votingModule = useVotingModule() const queryClient = useQueryClient() const loadingVaults = useQueryLoadingDataWithError( neutronVotingRegistryExtraQueries.vaultsWithInfo(queryClient, { - chainId, - address: votingModuleAddress, + chainId: votingModule.chainId, + address: votingModule.address, }) ) return { - votingRegistryAddress: votingModuleAddress, + votingRegistryAddress: votingModule.address, loadingVaults, } } diff --git a/packages/stateful/voting-module-adapter/adapters/index.ts b/packages/stateful/voting-module-adapter/adapters/index.ts index 0a98fcc4b..90f603ed4 100644 --- a/packages/stateful/voting-module-adapter/adapters/index.ts +++ b/packages/stateful/voting-module-adapter/adapters/index.ts @@ -1,7 +1,6 @@ export * from './DaoVotingCw4' export * from './DaoVotingCw20Staked' export * from './DaoVotingCw721Staked' -export * from './DaoVotingNativeStaked' export * from './DaoVotingOnftStaked' export * from './DaoVotingSgCommunityNft' export * from './DaoVotingTokenStaked' diff --git a/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.stories.tsx b/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.stories.tsx index d933e55b8..9c70392bc 100644 --- a/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.stories.tsx +++ b/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.stories.tsx @@ -1,5 +1,6 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' +import { HugeDecimal } from '@dao-dao/math' import { makeProps as makeUnstakingLineProps } from '@dao-dao/stateless/components/token/UnstakingLine.stories' import { CHAIN_ID } from '@dao-dao/storybook' import { TokenType, UnstakingTask, UnstakingTaskStatus } from '@dao-dao/types' @@ -48,8 +49,8 @@ export const makeProps = ( decimals: 6, imageUrl: undefined, }, - staked: stakedTokens ?? 50, - unstaked: 45.413, + staked: HugeDecimal.fromHumanReadable(stakedTokens ?? 50, 6), + unstaked: HugeDecimal.fromHumanReadable(45.413, 6), }, ], }, @@ -85,8 +86,8 @@ export const makeCantVoteOnProposalProps = ( decimals: 6, imageUrl: undefined, }, - staked: 0, - unstaked: 45.413, + staked: HugeDecimal.zero, + unstaked: HugeDecimal.fromHumanReadable(45.413, 6), }, ], }, diff --git a/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx b/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx index 6467ae193..9bd27672c 100644 --- a/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx +++ b/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, TokenAmountDisplay, UnstakingModal } from '@dao-dao/stateless' import { BaseProfileCardMemberInfoProps, @@ -30,8 +31,8 @@ export interface ProfileCardMemberInfoTokensProps loadingTokens: LoadingData< { token: GenericToken - staked: number - unstaked: number + staked: HugeDecimal + unstaked: HugeDecimal }[] > hideUnstaking?: boolean @@ -57,22 +58,28 @@ export const ProfileCardMemberInfoTokens = ({ () => unstakingTasks.reduce( (acc, task) => - acc + - // Only include balance of ready to claim tasks. - (task.status === UnstakingTaskStatus.ReadyToClaim ? task.amount : 0), - 0 - ) ?? 0, + acc.plus( + // Only include balance of ready to claim tasks. + task.status === UnstakingTaskStatus.ReadyToClaim + ? task.amount + : HugeDecimal.zero + ), + HugeDecimal.zero + ), [unstakingTasks] ) const totalUnstakingBalance = useMemo( () => unstakingTasks.reduce( (acc, task) => - acc + - // Only include balance of unstaking tasks. - (task.status === UnstakingTaskStatus.Unstaking ? task.amount : 0), - 0 - ) ?? 0, + acc.plus( + // Only include balance of unstaking tasks. + task.status === UnstakingTaskStatus.Unstaking + ? task.amount + : HugeDecimal.zero + ), + HugeDecimal.zero + ), [unstakingTasks] ) const unstakingBalanceByToken = useMemo( @@ -80,11 +87,15 @@ export const ProfileCardMemberInfoTokens = ({ unstakingTasks.reduce( (acc, task) => ({ ...acc, - [task.token.denomOrAddress]: - (acc[task.token.denomOrAddress] || 0) + - (task.status === UnstakingTaskStatus.Unstaking ? task.amount : 0), + [task.token.denomOrAddress]: ( + acc[task.token.denomOrAddress] || HugeDecimal.zero + ).plus( + task.status === UnstakingTaskStatus.Unstaking + ? task.amount + : HugeDecimal.zero + ), }), - {} as Partial<Record<string, number>> + {} as Partial<Record<string, HugeDecimal>> ), [unstakingTasks] ) @@ -93,10 +104,10 @@ export const ProfileCardMemberInfoTokens = ({ const hasStaked = !loadingTokens.loading && - loadingTokens.data.some(({ staked }) => staked > 0) + loadingTokens.data.some(({ staked }) => staked.isPositive()) const hasUnstaked = !loadingTokens.loading && - loadingTokens.data.some(({ unstaked }) => unstaked > 0) + loadingTokens.data.some(({ unstaked }) => unstaked.isPositive()) const isMember = !loadingVotingPower.loading && loadingVotingPower.data > 0 const canBeMemberButIsnt = !isMember && hasUnstaked @@ -195,6 +206,7 @@ export const ProfileCardMemberInfoTokens = ({ : 'text-text-tertiary' )} decimals={token.decimals} + suffix={` ${t('info.available')}`} symbol={token.symbol} /> )) @@ -223,31 +235,36 @@ export const ProfileCardMemberInfoTokens = ({ {/* Show unstaking balance if any are unstaking or claimable or if they are a member. */} {!hideUnstaking && - (isMember || totalUnstakingBalance > 0 || claimableBalance > 0) && ( + (isMember || + totalUnstakingBalance.isPositive() || + claimableBalance.isPositive()) && ( <div className="flex flex-row items-center justify-between"> <p>{t('title.unstakingTokens')}</p> <Button className={clsx( 'text-right font-mono underline-offset-2', - totalUnstakingBalance === 0 && 'text-text-tertiary' + totalUnstakingBalance.isZero() && 'text-text-tertiary' )} contentContainerClassName="justify-end flex flex-col items-end" onClick={() => setShowUnstakingTokens(true)} - variant={totalUnstakingBalance > 0 ? 'underline' : 'none'} + variant={ + totalUnstakingBalance.isPositive() ? 'underline' : 'none' + } > {!loadingTokens.loading && (onlyOneToken ? loadingTokens.data : loadingTokens.data.filter( ({ token }) => - !!unstakingBalanceByToken[token.denomOrAddress] + unstakingBalanceByToken[token.denomOrAddress] ) ).map(({ token }) => ( <TokenAmountDisplay key={token.denomOrAddress} amount={ - unstakingBalanceByToken[token.denomOrAddress] || 0 + unstakingBalanceByToken[token.denomOrAddress] || + HugeDecimal.zero } decimals={token.decimals} symbol={token.symbol} @@ -259,7 +276,7 @@ export const ProfileCardMemberInfoTokens = ({ </div> <div className="mt-6 flex flex-col gap-2"> - {claimableBalance > 0 && ( + {claimableBalance.isPositive() && ( <Button contentContainerClassName="justify-center" disabled={stakingLoading} @@ -276,9 +293,10 @@ export const ProfileCardMemberInfoTokens = ({ !onlyOneToken ? t('button.claimYourTokens') : t('button.claimNumTokens', { - amount: claimableBalance.toLocaleString(undefined, { - maximumFractionDigits: loadingTokens.data[0].token.decimals, - }), + amount: + claimableBalance.toInternationalizedHumanReadableString({ + decimals: loadingTokens.data[0].token.decimals, + }), tokenSymbol: onlyTokenSymbol, })} </Button> diff --git a/packages/stateful/voting-module-adapter/core.ts b/packages/stateful/voting-module-adapter/core.ts index 902911d39..2330d9234 100644 --- a/packages/stateful/voting-module-adapter/core.ts +++ b/packages/stateful/voting-module-adapter/core.ts @@ -1,7 +1,6 @@ import { IDaoBase, IVotingModuleAdapterContext, - IVotingModuleAdapterOptions, VotingModuleAdapter, } from '@dao-dao/types' @@ -9,7 +8,6 @@ import { DaoVotingCw20StakedAdapter, DaoVotingCw4Adapter, DaoVotingCw721StakedAdapter, - DaoVotingNativeStakedAdapter, DaoVotingOnftStakedAdapter, DaoVotingSgCommunityNftAdapter, DaoVotingTokenStakedAdapter, @@ -30,7 +28,6 @@ export const getAdapters = (): readonly VotingModuleAdapter[] => [ DaoVotingCw4Adapter, DaoVotingCw20StakedAdapter, DaoVotingCw721StakedAdapter, - DaoVotingNativeStakedAdapter, DaoVotingOnftStakedAdapter, DaoVotingSgCommunityNftAdapter, DaoVotingTokenStakedAdapter, @@ -42,9 +39,7 @@ export const getAdapterById = (id: string) => export const matchAdapter = (contractNameToMatch: string) => getAdapters().find((adapter) => - adapter.contractNames.some( - (contractName) => contractNameToMatch === contractName - ) + adapter.contractNames.includes(contractNameToMatch) ) || FallbackAdapter export const matchAndLoadAdapter = ( @@ -62,16 +57,9 @@ export const matchAndLoadAdapter = ( ) } - const options: IVotingModuleAdapterOptions = { - chainId: dao.chainId, - votingModuleAddress: dao.info.votingModuleAddress, - coreAddress: dao.coreAddress, - } - return { id: adapter.id, - adapter: adapter.load(options), - options, + adapter: adapter.load(dao.votingModule), votingModule: dao.votingModule, } } diff --git a/packages/stateful/voting-module-adapter/react/context.ts b/packages/stateful/voting-module-adapter/react/context.ts index 0f3b58686..cddbdebb8 100644 --- a/packages/stateful/voting-module-adapter/react/context.ts +++ b/packages/stateful/voting-module-adapter/react/context.ts @@ -3,7 +3,6 @@ import { createContext, useContext } from 'react' import { IVotingModuleAdapter, IVotingModuleAdapterContext, - IVotingModuleAdapterOptions, } from '@dao-dao/types' // External API @@ -30,17 +29,3 @@ export const useVotingModuleAdapterContextIfAvailable = (): export const useVotingModuleAdapter = (): IVotingModuleAdapter => useVotingModuleAdapterContext().adapter - -// For internal use to pass around options. -export const useVotingModuleAdapterOptions = - (): IVotingModuleAdapterOptions => { - const context = useContext(VotingModuleAdapterContext) - - if (!context) { - throw new Error( - 'useVotingModuleAdapterOptions can only be used in a descendant of VotingModuleAdapterProvider.' - ) - } - - return context.options - } diff --git a/packages/stateful/voting-module-adapter/react/provider.tsx b/packages/stateful/voting-module-adapter/react/provider.tsx index 800da2bd0..3e7c11cad 100644 --- a/packages/stateful/voting-module-adapter/react/provider.tsx +++ b/packages/stateful/voting-module-adapter/react/provider.tsx @@ -1,6 +1,6 @@ import { ReactNode, useState } from 'react' -import { useDaoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { IVotingModuleAdapterContext } from '@dao-dao/types' import { matchAndLoadAdapter } from '../core' @@ -14,7 +14,7 @@ export const VotingModuleAdapterProvider = ({ }: { children: ReactNode }) => { - const { dao } = useDaoContext() + const dao = useDao() const [context] = useState<IVotingModuleAdapterContext>(() => matchAndLoadAdapter(dao) ) diff --git a/packages/stateful/widgets/react/useWidgets.tsx b/packages/stateful/widgets/react/useWidgets.tsx index e19891de2..b1200acb5 100644 --- a/packages/stateful/widgets/react/useWidgets.tsx +++ b/packages/stateful/widgets/react/useWidgets.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useChain, useDaoInfoContext } from '@dao-dao/stateless' +import { useChain, useDao } from '@dao-dao/stateless' import { LoadedWidget, LoadingData, @@ -28,7 +28,7 @@ export const useWidgets = ({ }: UseWidgetsOptions = {}): UseWidgetsResult => { const { t } = useTranslation() const { chain_id: chainId } = useChain() - const { items } = useDaoInfoContext() + const { items } = useDao().info const { isMember = false } = useMembership() const loadingWidgets = useMemo((): LoadingData<LoadedWidget[]> => { diff --git a/packages/stateful/widgets/widgets/Press/PressEditor.tsx b/packages/stateful/widgets/widgets/Press/PressEditor.tsx index b988493fa..01943ca54 100644 --- a/packages/stateful/widgets/widgets/Press/PressEditor.tsx +++ b/packages/stateful/widgets/widgets/Press/PressEditor.tsx @@ -9,7 +9,7 @@ import { CopyableAddress, DaoSupportedChainPickerInput, useChain, - useDaoInfoContext, + useDao, useSupportedChainContext, } from '@dao-dao/stateless' import { ActionKey, ChainId, WidgetEditorProps } from '@dao-dao/types' @@ -36,7 +36,10 @@ export const PressEditor = ({ config: { polytone }, } = useSupportedChainContext() const { chain_id: nativeChainId } = useChain() - const { name: daoName, polytoneProxies } = useDaoInfoContext() + const { + name: daoName, + info: { polytoneProxies }, + } = useDao() const { setValue, setError, clearErrors, watch } = useFormContext<PressData>() const chainId = watch((fieldNamePrefix + 'chainId') as 'chainId') diff --git a/packages/stateful/widgets/widgets/Press/Renderer/Renderer.tsx b/packages/stateful/widgets/widgets/Press/Renderer/Renderer.tsx index 1b558cca7..a7ed059b2 100644 --- a/packages/stateful/widgets/widgets/Press/Renderer/Renderer.tsx +++ b/packages/stateful/widgets/widgets/Press/Renderer/Renderer.tsx @@ -7,12 +7,7 @@ import { import { ComponentType, useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { - Button, - Tooltip, - useDaoInfoContext, - useDaoNavHelpers, -} from '@dao-dao/stateless' +import { Button, Tooltip, useDao, useDaoNavHelpers } from '@dao-dao/stateless' import { ButtonLinkProps, IconButtonLinkProps, @@ -46,7 +41,7 @@ export const Renderer = ({ IconButtonLink, }: RendererProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { daoSubpathComponents, goToDao } = useDaoNavHelpers() const openPostId = diff --git a/packages/stateful/widgets/widgets/Press/Renderer/index.tsx b/packages/stateful/widgets/widgets/Press/Renderer/index.tsx index 1dbf4c638..e78a44d88 100644 --- a/packages/stateful/widgets/widgets/Press/Renderer/index.tsx +++ b/packages/stateful/widgets/widgets/Press/Renderer/index.tsx @@ -1,6 +1,6 @@ import { useCachedLoading, - useDaoInfoContext, + useDao, useDaoNavHelpers, useInitializedActionForKey, } from '@dao-dao/stateless' @@ -16,7 +16,7 @@ import { Renderer as StatelessRenderer } from './Renderer' export const Renderer = ({ variables: { chainId: configuredChainId, contract }, }: WidgetRendererProps<PressData>) => { - const { chainId: daoChainId, coreAddress } = useDaoInfoContext() + const { chainId: daoChainId, coreAddress } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { isMember = false } = useMembership() diff --git a/packages/stateful/widgets/widgets/Press/actions/DeletePost/Component.tsx b/packages/stateful/widgets/widgets/Press/actions/DeletePost/Component.tsx index c44030624..afbc424a4 100644 --- a/packages/stateful/widgets/widgets/Press/actions/DeletePost/Component.tsx +++ b/packages/stateful/widgets/widgets/Press/actions/DeletePost/Component.tsx @@ -8,7 +8,7 @@ import { InputLabel, Loader, SelectInput, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { ActionComponent, LoadingData } from '@dao-dao/types' @@ -35,7 +35,7 @@ export const DeletePostComponent: ActionComponent<DeletePostOptions> = ({ const { register, watch } = useFormContext<DeletePostData>() const id = watch((fieldNamePrefix + 'id') as 'id') - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoPath } = useDaoNavHelpers() return isCreating ? ( diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/SurveyRow.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/SurveyRow.tsx index 64b923329..cb8a278b1 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/SurveyRow.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/SurveyRow.tsx @@ -1,4 +1,4 @@ -import { useDaoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { LinkWrapper } from '../../../../../../components' import { @@ -16,7 +16,7 @@ export const SurveyRow = ({ const { isWalletConnected, hexPublicKey } = useWallet({ loadAccount: true, }) - const { dao } = useDaoContext() + const dao = useDao() // Load survey from query in case list is out of date. const loadingSurvey = useQueryLoadingDataWithError( diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/CreateSurvey.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/CreateSurvey.tsx index fc6786aa7..b1637c663 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/CreateSurvey.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/CreateSurvey.tsx @@ -4,16 +4,16 @@ import { useCallback, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { genericTokenBalancesSelector } from '@dao-dao/state' import { Loader, useCachedLoading, useChain, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { TokenType, WidgetId } from '@dao-dao/types' -import { convertDenomToMicroDenomStringWithDecimals } from '@dao-dao/utils' import { SuspenseLoader } from '../../../../../../../components' import { useCw20CommonGovernanceTokenInfoIfExists } from '../../../../../../../voting-module-adapter/react/hooks/useCw20CommonGovernanceTokenInfoIfExists' @@ -32,7 +32,7 @@ import { CreateSurvey as StatelessCreateSurvey } from '../../stateless/pages/Cre export const CreateSurvey = () => { const { t } = useTranslation() const { chain_id: chainId } = useChain() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoPath } = useDaoNavHelpers() const router = useRouter() @@ -107,18 +107,18 @@ export const CreateSurvey = () => { if (cw20Decimals !== undefined) { cw20Tokens.push({ address: denomOrAddress, - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( amount, cw20Decimals - ), + ).toString(), }) } else if (nativeDecimals !== undefined) { nativeTokens.push({ denom: denomOrAddress, - amount: convertDenomToMicroDenomStringWithDecimals( + amount: HugeDecimal.fromHumanReadable( amount, nativeDecimals - ), + ).toString(), }) } else { // Should never happen, but just in case. diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/Home.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/Home.tsx index 2a72d4804..5442c26fe 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/Home.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/Home.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' -import { useDaoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { IconButtonLink } from '../../../../../../../components' import { @@ -13,7 +13,7 @@ import { Home as StatelessHome } from '../../stateless/pages/Home' import { SurveyRow } from '../SurveyRow' export const Home = () => { - const { dao } = useDaoContext() + const dao = useDao() const { hexPublicKey } = useWallet({ loadAccount: true, }) diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Complete.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Complete.tsx index f33d9e6de..788d98216 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Complete.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Complete.tsx @@ -9,7 +9,7 @@ import { Loader, useCachedLoadable, useChain, - useDaoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { TokenType, UnifiedCosmosMsg } from '@dao-dao/types' @@ -53,7 +53,7 @@ export const Complete = ({ isMember, }: ViewSurveyPageProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { goToDaoProposal } = useDaoNavHelpers() const { chain_id: chainId, bech32_prefix: bech32Prefix } = useChain() const { address: walletAddress = '' } = useWallet() diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Info.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Info.tsx index 45ba48bc2..20fa4b357 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Info.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Info.tsx @@ -3,7 +3,7 @@ import { unparse as jsonToCsv } from 'papaparse' import { useState } from 'react' import toast from 'react-hot-toast' -import { useChain, useDaoContext } from '@dao-dao/stateless' +import { useChain, useDao } from '@dao-dao/stateless' import { secp256k1PublicKeyToBech32Address } from '@dao-dao/utils' import { ButtonLink } from '../../../../../../../../components' @@ -13,7 +13,7 @@ import { Info as StatelessInfo } from '../../../stateless/pages/ViewSurvey/Info' import { ViewSurveyPageProps } from './types' export const Info = ({ status, isMember }: ViewSurveyPageProps) => { - const { dao } = useDaoContext() + const dao = useDao() const { bech32_prefix: bech32Prefix } = useChain() const postRequest = usePostRequest() diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Rate.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Rate.tsx index 08dd7bade..1049ef07a 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Rate.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Rate.tsx @@ -8,12 +8,7 @@ import { cosmWasmClientForChainSelector, genericTokenWithUsdPriceSelector, } from '@dao-dao/state/recoil' -import { - Loader, - useCachedLoadable, - useChain, - useDaoInfoContext, -} from '@dao-dao/stateless' +import { Loader, useCachedLoadable, useChain, useDao } from '@dao-dao/stateless' import { TokenType } from '@dao-dao/types' import { secp256k1PublicKeyToBech32Address } from '@dao-dao/utils' @@ -41,7 +36,7 @@ import { ViewSurveyPageProps } from './types' export const Rate = ({ status, refreshRef, isMember }: ViewSurveyPageProps) => { const { t } = useTranslation() const { chain_id: chainId, bech32_prefix: bech32Prefix } = useChain() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const client = useRecoilValue(cosmWasmClientForChainSelector(chainId)) const postRequest = usePostRequest() diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Submit.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Submit.tsx index c7ac8c14f..a0c475afa 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Submit.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/Submit.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' -import { useDaoContext } from '@dao-dao/stateless' +import { useDao } from '@dao-dao/stateless' import { ConnectWallet, @@ -22,7 +22,7 @@ export const Submit = ({ connected, }: ViewSurveyPageProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { address: walletAddress = '' } = useWallet() const { entity: walletEntity } = useEntity(walletAddress) const postRequest = usePostRequest() diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/ViewSurvey.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/ViewSurvey.tsx index 7a417a7c3..15eb1fe42 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/ViewSurvey.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateful/pages/ViewSurvey/ViewSurvey.tsx @@ -4,7 +4,7 @@ import { ComponentType } from 'react' import { ErrorPage, Loader, - useDaoContext, + useDao, useDaoNavHelpers, useUpdatingRef, } from '@dao-dao/stateless' @@ -24,7 +24,7 @@ import { Submit } from './Submit' import { ViewSurveyPageProps } from './types' export const ViewSurvey = () => { - const { dao } = useDaoContext() + const dao = useDao() const { daoSubpathComponents } = useDaoNavHelpers() const { hexPublicKey } = useWallet({ diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/NewAttribute.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/NewAttribute.tsx index 87f85e88a..7e92cb2e2 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/NewAttribute.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/NewAttribute.tsx @@ -2,6 +2,7 @@ import { Add, Close } from '@mui/icons-material' import { useFieldArray, useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, IconButton, @@ -12,10 +13,7 @@ import { useChainContext, } from '@dao-dao/stateless' import { GenericToken } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - validateRequired, -} from '@dao-dao/utils' +import { validateRequired } from '@dao-dao/utils' import { NewSurveyFormData } from '../../types' @@ -37,6 +35,7 @@ export const NewAttribute = ({ register, formState: { errors }, setValue, + getValues, watch, } = useFormContext<NewSurveyFormData>() @@ -126,13 +125,16 @@ export const NewAttribute = ({ amount={{ watch, setValue, + getValues, register, fieldName: `attributes.${attributeIndex}.tokens.${tokenIndex}.amount`, error: errors?.attributes?.[attributeIndex]?.tokens?.[tokenIndex] ?.amount, - step: convertMicroDenomToDenomWithDecimals( - 1, + min: HugeDecimal.one.toHumanReadableNumber( + selectedToken?.decimals ?? 0 + ), + step: HugeDecimal.one.toHumanReadableNumber( selectedToken?.decimals ?? 0 ), }} diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/SurveyRow.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/SurveyRow.tsx index ff409e856..f87c64768 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/SurveyRow.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/SurveyRow.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { ProposalWalletVote, Tooltip, - useDaoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { LinkWrapperProps, WidgetId } from '@dao-dao/types' @@ -51,7 +51,7 @@ export const SurveyRow = ({ LinkWrapper, }: SurveyRowProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { getDaoPath } = useDaoNavHelpers() // Display upcoming date first, then date when contributions close. Even diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/TabRenderer.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/TabRenderer.tsx index eb54641c7..a09034229 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/TabRenderer.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/TabRenderer.tsx @@ -2,11 +2,7 @@ import { Add, ArrowBackRounded, Remove } from '@mui/icons-material' import { ComponentType } from 'react' import { useTranslation } from 'react-i18next' -import { - Tooltip, - useDaoInfoContext, - useDaoNavHelpers, -} from '@dao-dao/stateless' +import { Tooltip, useDao, useDaoNavHelpers } from '@dao-dao/stateless' import { ButtonLinkProps, WidgetId } from '@dao-dao/types' import { PagePath } from '../../types' @@ -23,7 +19,7 @@ export const TabRenderer = ({ ButtonLink, }: TabRendererProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { daoSubpathComponents, getDaoPath } = useDaoNavHelpers() const pagePath = daoSubpathComponents[1] || '' diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/Home.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/Home.tsx index 3d615d431..cfbb322c5 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/Home.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/Home.tsx @@ -6,7 +6,7 @@ import { ErrorPage, LineLoader, NoContent, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { LoadingDataWithError, WidgetId } from '@dao-dao/types' @@ -28,7 +28,7 @@ export type HomeProps = { export const Home = ({ loadingSurveys, isMember, SurveyRow }: HomeProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoPath } = useDaoNavHelpers() return ( diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Complete.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Complete.tsx index 48d7dfd50..4d86bc893 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Complete.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Complete.tsx @@ -11,6 +11,7 @@ import { import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, CosmosMessageDisplay, @@ -33,11 +34,7 @@ import { StatefulProposalListProps, } from '@dao-dao/types' import { Boolean } from '@dao-dao/types/contracts/DaoVotingCw721Staked' -import { - convertMicroDenomToDenomWithDecimals, - decodedMessagesString, - validateRequired, -} from '@dao-dao/utils' +import { decodedMessagesString, validateRequired } from '@dao-dao/utils' import { NewProposalData } from '../../../../../../../../proposal-module-adapter/adapters/DaoProposalSingle/types' import { CompleteRatings, SurveyWithMetadata } from '../../../../types' @@ -459,19 +456,19 @@ export const InnerComplete = ({ .reduce( (acc, { denomOrAddress, amount }) => ({ ...acc, - [denomOrAddress]: - (acc[denomOrAddress] ?? 0) + - convertMicroDenomToDenomWithDecimals( - amount, - tokenMap[denomOrAddress]?.token.decimals ?? 0 - ), + [denomOrAddress]: ( + acc[denomOrAddress] ?? HugeDecimal.zero + ).plus(amount), }), - {} as Record<string, number> + {} as Record<string, HugeDecimal> ) const totalUsdc = Object.entries(tokens) - .map( - ([denomOrAddress, amount]) => - (tokenMap[denomOrAddress]?.usdPrice ?? 0) * amount + .map(([denomOrAddress, amount]) => + amount + .times(tokenMap[denomOrAddress]?.usdPrice ?? 0) + .toHumanReadableNumber( + tokenMap[denomOrAddress]?.token.decimals ?? 0 + ) ) .reduce((acc, amount) => acc + amount, 0) @@ -541,7 +538,6 @@ export const InnerComplete = ({ className="caption-text text-right" dateFetched={tokenPrices[0]?.timestamp} estimatedUsdValue - hideApprox prefix="= " /> </div> diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Info.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Info.tsx index d3147bdc8..2e149f28d 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Info.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Info.tsx @@ -2,12 +2,7 @@ import { ArrowOutward, Download } from '@mui/icons-material' import { ComponentType } from 'react' import { useTranslation } from 'react-i18next' -import { - Button, - Tooltip, - useDaoContext, - useDaoNavHelpers, -} from '@dao-dao/stateless' +import { Button, Tooltip, useDao, useDaoNavHelpers } from '@dao-dao/stateless' import { ButtonLinkProps, LoadingDataWithError } from '@dao-dao/types' import { formatDateTimeTz } from '@dao-dao/utils' @@ -46,7 +41,7 @@ export const Info = ({ ButtonLink, }: InfoProps) => { const { t } = useTranslation() - const { dao } = useDaoContext() + const dao = useDao() const { getDaoProposalPath } = useDaoNavHelpers() return ( diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Rate.tsx b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Rate.tsx index f12b35bbc..d624bc83f 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Rate.tsx +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/components/stateless/pages/ViewSurvey/Rate.tsx @@ -3,6 +3,7 @@ import { ComponentType, useEffect, useMemo } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Button, Checkbox, @@ -21,7 +22,6 @@ import { TransProps, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, formatDateTimeTz, makeValidateAddress, transformIpfsUrlToHttpsIfNecessary, @@ -282,19 +282,19 @@ export const Rate = ({ .reduce( (acc, { denomOrAddress, amount }) => ({ ...acc, - [denomOrAddress]: - (acc[denomOrAddress] ?? 0) + - convertMicroDenomToDenomWithDecimals( - amount, - tokenMap[denomOrAddress]?.token.decimals ?? 0 - ), + [denomOrAddress]: ( + acc[denomOrAddress] ?? HugeDecimal.zero + ).plus(amount), }), - {} as Record<string, number> + {} as Record<string, HugeDecimal> ) const projectedTotalUsdc = Object.entries(projectedTokens) - .map( - ([denomOrAddress, amount]) => - (tokenMap[denomOrAddress]?.usdPrice ?? 0) * amount + .map(([denomOrAddress, amount]) => + amount + .times(tokenMap[denomOrAddress]?.usdPrice ?? 0) + .toHumanReadableNumber( + tokenMap[denomOrAddress]?.token.decimals ?? 0 + ) ) .reduce((acc, amount) => acc + amount, 0) @@ -428,7 +428,6 @@ export const Rate = ({ className="caption-text text-right" dateFetched={tokenPrices[0]?.timestamp} estimatedUsdValue - hideApprox prefix="= " /> </div> diff --git a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/utils.ts b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/utils.ts index dfecd8e4c..996b7f7f8 100644 --- a/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/utils.ts +++ b/packages/stateful/widgets/widgets/RetroactiveCompensation/Renderer/utils.ts @@ -1,3 +1,5 @@ +import { HugeDecimal } from '@dao-dao/math' + import { AnyToken, Attribute, @@ -86,18 +88,20 @@ export const computeCompensation = ( const nativeTokens = attribute.nativeTokens.map( ({ denom, amount }): AnyToken => ({ denomOrAddress: denom, - amount: BigInt( - Math.floor(Number(amount) * proportionalCompensation) - ).toString(), + amount: HugeDecimal.from(amount) + .times(proportionalCompensation) + .trunc() + .toString(), }) ) const cw20Tokens = attribute.cw20Tokens.map( ({ address, amount }): AnyToken => ({ denomOrAddress: address, - amount: BigInt( - Math.floor(Number(amount) * proportionalCompensation) - ).toString(), + amount: HugeDecimal.from(amount) + .times(proportionalCompensation) + .trunc() + .toString(), }) ) diff --git a/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/TabRenderer.tsx b/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/TabRenderer.tsx index 17dbf6f4b..44f7d2c36 100644 --- a/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/TabRenderer.tsx +++ b/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/TabRenderer.tsx @@ -11,7 +11,7 @@ import { Modal, NoContent, Tooltip, - useDaoInfoContext, + useDao, useDaoNavHelpers, } from '@dao-dao/stateless' import { @@ -48,7 +48,7 @@ export const TabRenderer = ({ Trans, }: TabRendererProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { daoSubpathComponents, goToDao } = useDaoNavHelpers() const { address: walletAddress } = useWallet() diff --git a/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/index.tsx b/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/index.tsx index d32f296fa..870a34633 100644 --- a/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/index.tsx +++ b/packages/stateful/widgets/widgets/VestingPayments/Renderer/TabRenderer/index.tsx @@ -7,7 +7,7 @@ import { cwVestingExtraQueries, } from '@dao-dao/state/query' import { - useDaoInfoContext, + useDao, useDaoNavHelpers, useInitializedActionForKey, } from '@dao-dao/stateless' @@ -33,7 +33,7 @@ import { TabRenderer as StatelessTabRenderer } from './TabRenderer' export const TabRenderer = ({ variables: { factories, factory, oldFactories }, }: WidgetRendererProps<VestingPaymentsWidgetData>) => { - const { chainId: defaultChainId, coreAddress, accounts } = useDaoInfoContext() + const { chainId: defaultChainId, coreAddress, accounts } = useDao() const { getDaoProposalPath } = useDaoNavHelpers() const { isMember = false } = useMembership() diff --git a/packages/stateless/components/PayEntityDisplay.tsx b/packages/stateless/components/PayEntityDisplay.tsx index 957912b5f..610cfad48 100644 --- a/packages/stateless/components/PayEntityDisplay.tsx +++ b/packages/stateless/components/PayEntityDisplay.tsx @@ -4,8 +4,8 @@ import { } from '@mui/icons-material' import clsx from 'clsx' +import { HugeDecimal } from '@dao-dao/math' import { PayEntityDisplayProps, PayEntityDisplayRowProps } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' import { useDetectWrap } from '../hooks' import { TokenAmountDisplay } from './token' @@ -41,7 +41,7 @@ const PayEntityDisplayRow = ({ > <TokenAmountDisplay key={token.denomOrAddress} - amount={convertMicroDenomToDenomWithDecimals(amount, token.decimals)} + amount={HugeDecimal.from(amount)} decimals={token.decimals} iconUrl={token.imageUrl} showFullAmount diff --git a/packages/stateless/components/ValidatorPicker.tsx b/packages/stateless/components/ValidatorPicker.tsx index 8f44abd73..a3fe53b0f 100644 --- a/packages/stateless/components/ValidatorPicker.tsx +++ b/packages/stateless/components/ValidatorPicker.tsx @@ -3,14 +3,12 @@ import clsx from 'clsx' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { PopupTriggerCustomComponent, ValidatorPickerProps, } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - formatPercentOf100, -} from '@dao-dao/utils' +import { formatPercentOf100 } from '@dao-dao/utils' import { Button } from './buttons' import { CopyToClipboard } from './CopyToClipboard' @@ -41,7 +39,7 @@ export const ValidatorPicker = ({ ...acc, [stake.validator.address]: stake.amount, }), - {} as Record<string, number | undefined> + {} as Record<string, HugeDecimal | undefined> ) // Sort staked first, then by total staked tokens (i.e. voting power and @@ -53,7 +51,7 @@ export const ValidatorPicker = ({ // If both validators have a stake, sort by stake. if (aStake && bStake) { - return bStake - aStake + return bStake.minus(aStake).toNumber() } // If only one validator has a stake, sort that one first. else if (aStake) { @@ -64,7 +62,7 @@ export const ValidatorPicker = ({ // If neither validator has a stake, sort by total tokens staked (i.e. // popularity). - return b.tokens - a.tokens + return b.tokens.minus(a.tokens).toNumber() }) const TriggerRenderer: PopupTriggerCustomComponent = useCallback( @@ -131,10 +129,7 @@ export const ValidatorPicker = ({ </p> <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - tokens, - token.decimals - )} + amount={tokens} className="inline-block" decimals={token.decimals} prefix={t('title.totalStaked') + ': '} diff --git a/packages/stateless/components/actions/ActionsEditor.tsx b/packages/stateless/components/actions/ActionsEditor.tsx index 7350ec0a4..21af8fc47 100644 --- a/packages/stateless/components/actions/ActionsEditor.tsx +++ b/packages/stateless/components/actions/ActionsEditor.tsx @@ -24,7 +24,7 @@ import { ActionMap, } from '@dao-dao/types/actions' -import { useActionsContext, useDaoInfoContextIfAvailable } from '../../contexts' +import { useActionsContext, useDaoIfAvailable } from '../../contexts' import { useLoadingPromise } from '../../hooks' import { Loader } from '../logo' import { ActionCard } from './ActionCard' @@ -52,7 +52,7 @@ export const ActionsEditor = ({ }>() const { actionMap } = useActionsContext() - const isDao = !!useDaoInfoContextIfAvailable() + const isDao = !!useDaoIfAvailable() // Type assertion assumes the passed in field name is correct. const actionDataFieldName = _actionDataFieldName as 'actionData' diff --git a/packages/stateless/components/actions/NativeCoinSelector.tsx b/packages/stateless/components/actions/NativeCoinSelector.tsx index 581c8844a..7bbbe556f 100644 --- a/packages/stateless/components/actions/NativeCoinSelector.tsx +++ b/packages/stateless/components/actions/NativeCoinSelector.tsx @@ -3,6 +3,7 @@ import { ComponentProps, useState } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { ActionComponent, GenericToken, @@ -10,7 +11,6 @@ import { LoadingData, TokenType, } from '@dao-dao/types' -import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' import { IconButton } from '../icon_buttons' import { InputErrorMessage, TokenInput } from '../inputs' @@ -83,7 +83,8 @@ export const NativeCoinSelector = ({ const amountField = (fieldNamePrefix + 'amount') as 'amount' const decimalsField = (fieldNamePrefix + 'decimals') as 'decimals' - const { register, setValue, watch } = useFormContext<NativeCoinForm>() + const { register, setValue, getValues, watch } = + useFormContext<NativeCoinForm>() const watchDenom = watch(denomField) const watchAmount = watch(amountField) const watchDecimals = watch(decimalsField) || 0 @@ -99,10 +100,7 @@ export const NativeCoinSelector = ({ token.denomOrAddress === watchDenom && (!chainId || token.chainId === chainId) ) - const balance = convertMicroDenomToDenomWithDecimals( - selectedToken?.balance ?? 0, - selectedToken?.token.decimals ?? 0 - ) + const balance = HugeDecimal.from(selectedToken?.balance ?? 0) const decimals = customToken ? watchDecimals @@ -121,16 +119,16 @@ export const NativeCoinSelector = ({ ? undefined : !selectedToken ? t('error.unknownDenom', { denom: watchDenom }) - : watchAmount > balance + : balance.toHumanReadable(decimals).lt(watchAmount) ? t('error.insufficientFundsWarning', { - amount: balance.toLocaleString(undefined, { - maximumFractionDigits: decimals, + amount: balance.toInternationalizedHumanReadableString({ + decimals, }), tokenSymbol: symbol, }) : undefined - const minUnit = convertMicroDenomToDenomWithDecimals(1, decimals) + const minUnit = HugeDecimal.one.toHumanReadableNumber(decimals) const minAmount = min ?? minUnit return ( @@ -140,6 +138,7 @@ export const NativeCoinSelector = ({ amount={{ watch, setValue, + getValues, register, fieldName: amountField, error: errors?.amount || errors?._error, @@ -164,11 +163,10 @@ export const NativeCoinSelector = ({ description: t('title.balance') + ': ' + - convertMicroDenomToDenomWithDecimals( - balance, - token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: token.decimals, + HugeDecimal.from( + balance + ).toInternationalizedHumanReadableString({ + decimals: token.decimals, }), })), } diff --git a/packages/stateless/components/dao/DaoCard.tsx b/packages/stateless/components/dao/DaoCard.tsx index 6c0495fca..99ee75a1b 100644 --- a/packages/stateless/components/dao/DaoCard.tsx +++ b/packages/stateless/components/dao/DaoCard.tsx @@ -119,12 +119,8 @@ export const DaoCard = ({ amount={ lazyData.loading || !lazyData.data.tokenWithBalance ? { loading: true } - : { - loading: false, - data: Number(lazyData.data.tokenWithBalance.balance), - } + : lazyData.data.tokenWithBalance.balance } - hideApprox {...(showingEstimatedUsdValue ? { estimatedUsdValue: true, diff --git a/packages/stateless/components/dao/DaoMemberCard.tsx b/packages/stateless/components/dao/DaoMemberCard.tsx index 4078c6729..7f82957fb 100644 --- a/packages/stateless/components/dao/DaoMemberCard.tsx +++ b/packages/stateless/components/dao/DaoMemberCard.tsx @@ -108,11 +108,7 @@ export const DaoMemberCard = ({ <p className="caption-text">{balanceLabel}</p> <TokenAmountDisplay - amount={ - balance.loading - ? { loading: true } - : { loading: false, data: balance.data.amount } - } + amount={balance.loading ? { loading: true } : balance.data.amount} className="caption-text font-mono" decimals={ balance.loading || !balance.data.token diff --git a/packages/stateless/components/dao/DaoRewardsDistributorClaimCard.tsx b/packages/stateless/components/dao/DaoRewardsDistributorClaimCard.tsx index 009affcc6..54e91ccd7 100644 --- a/packages/stateless/components/dao/DaoRewardsDistributorClaimCard.tsx +++ b/packages/stateless/components/dao/DaoRewardsDistributorClaimCard.tsx @@ -59,12 +59,10 @@ export const DaoRewardsDistributorClaimCard = ({ )} dateFetched={totalTimestamp} decimals={2} - hideApprox hideSymbol - maxDecimals={2} minAmount={hasRewards ? 0.01 : undefined} - minDecimals={2} prefix="$" + showAllDecimals /> </div> @@ -140,7 +138,7 @@ export const DaoRewardsDistributorClaimCard = ({ token.imageUrl || getFallbackImage(token.denomOrAddress) } - minDecimals={token.decimals} + showAllDecimals showFullAmount suffix={' $' + token.symbol} suffixClassName="whitespace-pre text-text-tertiary" @@ -156,12 +154,10 @@ export const DaoRewardsDistributorClaimCard = ({ )} dateFetched={timestamp} decimals={2} - hideApprox hideSymbol - maxDecimals={2} minAmount={usdValue > 0 ? 0.01 : undefined} - minDecimals={2} prefix="$" + showAllDecimals /> </div> </div> diff --git a/packages/stateless/components/dao/DaoSplashHeader.stories.tsx b/packages/stateless/components/dao/DaoSplashHeader.stories.tsx index 4dd8928b6..85c8d5bbb 100644 --- a/packages/stateless/components/dao/DaoSplashHeader.stories.tsx +++ b/packages/stateless/components/dao/DaoSplashHeader.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { DaoPageWrapperDecorator } from '@dao-dao/storybook' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { ButtonLink } from '../buttons' import { LinkWrapper } from '../LinkWrapper' import { DaoSplashHeader } from './DaoSplashHeader' @@ -14,7 +14,7 @@ export default { } as ComponentMeta<typeof DaoSplashHeader> const Template: ComponentStory<typeof DaoSplashHeader> = (args) => ( - <DaoSplashHeader {...args} daoInfo={useDaoInfoContext()} /> + <DaoSplashHeader {...args} dao={useDao()} /> ) export const Default = Template.bind({}) diff --git a/packages/stateless/components/dao/DaoSplashHeader.tsx b/packages/stateless/components/dao/DaoSplashHeader.tsx index 6f77047b3..71661a61b 100644 --- a/packages/stateless/components/dao/DaoSplashHeader.tsx +++ b/packages/stateless/components/dao/DaoSplashHeader.tsx @@ -7,7 +7,7 @@ import { formatPercentOf100 } from '@dao-dao/utils' import { DaoHeader } from './DaoHeader' export const DaoSplashHeader = ({ - daoInfo, + dao, follow, ButtonLink, LinkWrapper, @@ -18,32 +18,32 @@ export const DaoSplashHeader = ({ return ( <> - {!daoInfo.isActive && daoInfo.activeThreshold && ( + {!dao.info.isActive && dao.info.activeThreshold && ( <div className="mb-10 -mt-4 flex flex-row items-center justify-center gap-3 rounded-md bg-background-interactive-warning p-3 md:gap-2"> <WarningRounded className="!h-10 !w-10 text-icon-interactive-warning md:!h-6 md:!w-6" /> <p className="text-text-interactive-warning-body"> {t('error.daoIsInactive', { context: - 'percentage' in daoInfo.activeThreshold + 'percentage' in dao.info.activeThreshold ? 'percent' : 'absolute', percent: - 'percentage' in daoInfo.activeThreshold + 'percentage' in dao.info.activeThreshold ? formatPercentOf100( - Number(daoInfo.activeThreshold.percentage.percent) * 100 + Number(dao.info.activeThreshold.percentage.percent) * 100 ) : undefined, count: - 'percentage' in daoInfo.activeThreshold + 'percentage' in dao.info.activeThreshold ? undefined - : Number(daoInfo.activeThreshold.absolute_count.count), + : Number(dao.info.activeThreshold.absolute_count.count), })} </p> </div> )} - {daoInfo.parentDao && !daoInfo.parentDao.registeredSubDao && ( + {dao.info.parentDao && !dao.info.parentDao.registeredSubDao && ( <ButtonLink center className="mb-10 -mt-4 bg-background-interactive-warning" @@ -56,8 +56,8 @@ export const DaoSplashHeader = ({ <p className="text-text-interactive-warning-body"> {t('info.subDaoNotYetRecognized', { - parent: daoInfo.parentDao.name, - child: daoInfo.name, + parent: dao.info.parentDao.name, + child: dao.name, })} {!!parentProposalRecognizeSubDaoHref && ( @@ -69,7 +69,7 @@ export const DaoSplashHeader = ({ </ButtonLink> )} - {daoInfo.parentDao && daoInfo.contractAdmin === daoInfo.coreAddress && ( + {dao.info.parentDao && dao.info.contractAdmin === dao.coreAddress && ( <ButtonLink center className="mb-10 -mt-4 bg-background-interactive-warning" @@ -82,15 +82,15 @@ export const DaoSplashHeader = ({ <p className="text-text-interactive-warning-body"> {t('info.parentDaoNotAdmin', { - parent: daoInfo.parentDao.name, - child: daoInfo.name, + parent: dao.info.parentDao.name, + child: dao.name, })} {!!proposeUpdateAdminToParentHref && ( <span className="font-bold"> {' ' + t('button.clickHereToProposeSettingAdminToParent', { - parent: daoInfo.parentDao.name, + parent: dao.info.parentDao.name, })} </span> )} @@ -100,12 +100,12 @@ export const DaoSplashHeader = ({ <DaoHeader LinkWrapper={LinkWrapper} - coreAddress={daoInfo.coreAddress} - description={daoInfo.description} + coreAddress={dao.coreAddress} + description={dao.description} follow={follow} - imageUrl={daoInfo.imageUrl} - name={daoInfo.name} - parentDao={daoInfo.parentDao} + imageUrl={dao.imageUrl} + name={dao.name} + parentDao={dao.info.parentDao} /> </> ) diff --git a/packages/stateless/components/dao/MainDaoInfoCardsTokenLoader.tsx b/packages/stateless/components/dao/MainDaoInfoCardsTokenLoader.tsx index 0824f39af..c14f72dd9 100644 --- a/packages/stateless/components/dao/MainDaoInfoCardsTokenLoader.tsx +++ b/packages/stateless/components/dao/MainDaoInfoCardsTokenLoader.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import { formatDate } from '@dao-dao/utils' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { DaoInfoCards } from './DaoInfoCards' /** @@ -11,7 +11,7 @@ import { DaoInfoCards } from './DaoInfoCards' */ export const MainDaoInfoCardsTokenLoader = () => { const { t } = useTranslation() - const { activeThreshold, created } = useDaoInfoContext() + const { activeThreshold, created } = useDao().info return ( <DaoInfoCards diff --git a/packages/stateless/components/dao/create/DaoCreateConfigInputCard.stories.tsx b/packages/stateless/components/dao/create/DaoCreateConfigInputCard.stories.tsx index 7d74a074a..f62fa653a 100644 --- a/packages/stateless/components/dao/create/DaoCreateConfigInputCard.stories.tsx +++ b/packages/stateless/components/dao/create/DaoCreateConfigInputCard.stories.tsx @@ -17,7 +17,7 @@ export default { } as ComponentMeta<typeof DaoCreateConfigInputCard> const Template: ComponentStory<typeof DaoCreateConfigInputCard> = (args) => { - const { register, watch, setValue } = useForm<NewDao>({ + const { register, watch, setValue, getValues } = useForm<NewDao>({ defaultValues: makeDefaultNewDao(CHAIN_ID), mode: 'onChange', }) @@ -32,6 +32,22 @@ const Template: ComponentStory<typeof DaoCreateConfigInputCard> = (args) => { <VotingDurationInput data={newDao.proposalModuleAdapters[0].data} fieldNamePrefix="proposalModuleAdapters.0.data." + getValues={(fieldNameOrNames?: string | readonly string[]) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + ('proposalModuleAdapters.0.data.' + + fieldNameOrNames) as `proposalModuleAdapters.${number}.data.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + ('proposalModuleAdapters.0.data.' + + fieldName) as `proposalModuleAdapters.${number}.data.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register( diff --git a/packages/stateless/components/dao/create/pages/CreateDaoVoting.tsx b/packages/stateless/components/dao/create/pages/CreateDaoVoting.tsx index 4b1c22d25..2b8c6a611 100644 --- a/packages/stateless/components/dao/create/pages/CreateDaoVoting.tsx +++ b/packages/stateless/components/dao/create/pages/CreateDaoVoting.tsx @@ -14,6 +14,7 @@ export const CreateDaoVoting = ({ register, watch, setValue, + getValues, }, commonVotingConfig: { items: commonVotingConfigItems, @@ -94,6 +95,24 @@ export const CreateDaoVoting = ({ data={creatorData} errors={errors.creator?.data} fieldNamePrefix="creator.data." + getValues={( + fieldNameOrNames?: string | readonly string[] + ) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + ('creator.data.' + + fieldNameOrNames) as `creator.data.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + ('creator.data.' + + fieldName) as `creator.data.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register( @@ -155,6 +174,24 @@ export const CreateDaoVoting = ({ data={proposalModuleAdapters[index].data} errors={errors.proposalModuleAdapters?.[index]?.data} fieldNamePrefix={`proposalModuleAdapters.${index}.data.`} + getValues={( + fieldNameOrNames?: string | readonly string[] + ) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + (`proposalModuleAdapters.${index}.data.` + + fieldNameOrNames) as `proposalModuleAdapters.${number}.data.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + (`proposalModuleAdapters.${index}.data.` + + fieldName) as `proposalModuleAdapters.${number}.data.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register( @@ -213,6 +250,24 @@ export const CreateDaoVoting = ({ data={votingConfig} errors={errors.votingConfig} fieldNamePrefix="votingConfig." + getValues={( + fieldNameOrNames?: string | readonly string[] + ) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + ('votingConfig.' + + fieldNameOrNames) as `votingConfig.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + ('votingConfig.' + + fieldName) as `votingConfig.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register(('votingConfig.' + fieldName) as any, options) @@ -317,6 +372,24 @@ export const CreateDaoVoting = ({ data={creatorData} errors={errors.creator?.data} fieldNamePrefix="creator.data." + getValues={( + fieldNameOrNames?: string | readonly string[] + ) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + ('creator.data.' + + fieldNameOrNames) as `creator.data.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + ('creator.data.' + + fieldName) as `creator.data.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register( @@ -380,6 +453,24 @@ export const CreateDaoVoting = ({ errors.proposalModuleAdapters?.[index]?.data } fieldNamePrefix={`proposalModuleAdapters.${index}.data.`} + getValues={( + fieldNameOrNames?: string | readonly string[] + ) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + (`proposalModuleAdapters.${index}.data.` + + fieldNameOrNames) as `proposalModuleAdapters.${number}.data.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + (`proposalModuleAdapters.${index}.data.` + + fieldName) as `proposalModuleAdapters.${number}.data.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register( @@ -438,6 +529,24 @@ export const CreateDaoVoting = ({ data={votingConfig} errors={errors.votingConfig} fieldNamePrefix="votingConfig." + getValues={( + fieldNameOrNames?: string | readonly string[] + ) => + fieldNameOrNames === undefined + ? getValues() + : typeof fieldNameOrNames === 'string' + ? getValues( + ('votingConfig.' + + fieldNameOrNames) as `votingConfig.${string}` + ) + : getValues( + fieldNameOrNames.map( + (fieldName) => + ('votingConfig.' + + fieldName) as `votingConfig.${string}` + ) + ) + } newDao={newDao} register={(fieldName, options) => register( diff --git a/packages/stateless/components/dao/tabs/MembersTab.tsx b/packages/stateless/components/dao/tabs/MembersTab.tsx index 26641a7b5..ebe9347e3 100644 --- a/packages/stateless/components/dao/tabs/MembersTab.tsx +++ b/packages/stateless/components/dao/tabs/MembersTab.tsx @@ -4,6 +4,7 @@ import { ComponentType, Fragment, useRef, useState } from 'react' import { CSVLink } from 'react-csv' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { ButtonLinkProps, LoadingDataWithError, @@ -298,7 +299,13 @@ export const MembersTab = ({ ...members.data.map( ({ address, balance, votingPowerPercent }) => [ address, - balance.loading ? '...' : balance.data.amount.toString(), + balance.loading + ? '...' + : HugeDecimal.from( + balance.data.amount + ).toHumanReadableString( + balance.data.token?.decimals ?? 0 + ), votingPowerPercent.loading ? '...' : votingPowerPercent.data, ] ), diff --git a/packages/stateless/components/dao/tabs/ProposalsTab.stories.tsx b/packages/stateless/components/dao/tabs/ProposalsTab.stories.tsx index ab3d64c71..62d879f29 100644 --- a/packages/stateless/components/dao/tabs/ProposalsTab.stories.tsx +++ b/packages/stateless/components/dao/tabs/ProposalsTab.stories.tsx @@ -3,7 +3,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { DaoPageWrapperDecorator } from '@dao-dao/storybook/decorators' import { ProposalListProps } from '@dao-dao/types' -import { useDaoInfoContext } from '../../../contexts/Dao' +import { useDao } from '../../../contexts/Dao' import { ButtonLink } from '../../buttons' import { ProposalLineProps, ProposalList } from '../../proposal' import * as ProposalListStories from '../../proposal/ProposalList.stories' @@ -17,7 +17,7 @@ export default { } as ComponentMeta<typeof ProposalsTab> const Template: ComponentStory<typeof ProposalsTab> = (args) => ( - <ProposalsTab {...args} daoInfo={useDaoInfoContext()} /> + <ProposalsTab {...args} dao={useDao()} /> ) export const Default = Template.bind({}) diff --git a/packages/stateless/components/dao/tabs/ProposalsTab.tsx b/packages/stateless/components/dao/tabs/ProposalsTab.tsx index bfa69a40d..6c00391cc 100644 --- a/packages/stateless/components/dao/tabs/ProposalsTab.tsx +++ b/packages/stateless/components/dao/tabs/ProposalsTab.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { ButtonLinkProps, - DaoInfo, + IDaoBase, StatefulProposalListProps, } from '@dao-dao/types' @@ -13,13 +13,13 @@ import { useDaoNavHelpers, usePlatform } from '../../../hooks' import { Tooltip } from '../../tooltip/Tooltip' export interface ProposalsTabProps { - daoInfo: DaoInfo + dao: IDaoBase ProposalList: ComponentType<StatefulProposalListProps> ButtonLink: ComponentType<ButtonLinkProps> } export const ProposalsTab = ({ - daoInfo, + dao, ProposalList, ButtonLink, }: ProposalsTabProps) => { @@ -35,14 +35,14 @@ export const ProposalsTab = ({ if (((!isMac && event.ctrlKey) || event.metaKey) && event.shiftKey) { if (event.key === 'p') { event.preventDefault() - goToDaoProposal(daoInfo.coreAddress, 'create') + goToDaoProposal(dao.coreAddress, 'create') } } } document.addEventListener('keydown', handleKeyPress) return () => document.removeEventListener('keydown', handleKeyPress) - }, [isMac, daoInfo.coreAddress, goToDaoProposal]) + }, [isMac, dao.coreAddress, goToDaoProposal]) return ( <> @@ -64,7 +64,7 @@ export const ProposalsTab = ({ > <ButtonLink className="shrink-0" - href={getDaoProposalPath(daoInfo.coreAddress, 'create')} + href={getDaoProposalPath(dao.coreAddress, 'create')} > <Add className="!h-4 !w-4" /> <span className="hidden md:inline">{t('button.newProposal')}</span> diff --git a/packages/stateless/components/dao/tabs/SubDaosTab.tsx b/packages/stateless/components/dao/tabs/SubDaosTab.tsx index 2577f06d4..e30a1e2a8 100644 --- a/packages/stateless/components/dao/tabs/SubDaosTab.tsx +++ b/packages/stateless/components/dao/tabs/SubDaosTab.tsx @@ -11,7 +11,7 @@ import { StatefulDaoCardProps, } from '@dao-dao/types' -import { useDaoInfoContext } from '../../../contexts' +import { useDao } from '../../../contexts' import { useDaoNavHelpers } from '../../../hooks' import { ErrorPage } from '../../error' import { GridCardContainer } from '../../GridCardContainer' @@ -37,8 +37,12 @@ export const SubDaosTab = ({ ButtonLink, }: SubDaosTabProps) => { const { t } = useTranslation() - const { coreAddress, coreVersion, name, supportedFeatures } = - useDaoInfoContext() + const { + coreAddress, + coreVersion, + name, + info: { supportedFeatures }, + } = useDao() const { getDaoPath } = useDaoNavHelpers() const subDaosSupported = diff --git a/packages/stateless/components/dao/tabs/TreasuryTab.tsx b/packages/stateless/components/dao/tabs/TreasuryTab.tsx index 65507a4d7..df28dab85 100644 --- a/packages/stateless/components/dao/tabs/TreasuryTab.tsx +++ b/packages/stateless/components/dao/tabs/TreasuryTab.tsx @@ -23,7 +23,7 @@ import { serializeTokenSource, } from '@dao-dao/utils' -import { useDaoInfoContext, useSupportedChainContext } from '../../../contexts' +import { useDao, useSupportedChainContext } from '../../../contexts' import { useButtonPopupSorter, useTokenSortOptions } from '../../../hooks' import { ErrorPage } from '../../error' import { AccountSelector } from '../../inputs' @@ -76,7 +76,7 @@ export const TreasuryTab = <T extends TokenCardInfo, N extends object>({ chain: { chain_id: currentChainId }, config: { noIndexer }, } = useSupportedChainContext() - const { chainId: daoChainId, coreAddress, accounts } = useDaoInfoContext() + const { chainId: daoChainId, coreAddress, accounts } = useDao() // Combine chain tokens into loadable, lazily. Load all that are ready. const { nonValenceTokens, valenceTokens } = useMemo((): { diff --git a/packages/stateless/components/inputs/AccountSelector.tsx b/packages/stateless/components/inputs/AccountSelector.tsx index d19df689d..090b116b4 100644 --- a/packages/stateless/components/inputs/AccountSelector.tsx +++ b/packages/stateless/components/inputs/AccountSelector.tsx @@ -21,7 +21,7 @@ export type AccountSelectorProps = { /** * The list of accounts. */ - accounts: Account[] + accounts: readonly Account[] /** * Account selection callback function. */ diff --git a/packages/stateless/components/inputs/DaoSupportedChainPickerInput.tsx b/packages/stateless/components/inputs/DaoSupportedChainPickerInput.tsx index 564154afe..941d707de 100644 --- a/packages/stateless/components/inputs/DaoSupportedChainPickerInput.tsx +++ b/packages/stateless/components/inputs/DaoSupportedChainPickerInput.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import { AccountType, ChainPickerPopupProps } from '@dao-dao/types' import { getIbcTransferChainIdsForChain } from '@dao-dao/utils' -import { useChainContext, useDaoInfoContextIfAvailable } from '../../contexts' +import { useChainContext, useDaoIfAvailable } from '../../contexts' import { ChainPickerPopup } from '../popup' import { InputLabel } from './InputLabel' @@ -77,13 +77,13 @@ export const DaoSupportedChainPickerInput = ({ config, } = useChainContext() const { watch, setValue } = useFormContext() - const daoInfo = useDaoInfoContextIfAvailable() + const dao = useDaoIfAvailable() const includeChainIds = - onlyDaoChainIds && daoInfo + onlyDaoChainIds && dao ? [ - daoInfo.chainId, - ...daoInfo.accounts.flatMap(({ type, chainId }) => + dao.chainId, + ...dao.accounts.flatMap(({ type, chainId }) => accountTypes.includes(type as any) ? chainId : [] ), ] diff --git a/packages/stateless/components/inputs/NumberInput.tsx b/packages/stateless/components/inputs/NumberInput.tsx deleted file mode 100644 index 2396a679b..000000000 --- a/packages/stateless/components/inputs/NumberInput.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { Add, Remove } from '@mui/icons-material' -import clsx from 'clsx' -import { FieldValues, Path } from 'react-hook-form' -import { useTranslation } from 'react-i18next' - -import { NumberInputProps } from '@dao-dao/types' -import { - convertDenomToMicroDenomWithDecimals, - convertMicroDenomToDenomWithDecimals, - toAccessibleImageUrl, -} from '@dao-dao/utils' - -import { IconButton } from '../icon_buttons' - -// To show plus/minus buttons, make sure to provide (`value` or -// `watch`+`fieldName`) in addition to `setValue`. When using a react-hook-form -// form, `setValue`, `watch`, and `fieldName` already exist. When not using a -// react-hook-form form, the `setValue` function can easily be mocked, and the -// first fieldName argument (which will be an empty string) can be ignored. - -export const NumberInput = < - FV extends FieldValues, - FieldName extends Path<FV> ->({ - fieldName, - register, - error, - validation, - hidePlusMinus, - value: _value, - watch, - setValue, - disabled, - sizing, - className, - containerClassName, - required, - transformDecimals, - ghost, - unit, - unitIconUrl, - textClassName, - unitClassName, - unitIconClassName, - unitContainerClassName, - plusMinusButtonSize = 'sm', - ...props -}: NumberInputProps<FV, FieldName>) => { - const { t } = useTranslation() - const validate = validation?.reduce( - (a, v) => ({ ...a, [v.toString()]: v }), - {} - ) - - const watchedField = watch && fieldName ? watch(fieldName) : _value - const untransformedValue = - !watchedField && watchedField !== 0 ? watchedField : Number(watchedField) - const value = - untransformedValue && transformDecimals - ? convertMicroDenomToDenomWithDecimals( - untransformedValue, - transformDecimals - ) - : untransformedValue - - return ( - <div - className={clsx( - 'flex flex-row items-center gap-2 bg-transparent transition', - // Padding and outline - !ghost && 'rounded-md py-3 px-4 ring-1 ring-inset focus-within:ring-2', - // Outline color - error - ? 'ring-border-interactive-error' - : 'ring-border-primary focus-within:ring-border-interactive-focus', - // Sizing - { - 'w-28': sizing === 'sm', - 'w-40': sizing === 'md', - 'w-56': sizing === 'lg', - 'w-28 md:w-32 lg:w-40': sizing === 'auto', - 'w-full': sizing === 'fill', - }, - containerClassName - )} - > - {/* Do not require `fieldName` to be set in case a form is not being used. As long as `setValue` and `value` are present, these buttons will work. `value` is present if `watch`+`fieldName` are defined, or `value` is set directly. */} - {!hidePlusMinus && !disabled && setValue && value !== undefined && ( - <div - className={clsx( - 'flex flex-row items-center gap-1', - // Add small gap between buttons when larger buttons are used. - plusMinusButtonSize === 'lg' && 'gap-1' - )} - > - {/* Minus button */} - <IconButton - Icon={Remove} - disabled={disabled} - iconClassName="text-icon-secondary" - onClick={() => - setValue( - fieldName ?? '', - Math.min( - Math.max( - // Subtract 1 whole number. - Number(((value || 0) - 1).toFixed(0)), - typeof props.min === 'number' ? props.min : -Infinity - ), - typeof props.max === 'number' ? props.max : Infinity - ), - { - shouldValidate: true, - } - ) - } - size={ - // The larger button size for this NumberInput corresponds to the - // default icon button size. - plusMinusButtonSize === 'lg' ? 'default' : plusMinusButtonSize - } - variant="ghost" - /> - - <IconButton - Icon={Add} - disabled={disabled} - iconClassName="text-icon-secondary" - onClick={() => - setValue( - fieldName ?? '', - Math.min( - Math.max( - // Add 1 whole number. - Number(((value || 0) + 1).toFixed(0)), - typeof props.min === 'number' ? props.min : -Infinity - ), - typeof props.max === 'number' ? props.max : Infinity - ), - { - shouldValidate: true, - } - ) - } - size={ - // The larger button size for this NumberInput corresponds to the - // default icon button size. - plusMinusButtonSize === 'lg' ? 'default' : plusMinusButtonSize - } - variant="ghost" - /> - </div> - )} - - <input - className={clsx( - 'ring-none secondary-text w-full grow appearance-none border-none bg-transparent text-right text-text-body outline-none', - className, - textClassName - )} - disabled={disabled} - type="number" - value={value} - {...props} - {...(register && - fieldName && - register(fieldName, { - required: required && t('info.required'), - validate, - setValueAs: (value) => { - // If not a number AND not a string or an empty string, set NaN. - // Empty strings get converted to 0 with the Number constructor, - // which we don't want, because then the input can't be cleared. - const newValue = - typeof value !== 'number' && - (typeof value !== 'string' || value.trim() === '') - ? NaN - : // On first load, setValueAs seems to be called with the first value, which is probably default loaded from a save. We - // don't want to transform this first value. - transformDecimals && value !== untransformedValue - ? convertDenomToMicroDenomWithDecimals( - value, - transformDecimals - ) - : Number(value) - - return newValue - }, - }))} - /> - - {(unit || unitIconUrl) && ( - <div - className={clsx( - 'flex flex-row items-center gap-1.5 max-w-[10rem] shrink-0 min-w-0', - unitContainerClassName - )} - > - {unitIconUrl && ( - <div - className={clsx( - 'h-5 w-5 shrink-0 bg-cover bg-center rounded-full ml-1', - unitIconClassName - )} - style={{ - backgroundImage: `url(${toAccessibleImageUrl(unitIconUrl)})`, - }} - /> - )} - - <p - className={clsx( - 'secondary-text max-w-[10rem] shrink-0 truncate text-right text-text-tertiary', - textClassName, - unitClassName - )} - > - {unit} - </p> - </div> - )} - </div> - ) -} diff --git a/packages/stateless/components/inputs/NumberInput.stories.tsx b/packages/stateless/components/inputs/NumericInput.stories.tsx similarity index 68% rename from packages/stateless/components/inputs/NumberInput.stories.tsx rename to packages/stateless/components/inputs/NumericInput.stories.tsx index 4b2fca422..75d638c6a 100644 --- a/packages/stateless/components/inputs/NumberInput.stories.tsx +++ b/packages/stateless/components/inputs/NumericInput.stories.tsx @@ -3,23 +3,23 @@ import { useFormContext } from 'react-hook-form' import { ReactHookFormDecorator } from '@dao-dao/storybook/decorators' -import { NumberInput } from './NumberInput' +import { NumericInput } from './NumericInput' export default { - title: 'DAO DAO / packages / stateless / components / inputs / NumberInput', - component: NumberInput, + title: 'DAO DAO / packages / stateless / components / inputs / NumericInput', + component: NumericInput, decorators: [ReactHookFormDecorator], -} as ComponentMeta<typeof NumberInput> +} as ComponentMeta<typeof NumericInput> -const Template: ComponentStory<typeof NumberInput> = (args) => { - const { register, watch, setValue } = useFormContext() +const Template: ComponentStory<typeof NumericInput> = (args) => { + const { register, setValue, getValues } = useFormContext() return ( - <NumberInput + <NumericInput {...args} + getValues={getValues} register={register} setValue={setValue} - watch={watch} /> ) } diff --git a/packages/stateless/components/inputs/NumericInput.tsx b/packages/stateless/components/inputs/NumericInput.tsx new file mode 100644 index 000000000..0f1e2ddcd --- /dev/null +++ b/packages/stateless/components/inputs/NumericInput.tsx @@ -0,0 +1,246 @@ +import { Add, Remove } from '@mui/icons-material' +import clsx from 'clsx' +import { useRef } from 'react' +import { FieldValues, Path } from 'react-hook-form' +import { useTranslation } from 'react-i18next' + +import { HugeDecimal } from '@dao-dao/math' +import { NumericInputProps } from '@dao-dao/types' +import { toAccessibleImageUrl } from '@dao-dao/utils' + +import { IconButton } from '../icon_buttons' + +/** + * This input is designed for numeric values and takes advantage of the + * HugeDecimal class to handle large decimal numbers gracefully. + * + * By default, it uses human-readable strings for value storage. You can instead + * use a number by setting the `numericValue` prop, but this should only be used + * when not needing to store potentially large numbers. + * + * There is no need to provide `value` when providing `fieldName` and `register` + * via react-hook-form. + * + * To show plus/minus buttons, make sure to provide `setValue` and either + * `fieldName`+`getValues` or `value`. When using a react-hook-form form, + * `setValue` and `getValues` can be retrieved easily from `useForm` or + * `useFormContext`. When not using a react-hook-form form, the `setValue` + * function can easily be mocked, and its first argument (`fieldName`) can be + * ignored. + */ +export const NumericInput = < + FV extends FieldValues, + FieldName extends Path<FV> +>({ + fieldName, + register, + error, + validation, + hidePlusMinus, + numericValue, + value, + getValues, + setValue, + disabled, + sizing, + className, + containerClassName, + required, + ghost, + unit, + unitIconUrl, + textClassName, + unitClassName, + unitIconClassName, + unitContainerClassName, + plusMinusButtonSize = 'sm', + ...props +}: NumericInputProps<FV, FieldName>) => { + const { t } = useTranslation() + const validate = validation?.reduce( + (a, v) => ({ ...a, [v.toString()]: v }), + {} + ) + + const lastValueSet = useRef('') + + return ( + <div + className={clsx( + 'flex flex-row items-center gap-2 bg-transparent transition', + // Padding and outline + !ghost && 'rounded-md py-3 px-4 ring-1 ring-inset focus-within:ring-2', + // Outline color + error + ? 'ring-border-interactive-error' + : 'ring-border-primary focus-within:ring-border-interactive-focus', + // Sizing + { + 'w-28': sizing === 'sm', + 'w-40': sizing === 'md', + 'w-56': sizing === 'lg', + 'w-28 md:w-32 lg:w-40': sizing === 'auto', + 'w-full': sizing === 'fill', + }, + containerClassName + )} + > + {!hidePlusMinus && !disabled && setValue && ( + <div + className={clsx( + 'flex flex-row items-center gap-1', + // Add small gap between buttons when larger buttons are used. + plusMinusButtonSize === 'lg' && 'gap-1' + )} + > + {/* Minus button */} + <IconButton + Icon={Remove} + disabled={disabled} + iconClassName="text-icon-secondary" + onClick={() => + setValue( + fieldName ?? '', + HugeDecimal.min( + HugeDecimal.max( + ...(props.min !== undefined ? [props.min] : []), + // Subtract 1 whole number and truncate. + HugeDecimal.from( + (fieldName && getValues ? getValues(fieldName) : value) || + 0 + ) + .minus(1) + .trunc() + ), + ...(props.max !== undefined ? [props.max] : []) + ).toString(), + { + shouldValidate: true, + } + ) + } + size={ + // The larger button size for this input corresponds to the + // default icon button size. + plusMinusButtonSize === 'lg' ? 'default' : plusMinusButtonSize + } + variant="ghost" + /> + + <IconButton + Icon={Add} + disabled={disabled} + iconClassName="text-icon-secondary" + onClick={() => + setValue( + fieldName ?? '', + HugeDecimal.min( + HugeDecimal.max( + ...(props.min !== undefined ? [props.min] : []), + // Add 1 whole number and truncate. + HugeDecimal.from( + (fieldName && getValues ? getValues(fieldName) : value) || + 0 + ) + .plus(1) + .trunc() + ), + ...(props.max !== undefined ? [props.max] : []) + ).toString(), + { + shouldValidate: true, + } + ) + } + size={ + // The larger button size for this input corresponds to the + // default icon button size. + plusMinusButtonSize === 'lg' ? 'default' : plusMinusButtonSize + } + variant="ghost" + /> + </div> + )} + + <input + className={clsx( + 'ring-none secondary-text text-text-body w-full grow appearance-none border-none bg-transparent text-right outline-none', + className, + textClassName + )} + disabled={disabled} + onInput={ + // If not registering with react-hook-form, manually listen for input + // value change and call `setValue`. + fieldName && register + ? undefined + : setValue && + (({ target }) => { + const value = (target as HTMLInputElement).value + + // If a decimal point is entered, and we already set the same + // value without a decimal point, don't set again. We don't want + // to clear the decimal point in the case that this input is + // controlled and the parent component transforms the value + // manually into a number and back (which would clear the + // decimal point). + if (value === lastValueSet.current + '.') { + return + } + + lastValueSet.current = value + setValue( + fieldName ?? '', + numericValue + ? // Treat empty strings as NaN when manually setting value. + value.trim() === '' + ? NaN + : Number(value) + : value + ) + }) + } + type="number" + value={value} + {...props} + {...(fieldName && + register?.(fieldName, { + required: required && t('info.required'), + validate, + valueAsNumber: numericValue, + }))} + /> + + {(unit || unitIconUrl) && ( + <div + className={clsx( + 'flex flex-row items-center gap-1.5 max-w-[10rem] shrink-0 min-w-0', + unitContainerClassName + )} + > + {unitIconUrl && ( + <div + className={clsx( + 'h-5 w-5 shrink-0 bg-cover bg-center rounded-full ml-1', + unitIconClassName + )} + style={{ + backgroundImage: `url(${toAccessibleImageUrl(unitIconUrl)})`, + }} + /> + )} + + <p + className={clsx( + 'secondary-text text-text-tertiary max-w-[10rem] shrink-0 truncate text-right', + textClassName, + unitClassName + )} + > + {unit} + </p> + </div> + )} + </div> + ) +} diff --git a/packages/stateless/components/inputs/PercentButton.stories.tsx b/packages/stateless/components/inputs/PercentButton.stories.tsx index 1d2536a35..52acd7ccb 100644 --- a/packages/stateless/components/inputs/PercentButton.stories.tsx +++ b/packages/stateless/components/inputs/PercentButton.stories.tsx @@ -1,6 +1,8 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { useState } from 'react' +import { HugeDecimal } from '@dao-dao/math' + import { PercentButton } from './PercentButton' export default { @@ -9,7 +11,7 @@ export default { } as ComponentMeta<typeof PercentButton> const Template: ComponentStory<typeof PercentButton> = (args) => { - const [amount, setAmount] = useState(50) + const [amount, setAmount] = useState(HugeDecimal.fromHumanReadable(50, 6)) return <PercentButton {...args} amount={amount} setAmount={setAmount} /> } @@ -17,7 +19,6 @@ const Template: ComponentStory<typeof PercentButton> = (args) => { export const Default = Template.bind({}) Default.args = { label: '25%', - loadingMax: { loading: false, data: 1234 }, - percent: 0.25, - decimals: 6, + loadingMax: { loading: false, data: HugeDecimal.fromHumanReadable(1234, 6) }, + percent: 25, } diff --git a/packages/stateless/components/inputs/PercentButton.tsx b/packages/stateless/components/inputs/PercentButton.tsx index 0029f7d75..632d43e2a 100644 --- a/packages/stateless/components/inputs/PercentButton.tsx +++ b/packages/stateless/components/inputs/PercentButton.tsx @@ -1,65 +1,71 @@ import clsx from 'clsx' +import { HugeDecimal } from '@dao-dao/math' import { ButtonProps, LoadingData } from '@dao-dao/types' import { Button } from '../buttons' -export interface PercentButtonProps { - label: string - loadingMax: LoadingData<number> +export type PercentButtonProps = { + loadingMax: LoadingData<HugeDecimal> + /** + * Value out of 100 (e.g. 50 = 50%). + */ percent: number - amount: number - setAmount: (newAmount: number) => void - decimals: number + amount: HugeDecimal + setAmount: (newAmount: HugeDecimal) => void + /** + * Override the default label of `{percent}%`. + */ + label?: string className?: string - absoluteOffset?: number + absoluteOffset?: HugeDecimal size?: ButtonProps['size'] } export const PercentButton = ({ - label, loadingMax, percent, amount, setAmount, - decimals, + label = `${percent}%`, className, absoluteOffset, size, -}: PercentButtonProps) => ( - <Button - center - className={clsx('w-full', className)} - disabled={loadingMax.loading} - onClick={() => - !loadingMax.loading && - setAmount( - Math.min( - Math.max( - Number( - (loadingMax.data * percent + (absoluteOffset ?? 0)).toFixed( - decimals - ) - ), - 1 / Math.pow(10, decimals) - ), +}: PercentButtonProps) => { + const newAmount = loadingMax.loading + ? HugeDecimal.zero + : // Cap between 1 and max + HugeDecimal.min( + HugeDecimal.max( + HugeDecimal.one, loadingMax.data - ) + .times(percent) + .div(100) + .plus(absoluteOffset || 0) + .toFixed(0) + ), + loadingMax.data ) - } - pressed={ - // Only show as pressed if percent and amount are both zero or nonzero. If - // one is zero and the other is nonzero, the button should not be pressed. - // This ensures that the button doesn't show as pressed when the max is 0, - // since all percents of 0 are 0. - (percent === 0) === (amount === 0) && - !loadingMax.loading && - (loadingMax.data * percent + (absoluteOffset ?? 0)).toFixed(decimals) === - amount.toFixed(decimals) - } - size={size} - variant="secondary" - > - {label} - </Button> -) + + return ( + <Button + center + className={clsx('w-full', className)} + disabled={loadingMax.loading} + onClick={() => !loadingMax.loading && setAmount(newAmount)} + pressed={ + // Only show as pressed if percent and amount are both zero or nonzero. + // If one is zero and the other is nonzero, the button should not be + // pressed. This ensures that the button doesn't show as pressed when + // the max is 0, since all percents of 0 are 0. + (percent === 0) === amount.isZero() && + !loadingMax.loading && + newAmount.eq(amount) + } + size={size} + variant="secondary" + > + {label} + </Button> + ) +} diff --git a/packages/stateless/components/inputs/TokenInput.stories.tsx b/packages/stateless/components/inputs/TokenInput.stories.tsx index a967226f6..4db88d379 100644 --- a/packages/stateless/components/inputs/TokenInput.stories.tsx +++ b/packages/stateless/components/inputs/TokenInput.stories.tsx @@ -18,7 +18,7 @@ export default { } as ComponentMeta<typeof TokenInput> const Template: ComponentStory<typeof TokenInput> = (args) => { - const { register, watch, setValue } = useFormContext() + const { register, watch, setValue, getValues } = useFormContext() return ( <div className="max-w-sm"> @@ -27,6 +27,7 @@ const Template: ComponentStory<typeof TokenInput> = (args) => { amount={{ watch, setValue, + getValues, register, fieldName: 'amount', min: 0.00001, diff --git a/packages/stateless/components/inputs/TokenInput.tsx b/packages/stateless/components/inputs/TokenInput.tsx index 7478e5d15..dfeb17bb0 100644 --- a/packages/stateless/components/inputs/TokenInput.tsx +++ b/packages/stateless/components/inputs/TokenInput.tsx @@ -4,13 +4,13 @@ import { useCallback, useMemo, useState } from 'react' import { FieldValues, Path } from 'react-hook-form' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { PopupTriggerCustomComponent, TokenInputOption, TokenInputProps, } from '@dao-dao/types' import { - convertMicroDenomToDenomWithDecimals, getDisplayNameForChainId, getFallbackImage, toAccessibleImageUrl, @@ -26,7 +26,7 @@ import { ChainLogo } from '../chain/ChainLogo' import { IconButton } from '../icon_buttons' import { FilterableItem, FilterableItemPopup } from '../popup' import { Tooltip } from '../tooltip' -import { NumberInput } from './NumberInput' +import { NumericInput } from './NumericInput' import { TextInput } from './TextInput' /** @@ -61,14 +61,10 @@ export const TokenInput = < ? undefined : tokens.data.find((token) => tokensEqual(token, _selectedToken)) - const amount = amountField - ? amountField.convertMicroDenom - ? convertMicroDenomToDenomWithDecimals( - amountField.watch(amountField.fieldName), - selectedToken?.decimals ?? 0 - ) - : Number(amountField.watch(amountField.fieldName)) - : 0 + const amount = HugeDecimal.fromHumanReadable( + amountField?.watch(amountField.fieldName) || '0', + selectedToken?.decimals ?? 0 + ) // All tokens from same chain. const allTokensOnSameChain = @@ -110,9 +106,8 @@ export const TokenInput = < <p className="min-w-[4rem] grow truncate text-left"> {readOnly && amountField && - amount.toLocaleString(undefined, { - // Show as many decimals as possible (max is 20). - maximumFractionDigits: 20, + amount.toInternationalizedHumanReadableString({ + decimals: selectedToken.decimals, }) + (amountField.unit ? amountField.unit : '') + ' $'} @@ -123,7 +118,10 @@ export const TokenInput = < tokenFallback ?? ( <p className="text-text-secondary"> {readOnly - ? t('info.token', { count: amount }) + ? t('info.token', { + // Plural if amount is not 1. + count: amount.eq(1) ? 1 : 2, + }) : disabled ? t('info.noTokenSelected') : t('button.selectToken')} @@ -249,20 +247,17 @@ export const TokenInput = < ) : ( <> {amountField && ( - <NumberInput + <NumericInput {...amountField} containerClassName="min-w-[12rem] grow basis-[12rem]" disabled={disabled || (!selectedToken && !customSelected)} setValue={(fieldName, value, options) => amountField.setValue(fieldName, value as any, options) } - transformDecimals={ - amountField.convertMicroDenom - ? selectedToken?.decimals - : undefined - } validation={[ - amountField.min ? validatePositive : validateNonNegative, + HugeDecimal.from(amountField.min || 0).isZero() + ? validateNonNegative + : validatePositive, ...(required ? [validateRequired] : []), ...(amountField.validations ?? []), ]} diff --git a/packages/stateless/components/inputs/index.ts b/packages/stateless/components/inputs/index.ts index 8e219dede..c7519f1b3 100644 --- a/packages/stateless/components/inputs/index.ts +++ b/packages/stateless/components/inputs/index.ts @@ -13,7 +13,7 @@ export * from './ImageUploadInput' export * from './InputErrorMessage' export * from './InputLabel' export * from './InputThemedText' -export * from './NumberInput' +export * from './NumericInput' export * from './PercentButton' export * from './RadioInput' export * from './RangeInput' diff --git a/packages/stateless/components/layout/Breadcrumbs.tsx b/packages/stateless/components/layout/Breadcrumbs.tsx index 49ff985f5..be07aef2b 100644 --- a/packages/stateless/components/layout/Breadcrumbs.tsx +++ b/packages/stateless/components/layout/Breadcrumbs.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { BreadcrumbsProps, DaoPageMode } from '@dao-dao/types' -import { useDaoInfoContextIfAvailable } from '../../contexts' +import { useDaoIfAvailable } from '../../contexts' import { useDaoNavHelpers } from '../../hooks' import { Button } from '../buttons/Button' import { IconButton } from '../icon_buttons/IconButton' @@ -18,37 +18,38 @@ export const Breadcrumbs = ({ override = false, homeTab, current, - daoInfo: _daoInfo, + dao: _dao, className, }: BreadcrumbsProps) => { const { t } = useTranslation() - // Allow using Breadcrumbs outside of DaoPageWrapper. - const daoInfo = useDaoInfoContextIfAvailable() || _daoInfo const { mode } = useAppContext() const { getDaoPath } = useDaoNavHelpers() + // Allow using Breadcrumbs outside of DaoPageWrapper. + const dao = useDaoIfAvailable() || _dao + const [responsive, setResponsive] = useState(false) const crumbs = mode === DaoPageMode.Dapp - ? home || !daoInfo + ? home || !dao ? [{ href: '/', label: t('title.home') }] : [ { href: // Link to home tab if available. - getDaoPath(daoInfo.coreAddress, homeTab?.id), - label: daoInfo.name, + getDaoPath(dao.coreAddress, homeTab?.id), + label: dao.name, }, ] : // SDA - home || !daoInfo + home || !dao ? [] : [ { href: // Link to home tab if available. - getDaoPath(daoInfo.coreAddress, homeTab?.id), + getDaoPath(dao.coreAddress, homeTab?.id), label: homeTab?.sdaLabel || t('title.home'), }, ] diff --git a/packages/stateless/components/layout/SdaNavigation.tsx b/packages/stateless/components/layout/SdaNavigation.tsx index 9b1d6b4e5..1a2393f0f 100644 --- a/packages/stateless/components/layout/SdaNavigation.tsx +++ b/packages/stateless/components/layout/SdaNavigation.tsx @@ -13,7 +13,7 @@ import { DaoPageMode, DaoTabId } from '@dao-dao/types' import { SdaNavigationProps } from '@dao-dao/types/components/SdaNavigation' import { MAINNET, getDaoPath as baseGetDaoPath } from '@dao-dao/utils' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { useDaoNavHelpers } from '../../hooks' import { DaoImage } from '../dao/DaoImage' import { IconButton, ThemeToggle } from '../icon_buttons' @@ -53,7 +53,7 @@ export const SdaNavigation = ({ LinkWrapper, SidebarWallet, }: SdaNavigationProps) => { - const daoInfo = useDaoInfoContext() + const daoInfo = useDao() const { t } = useTranslation() const { getDaoPath, diff --git a/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx b/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx index ba2cbbe1b..7879895b3 100644 --- a/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx +++ b/packages/stateless/components/modals/DiscordNotifierConfigureModal.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import { DiscordNotifierRegistration, ModalProps } from '@dao-dao/types' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { Button, ButtonLink } from '../buttons' import { CopyToClipboard } from '../CopyToClipboard' import { IconButton, IconButtonLink } from '../icon_buttons' @@ -53,7 +53,7 @@ export const DiscordNotifierConfigureModal = ({ ...props }: DiscordNotifierConfigureModalProps) => { const { t } = useTranslation() - const { name: daoName } = useDaoInfoContext() + const { name: daoName } = useDao() const [registering, setRegistering] = useState(false) @@ -77,7 +77,7 @@ export const DiscordNotifierConfigureModal = ({ {...props} contentContainerClassName="gap-4" header={{ - title: t('title.discordNotifier', { daoName: daoName }), + title: t('title.discordNotifier', { daoName }), subtitle: t('info.discordNotifierSubtitle'), }} onClose={() => { diff --git a/packages/stateless/components/modals/TokenDepositModal.stories.tsx b/packages/stateless/components/modals/TokenDepositModal.stories.tsx index 0c3b0093c..9971b14b5 100644 --- a/packages/stateless/components/modals/TokenDepositModal.stories.tsx +++ b/packages/stateless/components/modals/TokenDepositModal.stories.tsx @@ -1,6 +1,8 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { useState } from 'react' +import { HugeDecimal } from '@dao-dao/math' + import { token } from '../token/TokenCard.stories' import { TokenDepositModal } from './TokenDepositModal' @@ -11,7 +13,7 @@ export default { } as ComponentMeta<typeof TokenDepositModal> const Template: ComponentStory<typeof TokenDepositModal> = (args) => { - const [amount, setAmount] = useState(1) + const [amount, setAmount] = useState(HugeDecimal.fromHumanReadable(1, 6)) return <TokenDepositModal {...args} amount={amount} setAmount={setAmount} /> } diff --git a/packages/stateless/components/modals/TokenDepositModal.tsx b/packages/stateless/components/modals/TokenDepositModal.tsx index 88f3416a5..c490d0bea 100644 --- a/packages/stateless/components/modals/TokenDepositModal.tsx +++ b/packages/stateless/components/modals/TokenDepositModal.tsx @@ -2,29 +2,27 @@ import { WarningRounded } from '@mui/icons-material' import { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { AmountWithTimestamp, GenericToken, LoadingData, ModalProps, } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - shortenTokenSymbol, -} from '@dao-dao/utils' +import { shortenTokenSymbol } from '@dao-dao/utils' import { Button } from '../buttons/Button' -import { NumberInput, PercentButton } from '../inputs' +import { NumericInput, PercentButton } from '../inputs' import { TokenAmountDisplay } from '../token/TokenAmountDisplay' import { Modal } from './Modal' export type TokenDepositModalProps = Pick<ModalProps, 'visible' | 'onClose'> & { token: GenericToken loadingBalance: LoadingData<AmountWithTimestamp> - onDeposit: (amount: number) => void | Promise<void> + onDeposit: (amount: HugeDecimal) => void | Promise<void> loading: boolean - amount: number - setAmount: Dispatch<SetStateAction<number>> + amount: HugeDecimal + setAmount: Dispatch<SetStateAction<HugeDecimal>> connected: boolean ConnectWallet?: ComponentType subtitle?: string | ReactNode @@ -48,7 +46,7 @@ export const TokenDepositModal = ({ }: TokenDepositModalProps) => { const { t } = useTranslation() - const min = convertMicroDenomToDenomWithDecimals(1, token.decimals) + const min = HugeDecimal.one.toHumanReadableNumber(token.decimals) const { tokenSymbol } = shortenTokenSymbol(token.symbol) @@ -70,7 +68,7 @@ export const TokenDepositModal = ({ disabled } loading={loading} - onClick={() => amount > 0 && onDeposit(amount)} + onClick={() => amount.isPositive() && onDeposit(amount)} > {t('button.deposit')} </Button> @@ -101,7 +99,7 @@ export const TokenDepositModal = ({ amount={ loadingBalance.loading ? loadingBalance - : { loading: false, data: loadingBalance.data.amount } + : loadingBalance.data.amount } dateFetched={ loadingBalance.loading @@ -116,31 +114,24 @@ export const TokenDepositModal = ({ </div> )} - <NumberInput + <NumericInput // Auto focus does not work on mobile Safari by design // (https://bugs.webkit.org/show_bug.cgi?id=195884#c4). autoFocus={modalProps.visible} max={loadingBalance.loading ? undefined : loadingBalance.data.amount} min={min} - onInput={(event) => - setAmount( - Number( - Number((event.target as HTMLInputElement).value).toFixed( - token.decimals - ) - ) - ) - } onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() onDeposit(amount) } }} - setValue={(_, value) => setAmount(value)} + setValue={(_, value) => + setAmount(HugeDecimal.fromHumanReadable(value, token.decimals)) + } step={min} unit={'$' + tokenSymbol} - value={amount} + value={amount.toHumanReadableString(token.decimals)} /> <div className="grid grid-cols-5 gap-2"> @@ -148,14 +139,18 @@ export const TokenDepositModal = ({ <PercentButton key={percent} amount={amount} - decimals={token.decimals} - label={`${percent}%`} loadingMax={ loadingBalance.loading ? loadingBalance - : { loading: false, data: loadingBalance.data.amount } + : { + loading: false, + data: HugeDecimal.fromHumanReadable( + loadingBalance.data.amount, + token.decimals + ), + } } - percent={percent / 100} + percent={percent} setAmount={setAmount} /> ))} diff --git a/packages/stateless/components/nft/NftCard.stories.tsx b/packages/stateless/components/nft/NftCard.stories.tsx index 3e65ac16a..817dbf61a 100644 --- a/packages/stateless/components/nft/NftCard.stories.tsx +++ b/packages/stateless/components/nft/NftCard.stories.tsx @@ -1,6 +1,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { useState } from 'react' +import { HugeDecimal } from '@dao-dao/math' import { EntityDisplay } from '@dao-dao/stateful' import { LazyNftCardInfo } from '@dao-dao/types' import { getNftKey } from '@dao-dao/utils' @@ -50,7 +51,7 @@ export const makeProps = (): NftCardProps => { description: `Description of NFT #${id}`, highestOffer: { // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), }, externalLink: { href: '/dog_nft.png', diff --git a/packages/stateless/components/nft/NftCard.tsx b/packages/stateless/components/nft/NftCard.tsx index fd6440ca9..e7ce6a654 100644 --- a/packages/stateless/components/nft/NftCard.tsx +++ b/packages/stateless/components/nft/NftCard.tsx @@ -19,7 +19,6 @@ import { } from '@dao-dao/types' import { NFT_VIDEO_EXTENSIONS, - convertMicroDenomToDenomWithDecimals, getImageUrlForChainId, getNftName, objectMatchesStructure, @@ -272,14 +271,11 @@ export const NftCard = forwardRef<HTMLDivElement, NftCardProps>( <div className="space-y-2"> <p className="secondary-text">{t('title.highestOffer')}</p> <div className="body-text space-y-1 font-mono"> - {typeof highestOffer.amount === 'number' && - !isNaN(highestOffer.amount) && + {highestOffer.amount && + highestOffer.amount.isPositive() && highestOffer.offerToken && ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - highestOffer.amount, - highestOffer.offerToken.decimals - )} + amount={highestOffer.amount} decimals={highestOffer.offerToken.decimals} iconUrl={highestOffer.offerToken.imageUrl} symbol={highestOffer.offerToken.symbol} diff --git a/packages/stateless/components/not_found/ProposalNotFound.tsx b/packages/stateless/components/not_found/ProposalNotFound.tsx index 6dfb06b23..aea8ead60 100644 --- a/packages/stateless/components/not_found/ProposalNotFound.tsx +++ b/packages/stateless/components/not_found/ProposalNotFound.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { DaoTabId, PageHeaderProps } from '@dao-dao/types' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { useDaoNavHelpers } from '../../hooks' import { ButtonLink } from '../buttons' import { ErrorPage } from '../error/ErrorPage' @@ -16,7 +16,7 @@ export const ProposalNotFound = ({ PageHeaderContent, }: ProposalNotFoundProps) => { const { t } = useTranslation() - const { coreAddress } = useDaoInfoContext() + const { coreAddress } = useDao() const { getDaoPath } = useDaoNavHelpers() return ( diff --git a/packages/stateless/components/proposal/ProposalModuleSelector.stories.tsx b/packages/stateless/components/proposal/ProposalModuleSelector.stories.tsx index 46e7d42d1..f3ec89d03 100644 --- a/packages/stateless/components/proposal/ProposalModuleSelector.stories.tsx +++ b/packages/stateless/components/proposal/ProposalModuleSelector.stories.tsx @@ -5,7 +5,7 @@ import { matchAdapter } from '@dao-dao/stateful/proposal-module-adapter' import { DaoPageWrapperDecorator } from '@dao-dao/storybook/decorators' import { DaoProposalSingleAdapterId } from '@dao-dao/utils' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { ProposalModuleSelector } from './ProposalModuleSelector' export default { @@ -16,11 +16,11 @@ export default { } as ComponentMeta<typeof ProposalModuleSelector> const Template: ComponentStory<typeof ProposalModuleSelector> = (args) => { - const daoInfo = useDaoInfoContext() + const dao = useDao() const [selectedProposalModule, setSelectedProposalModule] = useState( // Default to single choice proposal module. - daoInfo.proposalModules.find( + dao.proposalModules.find( ({ contractName }) => matchAdapter(contractName)?.id === DaoProposalSingleAdapterId )! diff --git a/packages/stateless/components/proposal/ProposalModuleSelector.tsx b/packages/stateless/components/proposal/ProposalModuleSelector.tsx index 76d0133e5..72149e7cc 100644 --- a/packages/stateless/components/proposal/ProposalModuleSelector.tsx +++ b/packages/stateless/components/proposal/ProposalModuleSelector.tsx @@ -3,14 +3,14 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { + IProposalModuleBase, PreProposeModuleType, ProposalModuleAdapter, - ProposalModuleInfo, TypedOption, } from '@dao-dao/types' import { ContractName } from '@dao-dao/utils' -import { useDaoInfoContext } from '../../contexts' +import { useDao } from '../../contexts' import { SegmentedControls } from '../inputs/SegmentedControls' export type ProposalModuleSelectorProps = { @@ -18,7 +18,7 @@ export type ProposalModuleSelectorProps = { * Address of the selected proposal module. */ selected: string - setSelected: (proposalModule: ProposalModuleInfo) => void + setSelected: (proposalModule: IProposalModuleBase) => void matchAdapter: ( contractNameToMatch: string ) => ProposalModuleAdapter | undefined @@ -35,7 +35,7 @@ export const ProposalModuleSelector = ({ className, }: ProposalModuleSelectorProps) => { const { t } = useTranslation() - const { proposalModules } = useDaoInfoContext() + const { proposalModules } = useDao() // List of proposal modules available, using the adapter ID to derive a label // to display in the selector. @@ -48,7 +48,7 @@ export const ProposalModuleSelector = ({ ({ prePropose }) => prePropose?.type !== PreProposeModuleType.NeutronOverruleSingle ) - .map((proposalModule): TypedOption<ProposalModuleInfo> | undefined => { + .map((proposalModule): TypedOption<IProposalModuleBase> | undefined => { const adapter = matchAdapter(proposalModule.contractName) return ( @@ -58,7 +58,7 @@ export const ProposalModuleSelector = ({ } ) }) - .filter((item): item is TypedOption<ProposalModuleInfo> => !!item) + .filter((item): item is TypedOption<IProposalModuleBase> => !!item) // Ignore proposals with an approver pre-propose since those are // automatically managed by a pre-propose-approval contract in another // DAO. diff --git a/packages/stateless/components/proposal/ProposalVetoConfigurer.tsx b/packages/stateless/components/proposal/ProposalVetoConfigurer.tsx index 865649231..8c30df394 100644 --- a/packages/stateless/components/proposal/ProposalVetoConfigurer.tsx +++ b/packages/stateless/components/proposal/ProposalVetoConfigurer.tsx @@ -24,7 +24,7 @@ import { FormSwitchCard, InputErrorMessage, InputLabel, - NumberInput, + NumericInput, SelectInput, } from '../inputs' @@ -52,7 +52,7 @@ export const ProposalVetoConfigurer = ({ const { t } = useTranslation() const { bech32_prefix: bech32Prefix } = useChain() - const { control, register, setValue, watch } = + const { control, register, setValue, getValues } = useFormContext<ProposalVetoConfig>() const { @@ -173,7 +173,7 @@ export const ProposalVetoConfigurer = ({ /> <div className="flex flex-row gap-2"> - <NumberInput + <NumericInput containerClassName="grow" disabled={disabled} error={errors?.timelockDuration?.value} @@ -181,13 +181,14 @@ export const ProposalVetoConfigurer = ({ (fieldNamePrefix + 'timelockDuration.value') as 'timelockDuration.value' } + getValues={getValues} min={0} + numericValue register={register} setValue={setValue} sizing="sm" step={1} - validation={[validateNonNegative, validateRequired]} - watch={watch} + validation={[validateRequired, validateNonNegative]} /> <SelectInput diff --git a/packages/stateless/components/token/StakingModal.stories.tsx b/packages/stateless/components/token/StakingModal.stories.tsx index 3386adfb1..7450cec84 100644 --- a/packages/stateless/components/token/StakingModal.stories.tsx +++ b/packages/stateless/components/token/StakingModal.stories.tsx @@ -1,10 +1,12 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' import { useState } from 'react' +import { HugeDecimal } from '@dao-dao/math' import { CHAIN_ID } from '@dao-dao/storybook' +import { StakingMode } from '@dao-dao/types' import { getNativeTokenForChainId } from '@dao-dao/utils' -import { StakingModal, StakingMode } from './StakingModal' +import { StakingModal } from './StakingModal' export default { title: 'DAO DAO / packages / stateless / components / token / StakingModal', @@ -12,20 +14,26 @@ export default { } as ComponentMeta<typeof StakingModal> const Template: ComponentStory<typeof StakingModal> = (args) => { - const [amount, setAmount] = useState(50) + const [amount, setAmount] = useState(HugeDecimal.fromHumanReadable(50, 6)) return <StakingModal {...args} amount={amount} setAmount={setAmount} /> } export const StakeUnstake = Template.bind({}) StakeUnstake.args = { - claimableTokens: 20, + claimableTokens: HugeDecimal.fromHumanReadable(20, 6), loading: false, initialMode: StakingMode.Stake, - proposalDeposit: 5, - loadingStakableTokens: { loading: false, data: 23456 }, + proposalDeposit: HugeDecimal.fromHumanReadable(5, 6), + loadingStakableTokens: { + loading: false, + data: HugeDecimal.fromHumanReadable(23456, 6), + }, token: getNativeTokenForChainId(CHAIN_ID), - loadingUnstakableTokens: { loading: false, data: 65432 }, + loadingUnstakableTokens: { + loading: false, + data: HugeDecimal.fromHumanReadable(65432, 6), + }, unstakingDuration: { time: 86400, }, @@ -33,13 +41,19 @@ StakeUnstake.args = { export const Claim = Template.bind({}) Claim.args = { - claimableTokens: 20, + claimableTokens: HugeDecimal.fromHumanReadable(20, 6), loading: false, initialMode: StakingMode.Claim, - proposalDeposit: 5, - loadingStakableTokens: { loading: false, data: 23456 }, + proposalDeposit: HugeDecimal.fromHumanReadable(5, 6), + loadingStakableTokens: { + loading: false, + data: HugeDecimal.fromHumanReadable(23456, 6), + }, token: getNativeTokenForChainId(CHAIN_ID), - loadingUnstakableTokens: { loading: false, data: 65432 }, + loadingUnstakableTokens: { + loading: false, + data: HugeDecimal.fromHumanReadable(65432, 6), + }, unstakingDuration: { time: 86400, }, diff --git a/packages/stateless/components/token/StakingModal.tsx b/packages/stateless/components/token/StakingModal.tsx index 4c7115f51..b59fc5eb2 100644 --- a/packages/stateless/components/token/StakingModal.tsx +++ b/packages/stateless/components/token/StakingModal.tsx @@ -1,6 +1,7 @@ -import { ChangeEvent, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { LoadingData } from '@dao-dao/types' import { StakingModalProps, @@ -12,7 +13,7 @@ import { convertDurationToHumanReadableString } from '@dao-dao/utils' import { Button } from '../buttons/Button' import { InputLabel, - NumberInput, + NumericInput, PercentButton, SegmentedControls, TokenInput, @@ -22,22 +23,16 @@ import { Tooltip } from '../tooltip/Tooltip' import { ValidatorPicker } from '../ValidatorPicker' import { TokenAmountDisplay } from './TokenAmountDisplay' -export * from '@dao-dao/types/components/StakingModal' - export const StakingModal = ({ initialMode, amount, setAmount, onClose, - // microdenom claimableTokens, - // macrodenom loadingStakableTokens, - // macrodenom loadingUnstakableTokens, unstakingDuration, token, - // macrodenom proposalDeposit, loading, error, @@ -65,14 +60,14 @@ export const StakingModal = ({ data: validatorPicker.stakes?.find( (stake) => stake.validator.address === targetValidator - )?.amount ?? 0, + )?.amount ?? HugeDecimal.zero, } } // If not choosing a validator and no unstakable amount passed, assume 0. else if (!loadingUnstakableTokens) { loadingUnstakableTokens = { loading: false, - data: 0, + data: HugeDecimal.zero, } } @@ -88,15 +83,17 @@ export const StakingModal = ({ const invalidAmount = (): string | undefined => { if (mode === StakingMode.Claim) { - return claimableTokens > 0 ? undefined : t('error.cannotTxZeroTokens') + return claimableTokens.isPositive() + ? undefined + : t('error.cannotTxZeroTokens') } - if (amount <= 0) { + if (!amount.isPositive()) { return t('error.cannotTxZeroTokens') } if (maxTx === undefined) { return t('error.loadingData') } - if (amount > maxTx) { + if (amount.gt(maxTx)) { return t('error.cannotStakeMoreThanYouHave') } } @@ -174,7 +171,8 @@ export const StakingModal = ({ validatorPicker.validators.filter((v) => validatorPicker.stakes?.some( (s) => - s.validator.address === v.address && s.amount > 0 + s.validator.address === v.address && + s.amount.isPositive() ) ) } @@ -200,7 +198,7 @@ export const StakingModal = ({ validatorPicker.stakes?.some( (s) => s.validator.address === v.address && - s.amount > 0 + s.amount.isPositive() ) ) } @@ -233,7 +231,7 @@ export const StakingModal = ({ } mode={mode} proposalDeposit={proposalDeposit} - setAmount={(amount: number) => setAmount(amount)} + setAmount={setAmount} tokenDecimals={token.decimals} tokenSymbol={token.symbol} unstakingDuration={unstakingDuration} @@ -244,14 +242,14 @@ export const StakingModal = ({ } interface StakeUnstakeModesBodyProps { - amount: number + amount: HugeDecimal mode: StakingMode - loadingMax: LoadingData<number> - setAmount: (newAmount: number) => void + loadingMax: LoadingData<HugeDecimal> + setAmount: (newAmount: HugeDecimal) => void tokenSymbol: string tokenDecimals: number unstakingDuration: Duration | null - proposalDeposit?: number + proposalDeposit?: HugeDecimal } const StakeUnstakeModesBody = ({ @@ -269,23 +267,26 @@ const StakeUnstakeModesBody = ({ return ( <> <h2 className="primary-text mb-6">{t('title.chooseTokenAmount')}</h2> - <NumberInput + <NumericInput containerClassName="py-7 w-full h-20 pl-6 pr-8 bg-background-secondary rounded-md gap-4" ghost - max={loadingMax.loading ? undefined : loadingMax.data} - min={1 / 10 ** tokenDecimals} - onChange={(e: ChangeEvent<HTMLInputElement>) => - setAmount(e.target.valueAsNumber) + max={ + loadingMax.loading + ? undefined + : loadingMax.data.toHumanReadableNumber(tokenDecimals) } + min={HugeDecimal.one.toHumanReadableNumber(tokenDecimals)} plusMinusButtonSize="lg" - setValue={(_, value) => setAmount(value)} - step={1 / 10 ** tokenDecimals} + setValue={(_, value) => + setAmount(HugeDecimal.fromHumanReadable(value, tokenDecimals)) + } + step={HugeDecimal.one.toHumanReadableNumber(tokenDecimals)} textClassName="font-mono leading-5 symbol-small-body-text" - unit={`$${tokenSymbol}`} - value={amount} + unit={'$' + tokenSymbol} + value={amount.toHumanReadableString(tokenDecimals)} /> - {!loadingMax.loading && amount > loadingMax.data && ( - <span className="caption-text mt-1 ml-1 text-text-interactive-error"> + {!loadingMax.loading && loadingMax.data.lt(amount) && ( + <span className="caption-text text-text-interactive-error mt-1 ml-1"> {t('error.cannotStakeMoreThanYouHave')} </span> )} @@ -303,10 +304,8 @@ const StakeUnstakeModesBody = ({ <PercentButton key={percent} amount={amount} - decimals={tokenDecimals} - label={`${percent}%`} loadingMax={loadingMax} - percent={percent / 100} + percent={percent} setAmount={setAmount} /> ))} @@ -314,20 +313,20 @@ const StakeUnstakeModesBody = ({ {mode === StakingMode.Stake && !!proposalDeposit && !loadingMax.loading && - loadingMax.data > proposalDeposit && ( + loadingMax.data.gt(proposalDeposit) && ( <PercentButton - absoluteOffset={-proposalDeposit} + absoluteOffset={proposalDeposit.negated()} amount={amount} className="mt-2" - decimals={tokenDecimals} label={t('button.stakeAllButProposalDeposit', { - proposalDeposit: proposalDeposit.toLocaleString(undefined, { - maximumFractionDigits: tokenDecimals, - }), + proposalDeposit: + proposalDeposit.toInternationalizedHumanReadableString({ + decimals: tokenDecimals, + }), tokenSymbol, })} loadingMax={loadingMax} - percent={1} + percent={100} setAmount={setAmount} /> )} @@ -340,7 +339,7 @@ const StakeUnstakeModesBody = ({ ('height' in unstakingDuration ? unstakingDuration.height : unstakingDuration.time) > 0 && ( - <div className="mt-7 space-y-5 border-t border-border-secondary pt-7"> + <div className="border-border-secondary mt-7 space-y-5 border-t pt-7"> <p className="primary-text text-text-secondary"> {t('title.unstakingPeriod') + `: ${convertDurationToHumanReadableString( @@ -363,7 +362,7 @@ const StakeUnstakeModesBody = ({ } interface ClaimModeBodyProps { - amount: number + amount: HugeDecimal tokenDecimals: number tokenSymbol: string } diff --git a/packages/stateless/components/token/TokenAmountDisplay.tsx b/packages/stateless/components/token/TokenAmountDisplay.tsx index 3cfd4ec6e..c87cfb93f 100644 --- a/packages/stateless/components/token/TokenAmountDisplay.tsx +++ b/packages/stateless/components/token/TokenAmountDisplay.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { TokenAmountDisplayProps } from '@dao-dao/types' import { formatTime, getDisplayNameForChainId, toAccessibleImageUrl, - toFixedDown, } from '@dao-dao/utils' import { ChainLogo } from '../chain/ChainLogo' @@ -28,11 +28,6 @@ import { Tooltip } from '../tooltip/Tooltip' // The only token amounts we intentionally don't show with full decimals are USD // value estimates (i.e. USDC) (max 2). -// Default maximum decimals to use in a USD estimate. -const USD_ESTIMATE_DEFAULT_MAX_DECIMALS = 2 -// Maximum decimals to use in a large compacted value. -const LARGE_COMPACT_MAX_DECIMALS = 2 - export const TokenAmountDisplay = ({ amount: _amount, decimals: _decimals = 0, @@ -40,12 +35,10 @@ export const TokenAmountDisplay = ({ prefixClassName, suffix, suffixClassName, - minDecimals, - maxDecimals, - hideApprox, dateFetched, minAmount: _minAmount, - showFullAmount, + showFullAmount = false, + showAllDecimals, iconUrl, iconClassName, showChainId, @@ -61,11 +54,11 @@ export const TokenAmountDisplay = ({ const tokenTranslation = estimatedUsdValue ? 'format.estUsdValue' : 'format.token' - const decimals = estimatedUsdValue - ? USD_ESTIMATE_DEFAULT_MAX_DECIMALS - : _decimals + const decimals = estimatedUsdValue ? 2 : _decimals const minAmount = estimatedUsdValue ? 0.01 : _minAmount + showAllDecimals ||= estimatedUsdValue + const translateOrOmitSymbol = (translationKey: string, amount: string) => hideSymbol ? amount @@ -78,6 +71,7 @@ export const TokenAmountDisplay = ({ if ( typeof _amount !== 'number' && _amount && + !(_amount instanceof HugeDecimal) && 'loading' in _amount && _amount.loading ) { @@ -88,100 +82,41 @@ export const TokenAmountDisplay = ({ ) } - // Extract amount from loaded value. + // Extract amount from loaded value and convert to HugeDecimal. let amount = - typeof _amount === 'number' ? _amount : _amount ? _amount.data : 0 - - // If amount too small, set to min and add `< ` to prefix. - const amountBelowMin = !!minAmount && amount < minAmount + _amount instanceof HugeDecimal + ? _amount + : typeof _amount === 'number' + ? HugeDecimal.fromHumanReadable(_amount, decimals) + : _amount + ? _amount.data instanceof HugeDecimal + ? _amount.data + : HugeDecimal.fromHumanReadable(_amount.data, decimals) + : HugeDecimal.zero + + // If amount too small and nonzero, set to min and add `< ` to prefix. + const amountBelowMin = + !!minAmount && amount.isPositive() && amount.lt(minAmount) if (amountBelowMin) { - amount = minAmount + amount = HugeDecimal.fromHumanReadable(minAmount, decimals) prefix = `< ${prefix || ''}` } - const options: Intl.NumberFormatOptions = { - // Always show all decimals if USD estimate. - minimumFractionDigits: estimatedUsdValue - ? USD_ESTIMATE_DEFAULT_MAX_DECIMALS - : minDecimals, - maximumFractionDigits: decimals, - } - - const maxCompactDecimals = - maxDecimals ?? - (estimatedUsdValue ? USD_ESTIMATE_DEFAULT_MAX_DECIMALS : decimals) - const compactOptions: Intl.NumberFormatOptions & { - roundingPriority: string - } = { - ...options, - notation: 'compact', - maximumFractionDigits: maxCompactDecimals, - // notation=compact seems to set maximumSignificantDigits if undefined. - // Because we are rounding toward more precision above, set - // maximumSignificantDigits to 1 so that notation=compact does not override - // it and display extra decimals in case maximumFractionDigits is less. This - // appears to work fine on both Chrome and Safari, which is good enough for - // now. This is a crazy hack. - maximumSignificantDigits: 1, - // Safari (and potentially other non-Chrome browsers) uses only 1 decimal - // when notation=compact. roundingPriority=morePrecision tells the formatter - // to resolve decimal contraint conflicts with the result with greater - // precision. - roundingPriority: 'morePrecision', - } - - const full = toFixedDown(amount, decimals).toLocaleString(undefined, options) + const minDecimals = showAllDecimals ? decimals : 0 - // Abbreviated number. Example: 1,000,000 => 1M, or 1.2345 => 1.23. - let compact = toFixedDown(amount, maxCompactDecimals).toLocaleString( - undefined, - compactOptions - ) + const amountDisplay = amount.toInternationalizedHumanReadableString({ + decimals, + showFullAmount, + minDecimals, + }) - const largeNumber = amount >= 1000 + const display = translateOrOmitSymbol(tokenTranslation, amountDisplay) - // If this is a large number that is compacted, and minDecimals/maxDecimals - // are not being overridden, use fewer decimals because compact notation looks - // bad with too many decimals. We first needed to use the same decimals to - // compare and see if compact had any effect. If compact changed nothing, we - // want to keep the original decimals. - if ( + // Show full value in tooltip if compacted and not an estimated USD value. + const shouldShowFullTooltip = !showFullAmount && - largeNumber && - full !== compact && - minDecimals === undefined && - maxDecimals === undefined - ) { - compact = toFixedDown(amount, LARGE_COMPACT_MAX_DECIMALS).toLocaleString( - undefined, - { - ...compactOptions, - maximumFractionDigits: LARGE_COMPACT_MAX_DECIMALS, - } - ) - } - - const wasCompacted = full !== compact - - // If compact is different from full and not a large number, display - // approximation indication (e.g. ~15.34 when the full value is 15.344913). - // When large, the compact notation (e.g. 1.52K or 23.5M) is enough to - // indicate that there is missing info, and we don't need the explicit - // approximation indication. - const display = - (!showFullAmount && - wasCompacted && - !largeNumber && - !hideApprox && + amount.toHumanReadable(decimals).gte(1000) && !estimatedUsdValue - ? '~' - : '') + - translateOrOmitSymbol(tokenTranslation, showFullAmount ? full : compact) - - // Show full value in tooltip if different from compact and not an estimated - // USD value. - const shouldShowFullTooltip = - !showFullAmount && wasCompacted && !estimatedUsdValue && amount > 0 return ( <Tooltip @@ -190,8 +125,14 @@ export const TokenAmountDisplay = ({ shouldShowFullTooltip || dateFetched ? ( <> {shouldShowFullTooltip && - // eslint-disable-next-line i18next/no-literal-string - translateOrOmitSymbol('format.token', full)} + translateOrOmitSymbol( + // eslint-disable-next-line i18next/no-literal-string + 'format.token', + amount.toInternationalizedHumanReadableString({ + decimals, + showFullAmount: true, + }) + )} {shouldShowFullTooltip && dateFetched && <br />} @@ -208,7 +149,7 @@ export const TokenAmountDisplay = ({ > <div className={clsx( - 'flex min-w-0 flex-row items-center gap-2', + 'flex min-w-0 flex-row items-center gap-2 max-w-full', onClick && 'cursor-pointer transition-opacity hover:opacity-80 active:opacity-70', wrapperClassName @@ -252,7 +193,7 @@ export const TokenAmountDisplay = ({ <p {...props} className={clsx( - 'min-w-0 max-w-full truncate', + 'min-w-0 max-w-full truncate whitespace-normal', onClick && 'underline', props.className )} diff --git a/packages/stateless/components/token/TokenCard.stories.tsx b/packages/stateless/components/token/TokenCard.stories.tsx index 3669926c7..620c6f19d 100644 --- a/packages/stateless/components/token/TokenCard.stories.tsx +++ b/packages/stateless/components/token/TokenCard.stories.tsx @@ -1,5 +1,6 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' +import { HugeDecimal } from '@dao-dao/math' import { EntityDisplay } from '@dao-dao/stateful' import { CHAIN_ID } from '@dao-dao/storybook' import { @@ -43,12 +44,14 @@ export const token: GenericToken = { export const makeProps = (isGovernanceToken = false): TokenCardProps => { // Random price between 0 and 10000 with up to 6 decimals. - const unstakedBalance = Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6 + const unstakedBalance = HugeDecimal.from( + Math.floor(Math.random() * (10000 * 1e6) + 1e6) + ) const stakes: TokenStake[] = [ { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: 'stakefish', moniker: 'Stakefish', @@ -56,14 +59,14 @@ export const makeProps = (isGovernanceToken = false): TokenCardProps => { details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 7, + tokens: HugeDecimal.fromHumanReadable(7, 6), }, - rewards: 1.23, + rewards: HugeDecimal.fromHumanReadable(1.23, 6), }, { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: '2x4ben', moniker: '2x4 Ben', @@ -71,14 +74,14 @@ export const makeProps = (isGovernanceToken = false): TokenCardProps => { details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 7, + tokens: HugeDecimal.fromHumanReadable(7, 6), }, - rewards: 4.56, + rewards: HugeDecimal.fromHumanReadable(4.56, 6), }, { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: 'cosmostation', moniker: 'Cosmostation', @@ -86,14 +89,14 @@ export const makeProps = (isGovernanceToken = false): TokenCardProps => { details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 7, + tokens: HugeDecimal.fromHumanReadable(7, 6), }, - rewards: 7.89, + rewards: HugeDecimal.fromHumanReadable(7.89, 6), }, { token, // Random price between 0 and 10000 with up to 6 decimals. - amount: Math.floor(Math.random() * (10000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (10000 * 1e6) + 1e6)), validator: { address: 'sg1', moniker: 'SG-1', @@ -101,26 +104,31 @@ export const makeProps = (isGovernanceToken = false): TokenCardProps => { details: '', commission: 0.05, status: 'BOND_STATUS_BONDED', - tokens: 7, + tokens: HugeDecimal.fromHumanReadable(7, 6), }, - rewards: 10.11, + rewards: HugeDecimal.fromHumanReadable(10.11, 6), }, ] const unstakingTasks = makeUnstakingModalProps('TOKEN').tasks - const totalStaked = stakes.reduce((acc, stake) => acc + stake.amount, 0) + const totalStaked = stakes.reduce( + (acc, stake) => acc.plus(stake.amount), + HugeDecimal.zero + ) const totalPendingRewards = stakes.reduce( - (acc, stake) => acc + stake.rewards, - 0 + (acc, stake) => acc.plus(stake.rewards), + HugeDecimal.zero ) - const totalUnstaking = - unstakingTasks.reduce( - (acc, task) => - acc + + const totalUnstaking = unstakingTasks.reduce( + (acc, task) => + acc.plus( // Only include balance of unstaking tasks. - (task.status === UnstakingTaskStatus.Unstaking ? task.amount : 0), - 0 - ) ?? 0 + task.status === UnstakingTaskStatus.Unstaking + ? task.amount + : HugeDecimal.zero + ), + HugeDecimal.zero + ) return { owner: { @@ -152,7 +160,7 @@ export const makeProps = (isGovernanceToken = false): TokenCardProps => { totalPendingRewards, totalUnstaking, }, - totalBalance: totalStaked + unstakedBalance + totalUnstaking, + totalBalance: totalStaked.plus(unstakedBalance).plus(totalUnstaking), }, }, onClaim: () => alert('claim'), diff --git a/packages/stateless/components/token/TokenCard.tsx b/packages/stateless/components/token/TokenCard.tsx index 5e8628a75..f8f36ac77 100644 --- a/packages/stateless/components/token/TokenCard.tsx +++ b/packages/stateless/components/token/TokenCard.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { ButtonPopupSection, TokenCardProps, TokenType } from '@dao-dao/types' import { getFallbackImage, @@ -18,7 +19,7 @@ import { toAccessibleImageUrl, } from '@dao-dao/utils' -import { useDaoInfoContextIfAvailable } from '../../contexts' +import { useDaoIfAvailable } from '../../contexts' import { useAddToken } from '../../hooks' import { Button } from '../buttons/Button' import { CopyToClipboard } from '../CopyToClipboard' @@ -47,7 +48,7 @@ export const TokenCard = ({ // If in a DAO context, don't show the DAOs governed section if the only DAO // this token governs is the current DAO. See the comment where this is used // for more details. - const { coreAddress } = useDaoInfoContextIfAvailable() ?? {} + const { coreAddress } = useDaoIfAvailable() ?? {} const lazyStakes = lazyInfo.loading || !lazyInfo.data.stakingInfo @@ -56,15 +57,15 @@ export const TokenCard = ({ const totalStaked = lazyInfo.loading || !lazyInfo.data.stakingInfo - ? 0 + ? HugeDecimal.zero : lazyInfo.data.stakingInfo.totalStaked const totalPendingRewards = lazyInfo.loading || !lazyInfo.data.stakingInfo - ? 0 + ? HugeDecimal.zero : lazyInfo.data.stakingInfo.totalPendingRewards const totalUnstaking = lazyInfo.loading || !lazyInfo.data.stakingInfo - ? 0 + ? HugeDecimal.zero : lazyInfo.data.stakingInfo.totalUnstaking const [showUnstakingTokens, setShowUnstakingTokens] = useState(false) @@ -223,6 +224,7 @@ export const TokenCard = ({ amount={lazyInfo.data.totalBalance} className="leading-5 text-text-body" decimals={token.decimals} + showFullAmount symbol={tokenSymbol} /> @@ -233,10 +235,10 @@ export const TokenCard = ({ token.decimals > 0 && ( <div className="flex flex-row items-center gap-1"> <TokenAmountDisplay - amount={ - lazyInfo.data.totalBalance * + amount={lazyInfo.data.totalBalance.toUsdValue( + token.decimals, lazyInfo.data.usdUnitPrice.usdPrice - } + )} dateFetched={lazyInfo.data.usdUnitPrice.timestamp} estimatedUsdValue /> @@ -253,7 +255,7 @@ export const TokenCard = ({ {/* Only display `unstakedBalance` if total is loading or if different from total. While loading, the total above will hide. */} {(lazyInfo.loading || - lazyInfo.data.totalBalance !== unstakedBalance) && ( + !lazyInfo.data.totalBalance.eq(unstakedBalance)) && ( <div className="flex flex-row items-start justify-between gap-8"> <p className="link-text">{t('info.availableBalance')}</p> <div className="caption-text flex min-w-0 flex-col items-end gap-1 text-right font-mono"> @@ -262,6 +264,7 @@ export const TokenCard = ({ amount={unstakedBalance} className="leading-5 text-text-body" decimals={token.decimals} + showFullAmount symbol={tokenSymbol} /> @@ -276,8 +279,10 @@ export const TokenCard = ({ lazyInfo.loading || !lazyInfo.data.usdUnitPrice?.usdPrice ? { loading: true } - : unstakedBalance * - lazyInfo.data.usdUnitPrice.usdPrice + : unstakedBalance.toUsdValue( + token.decimals, + lazyInfo.data.usdUnitPrice.usdPrice + ) } dateFetched={ lazyInfo.loading || !lazyInfo.data.usdUnitPrice @@ -309,6 +314,7 @@ export const TokenCard = ({ amount={lazyInfo.loading ? { loading: true } : totalStaked} className="caption-text text-right font-mono text-text-body" decimals={token.decimals} + showFullAmount symbol={tokenSymbol} /> </div> @@ -360,13 +366,13 @@ export const TokenCard = ({ <Button className={clsx( 'caption-text text-right font-mono underline-offset-2', - totalUnstaking > 0 && 'text-text-body', + totalUnstaking.isPositive() && 'text-text-body', lazyInfo.loading && 'animate-pulse !text-text-body' )} disabled={lazyInfo.loading} onClick={() => setShowUnstakingTokens(true)} variant={ - lazyInfo.loading || totalUnstaking === 0 + lazyInfo.loading || totalUnstaking.isZero() ? 'none' : 'underline' } @@ -374,6 +380,7 @@ export const TokenCard = ({ <TokenAmountDisplay amount={lazyInfo.loading ? { loading: true } : totalUnstaking} decimals={token.decimals} + showFullAmount symbol={tokenSymbol} /> </Button> @@ -388,6 +395,7 @@ export const TokenCard = ({ } className="caption-text text-right font-mono text-text-body" decimals={token.decimals} + showFullAmount symbol={tokenSymbol} /> </div> diff --git a/packages/stateless/components/token/TokenLine.tsx b/packages/stateless/components/token/TokenLine.tsx index 88d0ad963..3a635b184 100644 --- a/packages/stateless/components/token/TokenLine.tsx +++ b/packages/stateless/components/token/TokenLine.tsx @@ -102,7 +102,6 @@ export const TokenLine = <T extends TokenCardInfo>( className="body-text truncate text-right font-mono" decimals={token.decimals} hideSymbol - showFullAmount wrapperClassName="justify-end" /> @@ -113,8 +112,10 @@ export const TokenLine = <T extends TokenCardInfo>( amount={ lazyInfo.loading || !lazyInfo.data.usdUnitPrice?.usdPrice ? { loading: true } - : lazyInfo.data.totalBalance * - lazyInfo.data.usdUnitPrice.usdPrice + : lazyInfo.data.totalBalance.toUsdValue( + token.decimals, + lazyInfo.data.usdUnitPrice.usdPrice + ) } className="caption-text font-mono" dateFetched={ diff --git a/packages/stateless/components/token/UnstakingLine.stories.tsx b/packages/stateless/components/token/UnstakingLine.stories.tsx index 9c548b239..ad5600866 100644 --- a/packages/stateless/components/token/UnstakingLine.stories.tsx +++ b/packages/stateless/components/token/UnstakingLine.stories.tsx @@ -1,5 +1,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' +import { HugeDecimal } from '@dao-dao/math' + import { Button } from '../buttons/Button' import { token } from './TokenCard.stories' import { UnstakingLine, UnstakingLineProps } from './UnstakingLine' @@ -26,7 +28,7 @@ export const makeProps = ( }, status, // Random number between 0 and 1000, with up to 6 decimals. - amount: Math.floor(Math.random() * (1000 * 1e6) + 1e6) / 1e6, + amount: HugeDecimal.from(Math.floor(Math.random() * (1000 * 1e6) + 1e6)), date: new Date( Date.now() + Math.random() * diff --git a/packages/stateless/components/token/UnstakingModal.tsx b/packages/stateless/components/token/UnstakingModal.tsx index 36c55cbf5..e576035e2 100644 --- a/packages/stateless/components/token/UnstakingModal.tsx +++ b/packages/stateless/components/token/UnstakingModal.tsx @@ -58,7 +58,7 @@ export const UnstakingModal = ({ // If found, just modify existing by increasing amount. No need to // worry about the date since it will be replaced by a claim button. if (existingTask) { - existingTask.amount += task.amount + existingTask.amount = existingTask.amount.plus(task.amount) } else { // If not found, add this task as the new one. combinedTasks.push(task) diff --git a/packages/stateless/components/vesting/VestingPaymentCard.stories.tsx b/packages/stateless/components/vesting/VestingPaymentCard.stories.tsx index 3ae081dc9..9da7ccaa6 100644 --- a/packages/stateless/components/vesting/VestingPaymentCard.stories.tsx +++ b/packages/stateless/components/vesting/VestingPaymentCard.stories.tsx @@ -1,5 +1,6 @@ import { ComponentMeta, ComponentStory } from '@storybook/react' +import { HugeDecimal } from '@dao-dao/math' import { ButtonLink, EntityDisplay } from '@dao-dao/stateful' import { CHAIN_ID } from '@dao-dao/storybook' import { DaoPageWrapperDecorator } from '@dao-dao/storybook/decorators/DaoPageWrapperDecorator' @@ -58,9 +59,9 @@ Default.args = { claiming: false, onManageStake: () => alert('manage stake'), onAddToken: () => alert('add token'), - remainingBalanceVesting: 401239.5123, - distributableAmount: 1942.7984, - claimedAmount: 39.234, + remainingBalanceVesting: HugeDecimal.fromHumanReadable(401239.5123, 6), + distributableAmount: HugeDecimal.fromHumanReadable(1942.7984, 6), + claimedAmount: HugeDecimal.fromHumanReadable(39.234, 6), // Started 2 days ago. startDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // Ends in 7 days. @@ -69,12 +70,12 @@ Default.args = { { // Started 2 days ago. timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, - amount: 0, + amount: HugeDecimal.zero, }, { // Ends in 7 days. timestamp: Date.now() + 1000 * 60 * 60 * 24 * 7, - amount: 403221.5447, + amount: HugeDecimal.fromHumanReadable(403221.5447, 6), }, ], } diff --git a/packages/stateless/components/vesting/VestingPaymentCard.tsx b/packages/stateless/components/vesting/VestingPaymentCard.tsx index ee0cd828e..f036f70d1 100644 --- a/packages/stateless/components/vesting/VestingPaymentCard.tsx +++ b/packages/stateless/components/vesting/VestingPaymentCard.tsx @@ -10,6 +10,7 @@ import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import TimeAgo from 'react-timeago' +import { HugeDecimal } from '@dao-dao/math' import { ButtonLinkProps, ButtonPopupSection, @@ -53,9 +54,9 @@ export type VestingPaymentCardProps = { title: string | undefined | null description: string | undefined | null - remainingBalanceVesting: number - distributableAmount: number - claimedAmount: number + remainingBalanceVesting: HugeDecimal + distributableAmount: HugeDecimal + claimedAmount: HugeDecimal startDate: Date endDate: Date steps: VestingStep[] @@ -121,18 +122,24 @@ export const VestingPaymentCard = ({ ? [] : lazyInfo.data.stakingInfo.unstakingTasks - const totalStaked = - lazyStakes.reduce((acc, stake) => acc + stake.amount, 0) ?? 0 - const pendingRewards = - lazyStakes?.reduce((acc, stake) => acc + stake.rewards, 0) ?? 0 - const unstakingBalance = - lazyUnstakingTasks.reduce( - (acc, task) => - acc + + const totalStaked = lazyStakes.reduce( + (acc, stake) => acc.plus(stake.amount), + HugeDecimal.zero + ) + const pendingRewards = lazyStakes?.reduce( + (acc, stake) => acc.plus(stake.rewards), + HugeDecimal.zero + ) + const unstakingBalance = lazyUnstakingTasks.reduce( + (acc, task) => + acc.plus( // Only include balance of unstaking tasks. - (task.status === UnstakingTaskStatus.Unstaking ? task.amount : 0), - 0 - ) ?? 0 + task.status === UnstakingTaskStatus.Unstaking + ? task.amount + : HugeDecimal.zero + ), + HugeDecimal.zero + ) const [showUnstakingTokens, setShowUnstakingTokens] = useState(false) @@ -151,7 +158,7 @@ export const VestingPaymentCard = ({ const canWithdraw = isWalletConnected && (recipientIsWallet || recipientIsDao) && - distributableAmount > 0 + distributableAmount.isPositive() const buttonPopupSections: ButtonPopupSection[] = useMemo( () => [ @@ -420,7 +427,8 @@ export const VestingPaymentCard = ({ <div className="flex flex-col gap-3 border-t border-border-secondary py-4 px-6"> {/* Show available balance to withdraw if it is nonzero OR if there is still a balance vesting. This ensures that it explicitly displays that there is no balance to withdraw when the vest is not yet over. There may not be any balance if all vested tokens are staked or still unstaking, and it might be confusing if this line remains hidden in that case. */} - {(distributableAmount > 0 || remainingBalanceVesting > 0) && ( + {(distributableAmount.isPositive() || + remainingBalanceVesting.isPositive()) && ( <div className="flex flex-row items-start justify-between gap-8"> <p className="link-text">{t('info.availableBalance')}</p> @@ -443,8 +451,9 @@ export const VestingPaymentCard = ({ lazyInfo.loading || !lazyInfo.data.usdUnitPrice?.usdPrice ? { loading: true } - : distributableAmount * - lazyInfo.data.usdUnitPrice.usdPrice + : distributableAmount + .times(lazyInfo.data.usdUnitPrice.usdPrice) + .toHumanReadableNumber(token.decimals) } dateFetched={ lazyInfo.loading || !lazyInfo.data.usdUnitPrice @@ -464,7 +473,7 @@ export const VestingPaymentCard = ({ </div> )} - {remainingBalanceVesting > 0 && ( + {remainingBalanceVesting.isPositive() && ( <div className="flex flex-row items-start justify-between gap-8"> <p className="link-text">{t('info.remainingBalanceVesting')}</p> @@ -487,8 +496,9 @@ export const VestingPaymentCard = ({ lazyInfo.loading || !lazyInfo.data.usdUnitPrice?.usdPrice ? { loading: true } - : remainingBalanceVesting * - lazyInfo.data.usdUnitPrice.usdPrice + : remainingBalanceVesting + .times(lazyInfo.data.usdUnitPrice.usdPrice) + .toHumanReadableNumber(token.decimals) } dateFetched={ lazyInfo.loading || !lazyInfo.data.usdUnitPrice @@ -600,13 +610,13 @@ export const VestingPaymentCard = ({ <Button className={clsx( 'caption-text text-right font-mono underline-offset-2', - unstakingBalance > 0 && 'text-text-body', + unstakingBalance.isPositive() && 'text-text-body', lazyInfo.loading && 'animate-pulse !text-text-body' )} disabled={lazyInfo.loading} onClick={() => setShowUnstakingTokens(true)} variant={ - lazyInfo.loading || unstakingBalance === 0 + lazyInfo.loading || unstakingBalance.isZero() ? 'none' : 'underline' } diff --git a/packages/stateless/components/vesting/VestingPaymentLine.tsx b/packages/stateless/components/vesting/VestingPaymentLine.tsx index 1c15f9503..8b1f719be 100644 --- a/packages/stateless/components/vesting/VestingPaymentLine.tsx +++ b/packages/stateless/components/vesting/VestingPaymentLine.tsx @@ -3,11 +3,7 @@ import { useTranslation } from 'react-i18next' import TimeAgo from 'react-timeago' import { VestingPaymentLineProps } from '@dao-dao/types' -import { - convertMicroDenomToDenomWithDecimals, - formatDate, - formatDateTimeTz, -} from '@dao-dao/utils' +import { formatDate, formatDateTimeTz } from '@dao-dao/utils' import { useTranslatedTimeDeltaFormatter } from '../../hooks' import { ChainProvider } from '../chain' @@ -67,12 +63,9 @@ export const VestingPaymentLine = ({ <div className="hidden md:block"> {/* Only show balance available to withdraw if nonzero. */} - {distributable !== '0' && ( + {distributable.isPositive() && ( <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - distributable, - token.decimals - )} + amount={distributable} className="body-text truncate font-mono" decimals={token.decimals} symbol={token.symbol} @@ -81,10 +74,7 @@ export const VestingPaymentLine = ({ </div> <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - total, - token.decimals - )} + amount={total} className="body-text truncate text-right font-mono" decimals={token.decimals} symbol={token.symbol} @@ -122,10 +112,7 @@ export const VestingPaymentLine = ({ <div className="body-text flex flex-row items-center justify-end gap-1 justify-self-end text-right font-mono"> <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - vested, - token.decimals - )} + amount={vested} className="truncate" decimals={token.decimals} hideSymbol @@ -134,10 +121,7 @@ export const VestingPaymentLine = ({ <p>/</p> <TokenAmountDisplay - amount={convertMicroDenomToDenomWithDecimals( - total, - token.decimals - )} + amount={total} className="truncate" decimals={token.decimals} symbol={token.symbol} diff --git a/packages/stateless/components/vesting/VestingStepsLineGraph.tsx b/packages/stateless/components/vesting/VestingStepsLineGraph.tsx index 1a4cbee4a..bfe9239c2 100644 --- a/packages/stateless/components/vesting/VestingStepsLineGraph.tsx +++ b/packages/stateless/components/vesting/VestingStepsLineGraph.tsx @@ -53,7 +53,7 @@ export const VestingStepsLineGraph = ({ title={t('title.vestingCurve')} verticalLineAtX={verticalLineAtX} yTitle={'$' + tokenSymbol} - yValues={[0, ...steps.map(({ amount }) => amount)]} + yValues={[0, ...steps.map(({ amount }) => amount.toNumber())]} /> ) } diff --git a/packages/stateless/contexts/Dao.ts b/packages/stateless/contexts/Dao.ts index d77a8e19f..faf44848d 100644 --- a/packages/stateless/contexts/Dao.ts +++ b/packages/stateless/contexts/Dao.ts @@ -21,6 +21,7 @@ export const useDaoContext = () => { export const useDaoContextIfAvailable = () => useContext(DaoContext) -export const useDaoInfoContext = () => useDaoContext().dao.info -export const useDaoInfoContextIfAvailable = () => - useDaoContextIfAvailable()?.dao.info +export const useDao = () => useDaoContext().dao +export const useDaoIfAvailable = () => useDaoContextIfAvailable()?.dao + +export const useVotingModule = () => useDao().votingModule diff --git a/packages/stateless/hooks/useTokenSortOptions.ts b/packages/stateless/hooks/useTokenSortOptions.ts index 28594ffe2..9999f9572 100644 --- a/packages/stateless/hooks/useTokenSortOptions.ts +++ b/packages/stateless/hooks/useTokenSortOptions.ts @@ -1,7 +1,10 @@ import { useTranslation } from 'react-i18next' import { SortFn, TokenCardInfo, TypedOption } from '@dao-dao/types' -import { sortTokensValueDescending } from '@dao-dao/utils' +import { + sortTokensValueAscending, + sortTokensValueDescending, +} from '@dao-dao/utils' /** * Options to use with the `useButtonPopupSorter` hook and the `ButtonPopup` @@ -19,30 +22,7 @@ export const useTokenSortOptions = (): TypedOption< }, { label: t('info.lowestUsdValue'), - value: (a, b) => { - // If loading or no price, show at bottom. - const aPrice = - a.lazyInfo.loading || !a.lazyInfo.data.usdUnitPrice?.usdPrice - ? undefined - : a.lazyInfo.data.totalBalance * - a.lazyInfo.data.usdUnitPrice.usdPrice - const bPrice = - b.lazyInfo.loading || !b.lazyInfo.data.usdUnitPrice?.usdPrice - ? undefined - : b.lazyInfo.data.totalBalance * - b.lazyInfo.data.usdUnitPrice.usdPrice - - // If prices are equal, sort alphabetically by symbol. - return aPrice === bPrice - ? a.token.symbol - .toLocaleLowerCase() - .localeCompare(b.token.symbol.toLocaleLowerCase()) - : aPrice === undefined - ? 1 - : bPrice === undefined - ? -1 - : aPrice - bPrice - }, + value: sortTokensValueAscending, }, { // Most token symbols are in English, so no need to translate. diff --git a/packages/stateless/package.json b/packages/stateless/package.json index e670cb44d..8660c73e9 100644 --- a/packages/stateless/package.json +++ b/packages/stateless/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@cosmjs/encoding": "^0.32.3", + "@dao-dao/math": "2.5.0-rc.3", "@dao-dao/types": "2.5.0-rc.3", "@dao-dao/utils": "2.5.0-rc.3", "@emotion/react": "^11.10.4", diff --git a/packages/stateless/pages/DaoDappTabbedHome.tsx b/packages/stateless/pages/DaoDappTabbedHome.tsx index 195de9f7d..cbdc3747c 100644 --- a/packages/stateless/pages/DaoDappTabbedHome.tsx +++ b/packages/stateless/pages/DaoDappTabbedHome.tsx @@ -9,7 +9,7 @@ import { import { PageLoader, TabBar } from '../components' import { DaoSplashHeader } from '../components/dao/DaoSplashHeader' -import { useDaoInfoContext } from '../contexts' +import { useDao } from '../contexts' import { useTabBarScrollReset } from '../hooks' export const DaoDappTabbedHome = ({ @@ -21,7 +21,7 @@ export const DaoDappTabbedHome = ({ onSelectTabId, ...headerProps }: DaoDappTabbedHomeProps) => { - const daoInfo = useDaoInfoContext() + const dao = useDao() // Auto scroll to top of tab on change. const { tabBarRef, tabContainerRef } = useTabBarScrollReset({ @@ -34,7 +34,7 @@ export const DaoDappTabbedHome = ({ <DaoSplashHeader ButtonLink={ButtonLink} LinkWrapper={LinkWrapper} - daoInfo={daoInfo} + dao={dao} {...headerProps} /> </div> diff --git a/packages/types/chain.ts b/packages/types/chain.ts index b6c8464f0..248f7aff1 100644 --- a/packages/types/chain.ts +++ b/packages/types/chain.ts @@ -1,5 +1,7 @@ import { Chain } from '@chain-registry/types' +import { HugeDecimal } from '@dao-dao/math' + import { Coin } from './contracts' import { ContractVersion } from './features' import { GenericToken, TokenType } from './token' @@ -26,30 +28,30 @@ export type SupportedChainContext = Omit<ConfiguredChainContext, 'config'> & { config: SupportedChainConfig } -export interface Validator { +export type Validator = { address: string moniker: string website: string details: string commission: number status: string - tokens: number + tokens: HugeDecimal } -export interface Delegation { +export type Delegation = { validator: Validator delegated: Coin pendingReward: Coin } -export interface UnbondingDelegation { +export type UnbondingDelegation = { validator: Validator balance: Coin startedAtHeight: number finishesAt: Date } -export interface NativeDelegationInfo { +export type NativeDelegationInfo = { delegations: Delegation[] unbondingDelegations: UnbondingDelegation[] } diff --git a/packages/types/clients/proposal-module.ts b/packages/types/clients/proposal-module.ts index fb05aeb91..7ceb55801 100644 --- a/packages/types/clients/proposal-module.ts +++ b/packages/types/clients/proposal-module.ts @@ -25,6 +25,11 @@ export interface IProposalModuleBase< */ info: ProposalModuleInfo + /** + * Chain ID of the proposal module. + */ + chainId: string + /** * Contract address. */ diff --git a/packages/types/clients/voting-module.ts b/packages/types/clients/voting-module.ts index c8496c8db..f44ca1a18 100644 --- a/packages/types/clients/voting-module.ts +++ b/packages/types/clients/voting-module.ts @@ -14,6 +14,11 @@ export interface IVotingModuleBase<Dao extends IDaoBase = IDaoBase> { */ dao: Dao + /** + * Chain ID of the voting module. + */ + chainId: string + /** * Address of the voting module. */ diff --git a/packages/types/components/Breadcrumbs.ts b/packages/types/components/Breadcrumbs.ts index 94bac8047..25eef4bb7 100644 --- a/packages/types/components/Breadcrumbs.ts +++ b/packages/types/components/Breadcrumbs.ts @@ -1,6 +1,7 @@ import { ReactNode } from 'react' -import { DaoInfo, DaoTabId } from '../dao' +import { IDaoBase } from '../clients' +import { DaoTabId } from '../dao' export type BreadcrumbCrumb = { href: string @@ -32,10 +33,10 @@ export type BreadcrumbsProps = { */ current: ReactNode /** - * DAO info, if this is being rendered outside of the context provider (like - * in the PageHeader), but still needs access to the DAO info. + * DAO, if this is being rendered outside of the context provider (like in the + * PageHeader), but still needs access to the DAO. */ - daoInfo?: DaoInfo | null + dao?: IDaoBase | null /** * Optional container class name. */ diff --git a/packages/types/components/DaoDappTabbedHome.ts b/packages/types/components/DaoDappTabbedHome.ts index d02874a59..376cc337e 100644 --- a/packages/types/components/DaoDappTabbedHome.ts +++ b/packages/types/components/DaoDappTabbedHome.ts @@ -6,7 +6,7 @@ import { DaoSplashHeaderProps } from './DaoSplashHeader' import { LinkWrapperProps } from './LinkWrapper' import { SuspenseLoaderProps } from './SuspenseLoader' -export type DaoDappTabbedHomeProps = Omit<DaoSplashHeaderProps, 'daoInfo'> & { +export type DaoDappTabbedHomeProps = Omit<DaoSplashHeaderProps, 'dao'> & { SuspenseLoader: ComponentType<SuspenseLoaderProps> ButtonLink: ComponentType<ButtonLinkProps> LinkWrapper: ComponentType<LinkWrapperProps> diff --git a/packages/types/components/DaoMemberCard.ts b/packages/types/components/DaoMemberCard.ts index c4c93ed6e..6e7af1880 100644 --- a/packages/types/components/DaoMemberCard.ts +++ b/packages/types/components/DaoMemberCard.ts @@ -1,5 +1,7 @@ import { ComponentType } from 'react' +import { HugeDecimal } from '@dao-dao/math' + import { LoadingData } from '../misc' import { GenericToken } from '../token' import { ButtonLinkProps } from './Buttonifier' @@ -9,7 +11,7 @@ export type DaoMemberCardProps = { address: string balanceLabel: string balance: LoadingData<{ - amount: number + amount: number | HugeDecimal token?: GenericToken }> votingPowerPercent: LoadingData<number> diff --git a/packages/types/components/DaoSplashHeader.ts b/packages/types/components/DaoSplashHeader.ts index 00b8270a2..397721ee3 100644 --- a/packages/types/components/DaoSplashHeader.ts +++ b/packages/types/components/DaoSplashHeader.ts @@ -1,12 +1,12 @@ import { ComponentType } from 'react' -import { DaoInfo } from '../dao' +import { IDaoBase } from '../clients' import { ButtonLinkProps } from './Buttonifier' import { FollowState } from './DaoCard' import { LinkWrapperProps } from './LinkWrapper' export type DaoSplashHeaderProps = { - daoInfo: DaoInfo + dao: IDaoBase follow?: FollowState ButtonLink: ComponentType<ButtonLinkProps> LinkWrapper: ComponentType<LinkWrapperProps> diff --git a/packages/types/components/NumberInput.ts b/packages/types/components/NumberInput.ts deleted file mode 100644 index 4c87266a9..000000000 --- a/packages/types/components/NumberInput.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ComponentPropsWithoutRef } from 'react' -import { - FieldPathValue, - FieldValues, - Path, - UseFormRegister, - Validate, -} from 'react-hook-form' - -export interface NumberInputProps< - FV extends FieldValues, - FieldName extends Path<FV> -> extends Omit< - ComponentPropsWithoutRef<'input'>, - 'type' | 'required' | 'value' - > { - // The field name for the form. - fieldName?: FieldName - // Register function returned by `useForm`/`useFormContext`. - register?: UseFormRegister<FV> - // Validations to apply when registering this input. - validation?: Validate<FieldPathValue<FV, FieldName>>[] - // Applies to the input when registering with a form. - required?: boolean - // Transform the value displayed in the input by these decimals. - transformDecimals?: number - // If error present, outline input in red. - error?: any - // Hide plus/minus buttons - hidePlusMinus?: boolean - // Value passed to the input. - value?: number - // Used to get the value when the plus/minus buttons are clicked. Accepts the - // react-hook-form `watch` function, or any custom function. - watch?: (fieldName: any) => number | undefined - // Used to set the value when the plus/minus buttons are clicked. Accepts the - // react-hook-form `watch` function, or any custom function. - setValue?: ( - fieldName: any, - value: number, - options?: { shouldValidate: boolean } - ) => void - // Applies to the outer-most container, which contains the plus/minus buttons, - // the input, and the unit. - containerClassName?: string - // Size of the container. - sizing?: 'sm' | 'md' | 'lg' | 'auto' | 'fill' | 'none' - // Remove padding, rounded corners, and outline. - ghost?: boolean - // A unit to display to the right of the number. - unit?: string - // A unit icon URL that displays to the left of the unit. - unitIconUrl?: string - // Applies to both the input text and the unit. - textClassName?: string - // Applies to the unit only. - unitClassName?: string - // Applies to the unit icon only. - unitIconClassName?: string - // Applies to the unit container only. - unitContainerClassName?: string - // Size of the plus/minus buttons. Defaults to 'sm'. - plusMinusButtonSize?: 'sm' | 'lg' -} diff --git a/packages/types/components/NumericInput.ts b/packages/types/components/NumericInput.ts new file mode 100644 index 000000000..a9ef14205 --- /dev/null +++ b/packages/types/components/NumericInput.ts @@ -0,0 +1,107 @@ +import { ComponentPropsWithoutRef } from 'react' +import { + FieldPathValue, + FieldValues, + Path, + UseFormRegister, + Validate, +} from 'react-hook-form' + +export type NumericInputProps< + FV extends FieldValues, + FieldName extends Path<FV> +> = Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'required' | 'onInput'> & { + /** + * The field name for the form. + */ + fieldName?: FieldName + /** + * Register function returned by `useForm`/`useFormContext`. + */ + register?: UseFormRegister<FV> + /** + * Validations to apply when registering this input. + */ + validation?: Validate<FieldPathValue<FV, FieldName>>[] + /** + * Applies to the input when registering with a form. + */ + required?: boolean + /** + * If error present, outline input in red. + */ + error?: any + /** + * Hide plus/minus buttons + */ + hidePlusMinus?: boolean + /** + * Whether or not to store the value as a number in the react-hook-form form. + * This should only be used when not needing to store potentially large + * numbers. + * + * Defaults to false. + */ + numericValue?: boolean + /** + * Value passed to the input. + */ + value?: string + /** + * Used to get the value when the plus/minus buttons are clicked. Accepts the + * react-hook-form `getValues` function, or any custom function. + */ + getValues?: (fieldName: any) => string | undefined + /** + * Used to set the value when the input changes (if `register` is not pased + * and thus react-hook-form is not being used) and when plus/minus buttons are + * clicked. Accepts the react-hook-form `setValue` function, or any custom + * function. + */ + setValue?: ( + fieldName: any, + value: string | number, + options?: { shouldValidate: boolean } + ) => void + /** + * Applies to the outer-most container, which contains the plus/minus buttons, + * the input, and the unit. + */ + containerClassName?: string + /** + * Size of the container. + */ + sizing?: 'sm' | 'md' | 'lg' | 'auto' | 'fill' | 'none' + /** + * Remove padding, rounded corners, and outline. + */ + ghost?: boolean + /** + * A unit to display to the right of the number. + */ + unit?: string + /** + * A unit icon URL that displays to the left of the unit. + */ + unitIconUrl?: string + /** + * Applies to both the input text and the unit. + */ + textClassName?: string + /** + * Applies to the unit only. + */ + unitClassName?: string + /** + * Applies to the unit icon only. + */ + unitIconClassName?: string + /** + * Applies to the unit container only. + */ + unitContainerClassName?: string + /** + * Size of the plus/minus buttons. Defaults to 'sm'. + */ + plusMinusButtonSize?: 'sm' | 'lg' +} diff --git a/packages/types/components/StakingModal.ts b/packages/types/components/StakingModal.ts index dab280bde..19c8bd640 100644 --- a/packages/types/components/StakingModal.ts +++ b/packages/types/components/StakingModal.ts @@ -1,3 +1,5 @@ +import { HugeDecimal } from '@dao-dao/math' + import { Duration } from '../contracts/common' import { DOmit, LoadingData } from '../misc' import { GenericToken } from '../token' @@ -15,25 +17,25 @@ export interface StakingModalProps { // The mode to open the staking modal in. initialMode: StakingMode // The number of tokens in question. - amount: number + amount: HugeDecimal // Sets the number of tokens in question. - setAmount: (newAmount: number) => void + setAmount: (newAmount: HugeDecimal) => void // Called when the staking modal is closed. onClose: () => void // The number of tokens that are currently claimable. - claimableTokens: number + claimableTokens: HugeDecimal // The number of tokens that are unstakable. If undefined, will not be shown. // If `validatorPicker` is present, unstakable tokens will depend on the // chosen validator. - loadingUnstakableTokens?: LoadingData<number> + loadingUnstakableTokens?: LoadingData<HugeDecimal> // The number of tokens that are stakable. - loadingStakableTokens: LoadingData<number> + loadingStakableTokens: LoadingData<HugeDecimal> // The duration for unstaking. unstakingDuration: Duration | null // Token that is being staked. token: GenericToken // Proposal deposit for the token that is being staked. - proposalDeposit?: number + proposalDeposit?: HugeDecimal // Is there an error? error?: string | undefined // Are we ready to stake? Ex: is wallet connected? @@ -41,7 +43,7 @@ export interface StakingModalProps { // Triggered when the stake / unstake / claim button is pressed. onAction: ( mode: StakingMode, - amount: number, + amount: HugeDecimal, validator?: string, // If mode is `StakingMode.Restake`, this will be the validator to unstake // funds from. diff --git a/packages/types/components/TokenAmountDisplay.ts b/packages/types/components/TokenAmountDisplay.ts index 14059e727..0b409f119 100644 --- a/packages/types/components/TokenAmountDisplay.ts +++ b/packages/types/components/TokenAmountDisplay.ts @@ -1,5 +1,7 @@ import { ComponentPropsWithoutRef } from 'react' +import { HugeDecimal } from '@dao-dao/math' + import { Coin } from '../contracts' import { LoadingData } from '../misc' @@ -7,31 +9,21 @@ export type TokenAmountDisplayProps = Omit< ComponentPropsWithoutRef<'p'>, 'children' > & { - amount: number | LoadingData<number> + /** + * The amount to display. If a HugeDecimal instance is passed, it is assumed + * to be a raw number and will be converted to a human-readable number using + * the decimals passed. If a number is passed, it must be a human-readable + * number (with decimals). + */ + amount: number | HugeDecimal | LoadingData<number | HugeDecimal> prefix?: string prefixClassName?: string suffix?: string suffixClassName?: string - /** - * Max decimals to display. - */ - maxDecimals?: number - /** - * Min decimals to display. - */ - minDecimals?: number - /** - * Don't show approximation indication (like a tilde). - */ - hideApprox?: boolean /** * Add to tooltip if present. */ dateFetched?: Date - /** - * Show full amount if true. - */ - showFullAmount?: boolean /** * If present, will add a rounded icon to the left. */ @@ -66,6 +58,15 @@ export type TokenAmountDisplayProps = Omit< * prefix and display this value. */ minAmount?: number + /** + * Show full amount if true. Defaults to false. + */ + showFullAmount?: boolean + /** + * Pad decimal places by appending zeros if the value does not have as + * many decimals as specified. + */ + showAllDecimals?: boolean estimatedUsdValue?: false } // Alow hiding symbol. @@ -81,6 +82,15 @@ export type TokenAmountDisplayProps = Omit< * prefix and display this value. */ minAmount?: number + /** + * Show full amount if true. Defaults to false. + */ + showFullAmount?: boolean + /** + * Pad decimal places by appending zeros if the value does not have as + * many decimals as specified. + */ + showAllDecimals?: boolean estimatedUsdValue?: false } // If USD estimate, disallow symbol, decimals, and minAmount. @@ -89,6 +99,8 @@ export type TokenAmountDisplayProps = Omit< hideSymbol?: boolean decimals?: never minAmount?: never + showFullAmount?: never + showAllDecimals?: never estimatedUsdValue: true } ) @@ -99,8 +111,6 @@ export type StatefulTokenAmountDisplayProps = Pick< | 'prefixClassName' | 'suffix' | 'suffixClassName' - | 'maxDecimals' - | 'hideApprox' | 'showFullAmount' | 'iconClassName' | 'onClick' diff --git a/packages/types/components/TokenInput.ts b/packages/types/components/TokenInput.ts index 3ab93306c..b3b389f49 100644 --- a/packages/types/components/TokenInput.ts +++ b/packages/types/components/TokenInput.ts @@ -2,6 +2,7 @@ import { ReactNode, RefCallback } from 'react' import { FieldValues, Path, + UseFormGetValues, UseFormRegister, UseFormSetValue, UseFormWatch, @@ -10,14 +11,11 @@ import { import { LoadingData } from '../misc' import { GenericToken, TokenType } from '../token' -import { NumberInputProps } from './NumberInput' +import { NumericInputProps } from './NumericInput' -export type TokenInputOption = Omit<GenericToken, 'type' | 'decimals'> & { +export type TokenInputOption = Omit<GenericToken, 'type'> & { type: TokenType | string description?: string - // Only necessary if `convertMicroDenom` is true so the input can - // intelligently convert the value. 0 will be used if not provided. - decimals?: number } export type TokenInputProps< @@ -29,27 +27,20 @@ export type TokenInputProps< * The fields that control the amount input. */ amount?: Omit< - NumberInputProps<FV, FieldName>, + NumericInputProps<FV, FieldName>, | 'containerClassName' | 'disabled' - | 'transformDecimals' | 'register' - | 'watch' + | 'getValues' | 'setValue' | 'fieldName' > & { register: UseFormRegister<FV> watch: UseFormWatch<FV> setValue: UseFormSetValue<FV> + getValues: UseFormGetValues<FV> fieldName: FieldName validations?: Validate<number>[] - /* - * If true, will convert the amount to micro-denom using the token's - * decimals value for the form. Thus, the input will display the macro-denom - * amount, but the form will receive the micro-denom amount. Default is - * false. - */ - convertMicroDenom?: boolean } /** * The available tokens and selection handlers for the token. Various diff --git a/packages/types/components/index.ts b/packages/types/components/index.ts index 5d93c41ad..905efd0c9 100644 --- a/packages/types/components/index.ts +++ b/packages/types/components/index.ts @@ -36,7 +36,7 @@ export * from './Modal' export * from './NavWallet' export * from './NavWalletConnected' export * from './Notifications' -export * from './NumberInput' +export * from './NumericInput' export * from './PageHeader' export * from './PayEntityDisplay' export * from './Popup' diff --git a/packages/types/dao.ts b/packages/types/dao.ts index 88170469b..e70dce2cf 100644 --- a/packages/types/dao.ts +++ b/packages/types/dao.ts @@ -7,11 +7,14 @@ import { FieldPathValue, FieldValues, FormState, + UseFormGetValues, UseFormRegister, UseFormReturn, UseFormSetValue, } from 'react-hook-form' +import { HugeDecimal } from '@dao-dao/math' + import { Account } from './account' import { SupportedChainConfig, WithChainId } from './chain' import { SecretModuleInstantiateInfo } from './clients' @@ -348,6 +351,7 @@ export interface DaoCreationVotingConfigItemInputProps< data: ModuleData register: UseFormRegister<ModuleData> setValue: UseFormSetValue<ModuleData> + getValues: UseFormGetValues<ModuleData> watch: <TFieldName extends FieldPath<ModuleData>>( name: TFieldName, defaultValue?: FieldPathValue<ModuleData, TFieldName> @@ -410,7 +414,7 @@ export type DaoCreationVotingConfigWithAllowRevoting = { export type DaoCreationVotingConfigWithProposalDeposit = { proposalDeposit: { enabled: boolean - amount: number + amount: string // Token input fields. type: 'native' | 'cw20' | 'voting_module_token' denomOrAddress: string @@ -440,7 +444,7 @@ export type DaoCreationVotingConfigWithActiveThreshold = { activeThreshold: { enabled: boolean type: 'percent' | 'absolute' - value: number + value: string } } @@ -593,7 +597,7 @@ export type VotingVaultInfo = export type VotingVaultWithInfo = VotingVault & { info: VotingVaultInfo - totalPower: string + totalPower: HugeDecimal } /** @@ -635,7 +639,7 @@ export type DaoRewardDistributionWithRemaining = DaoRewardDistribution & { /** * Remaining rewards to be distributed. */ - remaining: number + remaining: HugeDecimal } /** @@ -653,7 +657,7 @@ export type PendingDaoRewards = { /** * Pending rewards for the distribution. */ - rewards: number + rewards: HugeDecimal }[] /** * Total pending rewards across all distributions, merged by token. diff --git a/packages/types/gov.ts b/packages/types/gov.ts index c5b5c9d6e..e3f3f0bef 100644 --- a/packages/types/gov.ts +++ b/packages/types/gov.ts @@ -155,15 +155,17 @@ export type GovernanceProposalActionData = { description: string metadata: string deposit: { - amount: number + amount: string denom: string + decimals: number }[] legacy: { typeUrl: string // CommunityPoolSpendProposal spends: { - amount: number + amount: string denom: string + decimals: number }[] spendRecipient: string // ParameterChangeProposal diff --git a/packages/types/nft.ts b/packages/types/nft.ts index aabdac3ba..38af6043a 100644 --- a/packages/types/nft.ts +++ b/packages/types/nft.ts @@ -1,5 +1,7 @@ import { ComponentType, ReactNode, RefAttributes } from 'react' +import { HugeDecimal } from '@dao-dao/math' + import { ChainId, WithChainId } from './chain' import { ButtonLinkProps, @@ -88,7 +90,7 @@ export type NftCardInfo = { metadata?: Record<string, any> highestOffer?: { offerToken?: GenericToken | null - amount?: number | null + amount?: HugeDecimal | null amountUsd?: number | null } name: string diff --git a/packages/types/package.json b/packages/types/package.json index e60099481..0ba972c87 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -13,6 +13,7 @@ "dependencies": { "@chain-registry/types": "^0.41.3", "@cosmos-kit/web3auth": "^2.8.0", + "@dao-dao/math": "2.5.0-rc.3", "next-i18next": "^11.0.0", "pusher-js": "^7.6.0", "react": "^18.2.0", diff --git a/packages/types/token.ts b/packages/types/token.ts index c31dc4d03..5abc3e3b8 100644 --- a/packages/types/token.ts +++ b/packages/types/token.ts @@ -1,5 +1,7 @@ import { ComponentType } from 'react' +import { HugeDecimal } from '@dao-dao/math' + import { Account } from './account' import { ChainId, Validator } from './chain' import { @@ -130,7 +132,7 @@ export enum UnstakingTaskStatus { export type UnstakingTask = { token: GenericToken status: UnstakingTaskStatus - amount: number + amount: HugeDecimal // If unstaking or ready to claim, date it will be/was unstaked. // If claimed, date it was claimed. date?: Date @@ -139,8 +141,8 @@ export type UnstakingTask = { export type TokenStake = { token: GenericToken validator: Validator - amount: number - rewards: number + amount: HugeDecimal + rewards: HugeDecimal } export type TokenCardLazyInfo = { @@ -150,20 +152,20 @@ export type TokenCardLazyInfo = { unstakingTasks: UnstakingTask[] unstakingDurationSeconds: number | undefined stakes: TokenStake[] - totalStaked: number - totalPendingRewards: number - totalUnstaking: number + totalStaked: HugeDecimal + totalPendingRewards: HugeDecimal + totalUnstaking: HugeDecimal } | undefined // unstakedBalance + totalStaked + totalUnstaking - totalBalance: number + totalBalance: HugeDecimal // Display DAOs that the token is used as governance in, and optionally an // amount of staked tokens. This is used to display how much a wallet has // staked. daosGoverned?: { coreAddress: string stakingContractAddress: string - stakedBalance?: number + stakedBalance?: HugeDecimal }[] } @@ -172,11 +174,15 @@ export type TokenCardInfo = { token: GenericToken isGovernanceToken: boolean subtitle?: string - unstakedBalance: number - // Only native tokens load staking info for now, so let's show a nice loader. + unstakedBalance: HugeDecimal + /** + * Only native tokens load staking info for now, so let's show a nice loader. + */ hasStakingInfo: boolean lazyInfo: LoadingData<TokenCardLazyInfo> - // If defined, adds a color indicator. + /** + * If defined, adds a color indicator. + */ color?: string /** * Whether or not to hide the chain icon on tokens. diff --git a/packages/types/vesting.ts b/packages/types/vesting.ts index 23a66ebf1..8daeb0829 100644 --- a/packages/types/vesting.ts +++ b/packages/types/vesting.ts @@ -1,3 +1,5 @@ +import { HugeDecimal } from '@dao-dao/math' + import { Vest } from './contracts/CwVesting' import { GenericToken } from './token' @@ -110,15 +112,15 @@ export type VestingInfo = { | undefined | null // Amount vested so far. - vested: string + vested: HugeDecimal // Amount available to distribute. - distributable: string + distributable: HugeDecimal // Total amount being vested. - total: string + total: HugeDecimal // The stakable balance. This is the unstaked amount still in the vesting // contract. It may be vested or not, but it is definitely not claimed nor // staked. - stakable: string + stakable: HugeDecimal // Slashes on staked or unstaked tokens that have occurred during the vest. slashes: VestingValidatorWithSlashes[] // Whether or not all slashes have been registered. @@ -135,7 +137,7 @@ export type VestingInfo = { export type VestingStep = { timestamp: number // Total amount vested at this timestamp. - amount: number + amount: HugeDecimal } export type CwVestingStakeEvent = { diff --git a/packages/types/voting-module-adapter.ts b/packages/types/voting-module-adapter.ts index 9c322a894..5f8234459 100644 --- a/packages/types/voting-module-adapter.ts +++ b/packages/types/voting-module-adapter.ts @@ -53,18 +53,11 @@ export type VotingModuleAdapter = { id: string contractNames: string[] - load: (options: IVotingModuleAdapterOptions) => IVotingModuleAdapter + load: (votingModule: IVotingModuleBase) => IVotingModuleAdapter } -export interface IVotingModuleAdapterOptions { - chainId: string - coreAddress: string - votingModuleAddress: string -} - -export interface IVotingModuleAdapterContext { +export type IVotingModuleAdapterContext = { id: string - options: IVotingModuleAdapterOptions adapter: IVotingModuleAdapter votingModule: IVotingModuleBase } diff --git a/packages/utils/chain.ts b/packages/utils/chain.ts index 6e0807406..20817af82 100644 --- a/packages/utils/chain.ts +++ b/packages/utils/chain.ts @@ -6,6 +6,7 @@ import uniq from 'lodash.uniq' import RIPEMD160 from 'ripemd160' import semverGte from 'semver/functions/gte' +import { HugeDecimal } from '@dao-dao/math' import { Account, BaseChainConfig, @@ -147,7 +148,7 @@ export const cosmosValidatorToValidator = ({ ? Number(commission.commissionRates.rate) : -1, status: bondStatusToJSON(status), - tokens: Number(tokens), + tokens: HugeDecimal.from(tokens), }) export const getImageUrlForChainId = (chainId: string): string => { diff --git a/packages/utils/conversion.ts b/packages/utils/conversion.ts index dcf218d65..13b0f58e4 100644 --- a/packages/utils/conversion.ts +++ b/packages/utils/conversion.ts @@ -16,35 +16,6 @@ import { Expiration } from '@dao-dao/types/contracts/common' import { getChainForChainId } from './chain' import { IPFS_GATEWAY_TEMPLATE, SITE_URL } from './constants' -export const convertMicroDenomToDenomWithDecimals = ( - amount: number | string, - decimals: number -) => { - if (typeof amount === 'string') { - amount = Number(amount) - } - amount = amount / Math.pow(10, decimals) - return isNaN(amount) ? 0 : amount -} - -export const convertDenomToMicroDenomWithDecimals = ( - amount: number | string, - decimals: number -) => { - if (typeof amount === 'string') { - amount = Number(amount) - } - // Need to round. Example: `8.029409 * Math.pow(10, 6)`. - amount = Math.round(amount * Math.pow(10, decimals)) - return isNaN(amount) ? 0 : amount -} - -// Using BigInt.toString() ensures the value is not abbreviated. The -// Number.toString() function abbreviates large numbers like 1e20. -export const convertDenomToMicroDenomStringWithDecimals = ( - ...params: Parameters<typeof convertDenomToMicroDenomWithDecimals> -) => BigInt(convertDenomToMicroDenomWithDecimals(...params)).toString() - export function convertFromMicroDenom(denom: string) { return denom?.substring(1).toUpperCase() } diff --git a/packages/utils/dao.ts b/packages/utils/dao.ts index 51c31a5bf..26590dd3a 100644 --- a/packages/utils/dao.ts +++ b/packages/utils/dao.ts @@ -1,5 +1,6 @@ import { TFunction } from 'react-i18next' +import { HugeDecimal } from '@dao-dao/math' import { Account, AccountType, @@ -14,10 +15,7 @@ import { import { InstantiateMsg as DaoDaoCoreInstantiateMsg } from '@dao-dao/types/contracts/DaoDaoCore' import { getSupportedChainConfig } from './chain' -import { - convertDurationToHumanReadableString, - convertMicroDenomToDenomWithDecimals, -} from './conversion' +import { convertDurationToHumanReadableString } from './conversion' export const getParentDaoBreadcrumbs = ( getDaoPath: (coreAddress: string) => string, @@ -97,7 +95,7 @@ export const getAccountChainId = ({ accounts, address, }: { - accounts: Account[] + accounts: readonly Account[] address: string }): string | undefined => accounts.find((account) => account.address === address)?.chainId @@ -199,11 +197,10 @@ export const getHumanReadableRewardDistributionLabel = ( : 'paused' in distribution.active_epoch.emission_rate ? t('title.paused') : t('info.amountEveryDuration', { - amount: convertMicroDenomToDenomWithDecimals( - distribution.active_epoch.emission_rate.linear.amount, - distribution.token.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: distribution.token.decimals, + amount: HugeDecimal.from( + distribution.active_epoch.emission_rate.linear.amount + ).toInternationalizedHumanReadableString({ + decimals: distribution.token.decimals, }), duration: convertDurationToHumanReadableString( t, diff --git a/packages/utils/format.ts b/packages/utils/format.ts index 43c89a830..61599b420 100644 --- a/packages/utils/format.ts +++ b/packages/utils/format.ts @@ -78,6 +78,7 @@ export const formatTime = (date: Date) => timeFormatter.format(date) export const isoStringForLocalDateString = (dateString: string) => new Date(dateString).toISOString() +// TODO(huge): replace with HugeDecimal // Select number of decimal digits, rounding down / truncating. export const toFixedDown = (value: number, digits: number) => { // If contains scientific notation, truncate and use BigInt to get rid of it. diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js new file mode 100644 index 000000000..50200e3d7 --- /dev/null +++ b/packages/utils/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config') diff --git a/packages/utils/nft.ts b/packages/utils/nft.ts index 1ae46d7d9..50503ab4a 100644 --- a/packages/utils/nft.ts +++ b/packages/utils/nft.ts @@ -1,5 +1,6 @@ // If name is only a number, prefix with collection name. Fallback to token ID +import { HugeDecimal } from '@dao-dao/math' import { GenericToken, NftCardInfo, @@ -110,7 +111,7 @@ export const nftCardInfoFromStargazeIndexerNft = ( ? { offerToken, amountUsd: token.highestOffer?.offerPrice?.amountUsd, - amount: Number(token.highestOffer?.offerPrice?.amount), + amount: HugeDecimal.from(token.highestOffer?.offerPrice?.amount || -1), } : undefined, fetchedTimestamp: timestamp, diff --git a/packages/utils/package.json b/packages/utils/package.json index 29743de6b..f77e2ece3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -19,6 +19,7 @@ "@cosmjs/encoding": "^0.32.3", "@cosmjs/stargate": "^0.32.3", "@cosmjs/tendermint-rpc": "^0.32.3", + "@dao-dao/math": "2.5.0-rc.3", "@dao-dao/types": "2.5.0-rc.3", "@sentry/nextjs": "^7.7.0", "@types/lodash.clonedeep": "^4.5.0", diff --git a/packages/utils/token.ts b/packages/utils/token.ts index 76f1f0d0c..3d98fa3e2 100644 --- a/packages/utils/token.ts +++ b/packages/utils/token.ts @@ -132,11 +132,15 @@ export const sortTokensValueDescending: SortFn< const aPrice = a.lazyInfo.loading || !a.lazyInfo.data.usdUnitPrice?.usdPrice ? undefined - : a.lazyInfo.data.totalBalance * a.lazyInfo.data.usdUnitPrice.usdPrice + : a.lazyInfo.data.totalBalance.times( + a.lazyInfo.data.usdUnitPrice.usdPrice + ) const bPrice = b.lazyInfo.loading || !b.lazyInfo.data.usdUnitPrice?.usdPrice ? undefined - : b.lazyInfo.data.totalBalance * b.lazyInfo.data.usdUnitPrice.usdPrice + : b.lazyInfo.data.totalBalance.times( + b.lazyInfo.data.usdUnitPrice.usdPrice + ) // If prices are equal, sort alphabetically by symbol. return aPrice === bPrice @@ -147,5 +151,45 @@ export const sortTokensValueDescending: SortFn< ? 1 : bPrice === undefined ? -1 - : bPrice - aPrice + : aPrice.eq(bPrice) + ? 0 + : aPrice.gt(bPrice) + ? -1 + : 1 +} + +/** + * Function to sort token lists ascending by USD value. + */ +export const sortTokensValueAscending: SortFn< + Pick<TokenCardInfo, 'token' | 'unstakedBalance' | 'lazyInfo'> +> = (a, b) => { + // If loading or no price, show at bottom. + const aPrice = + a.lazyInfo.loading || !a.lazyInfo.data.usdUnitPrice?.usdPrice + ? undefined + : a.lazyInfo.data.totalBalance.times( + a.lazyInfo.data.usdUnitPrice.usdPrice + ) + const bPrice = + b.lazyInfo.loading || !b.lazyInfo.data.usdUnitPrice?.usdPrice + ? undefined + : b.lazyInfo.data.totalBalance.times( + b.lazyInfo.data.usdUnitPrice.usdPrice + ) + + // If prices are equal, sort alphabetically by symbol. + return aPrice === bPrice + ? a.token.symbol + .toLocaleLowerCase() + .localeCompare(b.token.symbol.toLocaleLowerCase()) + : aPrice === undefined + ? 1 + : bPrice === undefined + ? -1 + : aPrice.eq(bPrice) + ? 0 + : aPrice.gt(bPrice) + ? 1 + : -1 } diff --git a/packages/utils/validation/index.ts b/packages/utils/validation/index.ts index 832af7e4f..92a678fd0 100644 --- a/packages/utils/validation/index.ts +++ b/packages/utils/validation/index.ts @@ -1,5 +1,7 @@ import JSON5 from 'json5' +import { HugeDecimal } from '@dao-dao/math' + import { isValidBech32Address, isValidTokenFactoryDenom, @@ -21,11 +23,11 @@ export const validateRequired = (v: any) => { return (v !== null && v !== undefined) || 'Field is required' } -export const validatePositive = (v: string | number | undefined) => - (v && !isNaN(Number(v)) && Number(v) > 0) || 'Must be positive' +export const validatePositive = (v: HugeDecimal.Value | undefined) => + (v !== undefined && HugeDecimal.from(v).isPositive()) || 'Must be positive' -export const validateNonNegative = (v: string | number) => - (!isNaN(Number(v)) && Number(v) >= 0) || 'Must be non-negative' +export const validateNonNegative = (v: HugeDecimal.Value | undefined) => + (v !== undefined && HugeDecimal.from(v).gte(0)) || 'Must be 0 or more' export const validatePercent = (v: string | number | undefined) => { const p = v ? Number(v) : NaN