diff --git a/apps/dapp/assets/icons/connect.svg b/apps/dapp/assets/icons/connect.svg deleted file mode 100644 index 9128d6da6..000000000 --- a/apps/dapp/assets/icons/connect.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/dapp/assets/icons/wallet.svg b/apps/dapp/assets/icons/wallet.svg deleted file mode 100644 index 55800008e..000000000 --- a/apps/dapp/assets/icons/wallet.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/dapp/atoms/localStorageEffect.ts b/apps/dapp/atoms/localStorageEffect.ts index 5dc13c470..c71588593 100644 --- a/apps/dapp/atoms/localStorageEffect.ts +++ b/apps/dapp/atoms/localStorageEffect.ts @@ -2,7 +2,7 @@ import { AtomEffect } from 'recoil' export const localStorageEffect: (key: string) => AtomEffect = (key) => - ({ setSelf, onSet, node }) => { + ({ setSelf, onSet, node: _ }) => { const savedValue = localStorage.getItem(key) if (savedValue != null) { const json = JSON.parse(savedValue) diff --git a/apps/dapp/atoms/proposals.ts b/apps/dapp/atoms/proposals.ts index 1b235cc77..37364e3fa 100644 --- a/apps/dapp/atoms/proposals.ts +++ b/apps/dapp/atoms/proposals.ts @@ -1,8 +1,6 @@ import { atom, atomFamily } from 'recoil' -import { ContractProposalMap, ExtendedProposalResponse } from 'types/proposals' - -import { localStorageEffect } from './localStorageEffect' +import { ProposalResponse } from '@dao-dao/types/contracts/cw3-dao' // By depending on this atom, the selector for retrieving the list // of on-chain proposals can know when it's time to actually re-calculate @@ -22,7 +20,7 @@ export const proposalsRequestStartBeforeAtom = atom({ }) // The loaded list of proposals. -export const proposalListAtom = atomFamily({ +export const proposalListAtom = atomFamily({ key: 'proposalList', default: [], }) @@ -54,21 +52,3 @@ export const proposalsUpdated = atomFamily({ key: 'proposalsUpdatedAtom', default: [], }) - -// The next numeric ID for creating draft proposals. Saved to localstorage -// and incremented so that we don't end up with multiple draft proposals and -// multisigs with the same ID. -export const nextDraftProposalIdAtom = atom({ - key: 'nextDraftProposalId', - default: 10000, - effects_UNSTABLE: [localStorageEffect('nextDraftProposalId')], -}) - -// The map of draft proposals associated with a given contract. -export const contractProposalMapAtom = atom({ - key: 'contractProposalMap', - default: {}, - effects_UNSTABLE: [ - localStorageEffect('contractProposalMap'), - ], -}) diff --git a/apps/dapp/components/BetaWarning.tsx b/apps/dapp/components/BetaWarning.tsx index fa7eba027..87f7c971c 100644 --- a/apps/dapp/components/BetaWarning.tsx +++ b/apps/dapp/components/BetaWarning.tsx @@ -1,9 +1,7 @@ +import { Message } from '@dao-dao/icons' +import { Button } from '@dao-dao/ui' import { ChevronRightIcon, XIcon } from '@heroicons/react/outline' -import { Button } from '@components' - -import SvgMessage from 'components/icons/Message' - export function BetaWarningModal({ onAccept }: { onAccept: Function }) { return (
@@ -50,7 +48,7 @@ export function BetaNotice({ onClose }: { onClose: Function }) { return (
- +

DAO DAO is in
beta!

diff --git a/apps/dapp/components/Breadcrumbs.tsx b/apps/dapp/components/Breadcrumbs.tsx deleted file mode 100644 index e0792c63c..000000000 --- a/apps/dapp/components/Breadcrumbs.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Link from 'next/link' - -import { ArrowNarrowLeftIcon } from '@heroicons/react/outline' - -/* - * Breadcrumb style navivation bar. Expects arguments in the form [[link, name], ...]. - */ -export function Breadcrumbs({ crumbs }: { crumbs: Array<[string, string]> }) { - return ( -
    -
  • - - - - - -
  • - {crumbs.map(([link, name], idx) => ( -
  • - - {name} - - {idx != crumbs.length - 1 && '/'} -
  • - ))} -
- ) -} diff --git a/apps/dapp/components/ChainEnableModal.tsx b/apps/dapp/components/ChainEnableModal.tsx index 5134b1d4c..478823e29 100644 --- a/apps/dapp/components/ChainEnableModal.tsx +++ b/apps/dapp/components/ChainEnableModal.tsx @@ -1,7 +1,6 @@ +import { Button, Modal } from '@dao-dao/ui' import { XIcon } from '@heroicons/react/outline' -import { Button } from 'ui/Button' -import { Modal } from './Modal' const CHAIN_ID = process.env.NEXT_PUBLIC_CHAIN_ID const CHAIN_NAME = process.env.NEXT_PUBLIC_CHAIN_NAME diff --git a/apps/dapp/components/Claims.tsx b/apps/dapp/components/Claims.tsx index 7c09a3931..ace7d03a2 100644 --- a/apps/dapp/components/Claims.tsx +++ b/apps/dapp/components/Claims.tsx @@ -1,115 +1,14 @@ -import { MouseEventHandler } from 'react' -import { useState } from 'react' -import { useEffect } from 'react' - import { useRecoilValue } from 'recoil' -import { Duration } from '@dao-dao/types/contracts/cw3-dao' -import { Claim, TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20' -import { CheckIcon } from '@heroicons/react/outline' +import { TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20' +import { + ClaimsListItem, + ClaimsAvaliableCard as StatelessClaimsAvaliableCard, +} from '@dao-dao/ui' +import { claimAvaliable } from '@dao-dao/utils' import { unstakingDuration } from 'selectors/daos' import { getBlockHeight, walletClaims } from 'selectors/treasury' -import { - convertMicroDenomToDenomWithDecimals, - humanReadableDuration, -} from 'util/conversion' - -import { LogoNoBorder } from './Logo' - -export function claimAvaliable(claim: Claim, blockHeight: number) { - if ('at_height' in claim.release_at) { - return blockHeight >= claim.release_at.at_height - } else if ('at_time' in claim.release_at) { - const currentTimeNs = new Date().getTime() * 1000000 - return currentTimeNs >= Number(claim.release_at.at_time) - } - - // Unreachable. - return true -} - -function claimDurationRemaining(claim: Claim, blockHeight: number): Duration { - if (claimAvaliable(claim, blockHeight)) { - return { time: 0 } - } - if ('at_height' in claim.release_at) { - const releaseBlock = claim.release_at.at_height - return { height: releaseBlock - blockHeight } - } else if ('at_time' in claim.release_at) { - const currentTimeNs = new Date().getTime() * 1000000 - return { - time: - (Number(claim.release_at.at_time) - currentTimeNs) / 1000000000 || 0, // To seconds. - } - } - - // Unreachable. - return { time: 0 } -} - -function ClaimListItem({ - claim, - unstakingDuration, - blockHeight, - tokenInfo, - incrementClaimsAvaliable, -}: { - claim: Claim - unstakingDuration: Duration - blockHeight: number - tokenInfo: TokenInfoResponse - incrementClaimsAvaliable: (n: number) => void -}) { - const avaliable = claimAvaliable(claim, blockHeight) - - const durationForHumans = humanReadableDuration(unstakingDuration) - const durationRemaining = claimDurationRemaining(claim, blockHeight) - - // Once the claim expires increment claims avaliable. - useEffect(() => { - if ('time' in durationRemaining) { - const id = setTimeout( - () => incrementClaimsAvaliable(Number(claim.amount)), - durationRemaining.time * 1000 - ) - return () => clearTimeout(id) - } - }, [claim.amount, durationRemaining, incrementClaimsAvaliable]) - - const [durationRemainingForHumans, setDurationRemainingForHumans] = useState( - humanReadableDuration(durationRemaining) - ) - - useEffect(() => { - const id = setInterval(() => { - setDurationRemainingForHumans((_) => - humanReadableDuration(claimDurationRemaining(claim, blockHeight)) - ) - }, 1000) - return () => clearInterval(id) - }, [claim, blockHeight, setDurationRemainingForHumans]) - - return ( -
- {avaliable ? ( -

- Avaliable - -

- ) : ( -
-

{durationRemainingForHumans || '0'} left

-

/ {durationForHumans}

-
- )} -

- {convertMicroDenomToDenomWithDecimals(claim.amount, tokenInfo.decimals)} - ${tokenInfo.symbol} -

-
- ) -} export function ClaimsPendingList({ stakingAddress, @@ -134,7 +33,7 @@ export function ClaimsPendingList({
    {claimsPending.map((claim, idx) => { return ( - + onClaim: () => void loading: boolean }) { const blockHeight = useRecoilValue(getBlockHeight) @@ -168,31 +67,11 @@ export function ClaimAvaliableCard({ .reduce((p, n) => p + Number(n.amount), 0) return ( -
    -

    - Unclaimed (unstaked ${tokenInfo.symbol}) -

    - {loading ? ( -
    - -
    - ) : ( -

    - {convertMicroDenomToDenomWithDecimals( - claimsAvaliable, - tokenInfo.decimals - )} - ${tokenInfo.symbol} -

    - )} -
    - -
    -
    + ) } diff --git a/apps/dapp/components/CodeIdSelect.tsx b/apps/dapp/components/CodeIdSelect.tsx index f29f2f8ee..61e5a7964 100644 --- a/apps/dapp/components/CodeIdSelect.tsx +++ b/apps/dapp/components/CodeIdSelect.tsx @@ -1,7 +1,6 @@ +import { Button } from '@dao-dao/ui' import { ChevronDownIcon } from '@heroicons/react/outline' -import { Button } from '@components' - export interface ContractVersion { name: string codeId: number diff --git a/apps/dapp/components/ConnectWalletButton.tsx b/apps/dapp/components/ConnectWalletButton.tsx index 634171717..037812a9d 100644 --- a/apps/dapp/components/ConnectWalletButton.tsx +++ b/apps/dapp/components/ConnectWalletButton.tsx @@ -1,11 +1,15 @@ -import { useCallback, useState } from 'react' +import { useCallback } from 'react' import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil' -import { CheckCircleIcon, LogoutIcon } from '@heroicons/react/outline' -import Tooltip from '@reach/tooltip' - -import { Button } from '@components' +import { WalletConnect as StatelessWalletConnect } from '@dao-dao/ui' +import { + CHAIN_ID, + NATIVE_DECIMALS, + NATIVE_DENOM, + convertDenomToHumanReadableDenom, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' import { connectedWalletAtom, @@ -18,46 +22,6 @@ import { noKeplrAccountAtom, } from 'selectors/cosm' import { connectKeplrWithoutAlerts } from 'services/keplr' -import { CHAIN_ID, NATIVE_DECIMALS, NATIVE_DENOM } from 'util/constants' -import { - convertDenomToHumanReadableDenom, - convertMicroDenomToDenomWithDecimals, -} from 'util/conversion' - -import SvgCopy from './icons/Copy' -import SvgWallet from './icons/Wallet' - -function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false) - return ( - - - - ) -} - -function DisconnectButton({ onClick }: { onClick: () => void }) { - return ( - - - - ) -} function WalletConnect() { const [wallet, setWallet] = useRecoilState(connectedWalletAtom) @@ -80,6 +44,7 @@ function WalletConnect() { setInstallWarningVisible(true) } else { try { + console.log(CHAIN_ID) await connectKeplrWithoutAlerts() await (window as any).keplr.enable(CHAIN_ID) setInstallWarningVisible(false) @@ -106,37 +71,14 @@ function WalletConnect() { setWallet, ]) - if (walletAddress) { - return ( -
    -
    - -
    - {walletName} -
    - - {walletBalanceHuman} {chainDenomHuman} - -
    -
    -
    - - -
    -
    - ) - } return ( -
    - -
    + ) } diff --git a/apps/dapp/components/ContractCard.tsx b/apps/dapp/components/ContractCard.tsx index 72cdf77b7..a9421c0ad 100644 --- a/apps/dapp/components/ContractCard.tsx +++ b/apps/dapp/components/ContractCard.tsx @@ -2,25 +2,19 @@ import { ReactNode } from 'react' import Link from 'next/link' -import { PlusIcon, StarIcon as StarIconOutline } from '@heroicons/react/outline' -import { StarIcon as StarIconSolid } from '@heroicons/react/solid' - -import { - convertDenomToHumanReadableDenom, - convertMicroDenomToDenomWithDecimals, -} from 'util/conversion' - +import { Dao, Pencil, Votes } from '@dao-dao/icons' +import { Logo } from '@dao-dao/ui' import { CARD_IMAGES_ENABLED, NATIVE_DECIMALS, NATIVE_DENOM, -} from '../util/constants' -import SvgDao from './icons/Dao' -import SvgPencil from './icons/Pencil' -import SvgVotes from './icons/Votes' -import { Logo } from './Logo' + convertDenomToHumanReadableDenom, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' +import { StarIcon as StarIconOutline } from '@heroicons/react/outline' +import { StarIcon as StarIconSolid } from '@heroicons/react/solid' -function DIYLogo({ +function ContractCardBase({ title, body, href, @@ -42,7 +36,7 @@ function DIYLogo({ return ( -
    +
    @@ -68,7 +62,7 @@ function DIYLogo({
    {balance && (

    - + {convertMicroDenomToDenomWithDecimals( balance, NATIVE_DECIMALS @@ -78,19 +72,13 @@ function DIYLogo({ )} {proposals != undefined && (

    - + {proposals} proposal{weight != 1 && 's'}

    )} {weight != undefined && (

    - + {weight} vote{weight != 1 && 's'}

    )} @@ -124,7 +112,7 @@ export function ContractCard({ }) { return (
    - )} - +
    ) } - -const EmptyStateContractCard = ({ - title, - description, - backgroundUrl, - href, -}: { - title: string - description: string - backgroundUrl: string - href: string -}) => { - return ( - -
    -
    -
    -
    - - {title} -
    -
    {description}
    -
    -
    - - ) -} diff --git a/apps/dapp/components/ContractView.tsx b/apps/dapp/components/ContractView.tsx index 988f66359..712da29b2 100644 --- a/apps/dapp/components/ContractView.tsx +++ b/apps/dapp/components/ContractView.tsx @@ -1,17 +1,14 @@ -import { Children, MouseEventHandler, ReactNode } from 'react' - import Link from 'next/link' import { useRecoilValue, waitForAll } from 'recoil' -import { StarIcon as StarOutline } from '@heroicons/react/outline' -import { StarIcon as StarSolid } from '@heroicons/react/solid' -import Tooltip from '@reach/tooltip' -import { useThemeContext } from 'ui' - -import { Button } from '@components' +import { + Button, + TreasuryBalances as StatelessTreasuryBalances, + Tooltip, +} from '@dao-dao/ui' +import { NATIVE_DECIMALS, nativeTokenDecimals } from '@dao-dao/utils' -import { contractInstantiateTime } from 'selectors/contracts' import { isMemberSelector } from 'selectors/cosm' import { cw20Balances, @@ -20,238 +17,34 @@ import { walletAddress, walletTokenBalanceLoading, } from 'selectors/treasury' -import { NATIVE_DECIMALS, HEADER_IMAGES_ENABLED } from 'util/constants' -import { - convertDenomToHumanReadableDenom, - convertMicroDenomToDenomWithDecimals, - nativeTokenLabel, - nativeTokenLogoURI, -} from 'util/conversion' -import { Logo, LogoNoBorder } from './Logo' import { ProposalList } from './ProposalList' -export function GradientHero({ children }: { children: ReactNode }) { - const theme = useThemeContext() - const endStop = theme.theme === 'dark' ? '#111213' : '#FFFFFF' - const baseRgb = theme.accentColor - ? theme.accentColor.split('(')[1].split(')')[0] - : '73, 55, 192' - return ( -
    -
    - {children} -
    -
    - ) -} - -export function StarButton({ - pinned, - onPin, -}: { - pinned: boolean - onPin: Function -}) { - const { accentColor } = useThemeContext() - - return ( - - ) -} - -export function EstablishedDate({ address }: { address: string }) { - const instantiateDate = useRecoilValue(contractInstantiateTime(address)) - const formattedDate = instantiateDate.toLocaleDateString(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric', - }) - return

    Est. {formattedDate}

    -} - -export function HeroContractHeader({ - name, - address, - description, - imgUrl, -}: { - name: string - address: string - description: string - imgUrl?: string | null -}) { - return ( -
    - {imgUrl && HEADER_IMAGES_ENABLED ? ( -
    - ) : ( - - )} -
    -

    {name}

    - -
    -
    -

    {description}

    -
    -
    - ) -} - -export function HeroContractHorizontalInfoSection({ - children, -}: { - children: ReactNode -}) { - return ( -
    - {children} -
    - ) -} - -export function HeroContractHorizontalInfo({ - children, -}: { - children: ReactNode -}) { - const childList = Children.toArray(children) - return ( -
    -
      - {Children.map(childList, (child) => ( -
    • {child}
    • - ))} -
    -
    - ) -} - -export function GovInfoListItem({ - icon, - text, - value, -}: { - icon: ReactNode - text: string - value: string -}) { - return ( -
  • - - {icon} {text}: - - {value} -
  • - ) -} - -export function BalanceIcon({ iconURI }: { iconURI?: string }) { - const { accentColor } = useThemeContext() - - return ( -
    - ) -} - -export function BalanceListItem({ children }: { children: ReactNode }) { - return ( -
  • - {children} -
  • - ) -} - export function TreasuryBalances({ address }: { address: string }) { const nativeBalances = useRecoilValue(nativeBalance(address)) - const cw20List = useRecoilValue(cw20Balances(address)) + const cw20List = useRecoilValue(cw20Balances(address)) const cw20Info = useRecoilValue( waitForAll(cw20List.map(({ address }) => cw20TokenInfo(address))) ) - const cw20InfoBalance = cw20Info.map((info, idx) => ({ - info: info, + + const cw20Tokens = cw20Info.map((info, idx) => ({ + symbol: info.symbol, amount: cw20List[idx].amount, + decimals: info.decimals, + })) + + const nativeTokens = nativeBalances.map(({ denom, amount }) => ({ + denom: denom, + amount, + decimals: nativeTokenDecimals(denom) || NATIVE_DECIMALS, })) return ( -
      - {nativeBalances.map((coin, idx) => { - const symbol = nativeTokenLabel(coin.denom) - const icon = nativeTokenLogoURI(coin.denom) - return ( - - - {convertMicroDenomToDenomWithDecimals( - coin.amount, - NATIVE_DECIMALS - ).toLocaleString(undefined, { - maximumFractionDigits: 20, - })}{' '} - ${symbol} - - ) - })} - {!nativeBalances.length && ( - - 0 $ - {convertDenomToHumanReadableDenom( - process.env.NEXT_PUBLIC_STAKING_DENOM as string - ).toUpperCase()} - - )} - {cw20InfoBalance.map(({ info, amount }) => { - return ( - - - {convertMicroDenomToDenomWithDecimals( - amount, - info.decimals - ).toLocaleString(undefined, { - maximumFractionDigits: 20, - })}{' '} - ${info.symbol} - - ) - })} -
    + ) } @@ -295,38 +88,3 @@ export function ContractProposalsDispaly({ ) } - -export function BalanceCard({ - denom, - title, - amount, - onManage, - loading, -}: { - denom: string - title: string - amount: string - onManage: MouseEventHandler - loading: boolean -}) { - return ( -
    -

    {title}

    - {loading ? ( -
    - -
    - ) : ( -
    - - {amount} ${denom} -
    - )} -
    - -
    -
    - ) -} diff --git a/apps/dapp/components/DaoContractInfo.tsx b/apps/dapp/components/DaoContractInfo.tsx index dc68eec36..11d1e7a7b 100644 --- a/apps/dapp/components/DaoContractInfo.tsx +++ b/apps/dapp/components/DaoContractInfo.tsx @@ -1,5 +1,12 @@ import { useRecoilValue } from 'recoil' +import { Votes } from '@dao-dao/icons' +import { CopyToClipboardAccent, GovInfoListItem } from '@dao-dao/ui' +import { + humanReadableDuration, + convertMicroDenomToDenomWithDecimals, + getThresholdAndQuorumDisplay, +} from '@dao-dao/utils' import { CashIcon, ChartPieIcon } from '@heroicons/react/outline' import { @@ -7,16 +14,8 @@ import { tokenConfig, unstakingDuration as unstakingDurationSelector, } from 'selectors/daos' -import { - humanReadableDuration, - convertMicroDenomToDenomWithDecimals, - getThresholdAndQuorumDisplay, -} from 'util/conversion' -import { GovInfoListItem } from './ContractView' -import { CopyToClipboardAccent } from './CopyToClipboard' import { DaoTreasury } from './DaoTreasury' -import SvgVotes from './icons/Votes' export function DaoContractInfo({ address }: { address: string }) { const daoInfo = useRecoilValue(daoSelector(address)) @@ -43,13 +42,13 @@ export function DaoContractInfo({ address }: { address: string }) { value={humanReadableDuration(unstakingDuration)} /> } + icon={} text="Passing threshold" value={threshold} /> {quorum && ( } + icon={} text="Quorum" value={quorum} /> @@ -61,7 +60,7 @@ export function DaoContractInfo({ address }: { address: string }) { />
  • - {' '} + {' '} {convertMicroDenomToDenomWithDecimals( daoInfo.config.proposal_deposit, govTokenInfo.decimals diff --git a/apps/dapp/components/DaoTreasury.tsx b/apps/dapp/components/DaoTreasury.tsx index 58ce1fd73..a05495635 100644 --- a/apps/dapp/components/DaoTreasury.tsx +++ b/apps/dapp/components/DaoTreasury.tsx @@ -4,10 +4,9 @@ import { useRouter } from 'next/router' import { useRecoilValue } from 'recoil' +import { Button } from '@dao-dao/ui' import { PlusSmIcon } from '@heroicons/react/outline' -import { Button } from '@components' - import { daoSelector } from 'selectors/daos' import { addToken } from 'util/addToken' diff --git a/apps/dapp/components/HomepageLayout.tsx b/apps/dapp/components/HomepageLayout.tsx index 7bc39152f..18d3de2d0 100644 --- a/apps/dapp/components/HomepageLayout.tsx +++ b/apps/dapp/components/HomepageLayout.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react' import Head from 'next/head' -import { SITE_TITLE } from 'util/constants' +import { SITE_TITLE } from '@dao-dao/utils' export function HomepageLayout({ children }: { children: ReactNode }) { return ( diff --git a/apps/dapp/components/InstallKeplr.tsx b/apps/dapp/components/InstallKeplr.tsx index 5883f74e1..0d1a5b44a 100644 --- a/apps/dapp/components/InstallKeplr.tsx +++ b/apps/dapp/components/InstallKeplr.tsx @@ -1,7 +1,5 @@ +import { Button, Modal } from '@dao-dao/ui' import { ChevronRightIcon, XIcon } from '@heroicons/react/outline' -import { Button } from 'ui/Button' - -import { Modal } from './Modal' export function InstallKeplr({ onClose }: { onClose: () => void }) { return ( diff --git a/apps/dapp/components/Layout.tsx b/apps/dapp/components/Layout.tsx index 13e71f693..68f11aa7e 100644 --- a/apps/dapp/components/Layout.tsx +++ b/apps/dapp/components/Layout.tsx @@ -8,10 +8,11 @@ import { useSetRecoilState, } from 'recoil' +import { LoadingScreen } from '@dao-dao/ui' +import { SITE_TITLE } from '@dao-dao/utils' import { Keplr } from '@keplr-wallet/types' import { betaWarningAcceptedAtom, showBetaNoticeAtom } from 'atoms/status' -import LoadingScreen from 'components/LoadingScreen' import { SidebarLayout } from 'components/SidebarLayout' import { kelprOfflineSigner, @@ -25,7 +26,6 @@ import { } from 'selectors/cosm' import { getKeplr, connectKeplrWithoutAlerts } from 'services/keplr' -import { SITE_TITLE } from '../util/constants' import { BetaNotice, BetaWarningModal } from './BetaWarning' import ChainEnableModal from './ChainEnableModal' import { InstallKeplr } from './InstallKeplr' diff --git a/apps/dapp/components/MarkdownPreview.tsx b/apps/dapp/components/MarkdownPreview.tsx deleted file mode 100644 index 30a0e18d7..000000000 --- a/apps/dapp/components/MarkdownPreview.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ReactMarkdown from 'react-markdown' - -export function MarkdownPreview({ markdown }: { markdown: string }) { - return ( - - {markdown} - - ) -} diff --git a/apps/dapp/components/Modal.tsx b/apps/dapp/components/Modal.tsx deleted file mode 100644 index e451a17f9..000000000 --- a/apps/dapp/components/Modal.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ReactNode } from 'react' - -export function Modal({ children }: { children: ReactNode }) { - { - return ( -
    - {children} -
    - ) - } -} diff --git a/apps/dapp/components/MultisigContractInfo.tsx b/apps/dapp/components/MultisigContractInfo.tsx index 603a7c00c..8db20f24a 100644 --- a/apps/dapp/components/MultisigContractInfo.tsx +++ b/apps/dapp/components/MultisigContractInfo.tsx @@ -1,14 +1,13 @@ import { useRecoilValue } from 'recoil' +import { Votes } from '@dao-dao/icons' +import { CopyToClipboardAccent, GovInfoListItem } from '@dao-dao/ui' +import { humanReadableDuration, thresholdString } from '@dao-dao/utils' import { ClockIcon } from '@heroicons/react/outline' import { sigSelector } from 'selectors/multisigs' -import { humanReadableDuration, thresholdString } from 'util/conversion' -import { GovInfoListItem } from './ContractView' -import { CopyToClipboardAccent } from './CopyToClipboard' import { DaoTreasury } from './DaoTreasury' -import SvgVotes from './icons/Votes' export function MultisigContractInfo({ address }: { address: string }) { const sigInfo = useRecoilValue(sigSelector(address)) @@ -19,7 +18,7 @@ export function MultisigContractInfo({ address }: { address: string }) {

    Governance Details

      } + icon={} text="Passing threshold" value={thresholdString(sigInfo.config.threshold, true, 0)} /> diff --git a/apps/dapp/components/Nav.tsx b/apps/dapp/components/Nav.tsx index 68ea7a942..6fec18114 100644 --- a/apps/dapp/components/Nav.tsx +++ b/apps/dapp/components/Nav.tsx @@ -2,6 +2,8 @@ import Link from 'next/link' import { useRecoilValue, waitForAll } from 'recoil' +import { Logo } from '@dao-dao/ui' +import { SITE_TITLE } from '@dao-dao/utils' import { ArrowRightIcon, ExternalLinkIcon, @@ -11,12 +13,10 @@ import { MenuIcon } from '@heroicons/react/outline' import { pinnedDaosAtom, pinnedMultisigsAtom } from 'atoms/pinned' import { showBetaNoticeAtom } from 'atoms/status' -import { Logo } from 'components/Logo' import ThemeToggle from 'components/ThemeToggle' import { daoSelector } from 'selectors/daos' import { sigSelector } from 'selectors/multisigs' -import { SITE_TITLE } from '../util/constants' import ConnectWalletButton from './ConnectWalletButton' function MemberDisplay({ name }: { name: string }) { diff --git a/apps/dapp/components/NoKeplrAccountModal.tsx b/apps/dapp/components/NoKeplrAccountModal.tsx index 822f0447e..489e27b97 100644 --- a/apps/dapp/components/NoKeplrAccountModal.tsx +++ b/apps/dapp/components/NoKeplrAccountModal.tsx @@ -1,7 +1,5 @@ +import { Button, Modal } from '@dao-dao/ui' import { ChevronRightIcon, XIcon } from '@heroicons/react/outline' -import { Button } from 'ui/Button' - -import { Modal } from './Modal' export function NoKeplrAccountModal({ onClose }: { onClose: () => void }) { return ( diff --git a/apps/dapp/components/Paginator.tsx b/apps/dapp/components/Paginator.tsx index 9937bc6a8..f62302873 100644 --- a/apps/dapp/components/Paginator.tsx +++ b/apps/dapp/components/Paginator.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' -import { Button } from 'ui' +import { Button } from '@dao-dao/ui' function Paginator({ page, diff --git a/apps/dapp/components/ProposalDetails.tsx b/apps/dapp/components/ProposalDetails.tsx index a21f2b1c7..3d56d76a7 100644 --- a/apps/dapp/components/ProposalDetails.tsx +++ b/apps/dapp/components/ProposalDetails.tsx @@ -1,18 +1,20 @@ -import { ReactNode, useState } from 'react' +import { useState } from 'react' import { useRouter } from 'next/router' import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil' import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' -import { CosmosMsgFor_Empty } from '@dao-dao/types/contracts/cw3-dao' -import { EyeIcon, EyeOffIcon } from '@heroicons/react/outline' +import { + StakingMode, + ProposalDetails as StatelessProposalDetails, + CosmosMessageDisplay, +} from '@dao-dao/ui' +import { VoteChoice } from '@dao-dao/ui' import { FormProvider, useForm } from 'react-hook-form' import toast from 'react-hot-toast' -import { Button } from 'ui' import { proposalUpdateCountAtom, proposalsUpdated } from 'atoms/proposals' -import { MarkdownPreview } from 'components/MarkdownPreview' import { cosmWasmSigningClient, walletAddress as walletAddressSelector, @@ -26,17 +28,12 @@ import { import { walletTokenBalanceLoading } from 'selectors/treasury' import { FromCosmosMsgProps, - MessageTemplate, messageTemplateAndValuesForDecodedCosmosMsg, } from 'templates/templateList' import { cleanChainError } from 'util/cleanChainError' -import { decodedMessagesString, decodeMessages } from 'util/messagehelpers' import { treasuryTokenListUpdates } from '../atoms/treasury' -import { CosmosMessageDisplay } from './CosmosMessageDisplay' -import { Execute } from './Execute' -import { StakingModal, StakingMode } from './StakingModal' -import { Vote, VoteChoice } from './Vote' +import { StakingModal } from './StakingModal' function executeProposalVote( choice: VoteChoice, @@ -124,77 +121,6 @@ function executeProposalExecute( }) } -interface ProposalMessageTemplateListItemProps { - template: MessageTemplate - values: any - contractAddress: string - multisig?: boolean -} - -function ProposalMessageTemplateListItem({ - template, - values, - contractAddress, - multisig, -}: ProposalMessageTemplateListItemProps) { - const formMethods = useForm({ - defaultValues: values, - }) - - return ( - -
      - field} - multisig={multisig} - readOnly - /> - -
      - ) -} - -interface ProposalMessageTemplateListProps { - msgs: CosmosMsgFor_Empty[] - contractAddress: string - multisig?: boolean - fromCosmosMsgProps: FromCosmosMsgProps -} - -function ProposalMessageTemplateList({ - msgs, - contractAddress, - multisig, - fromCosmosMsgProps, -}: ProposalMessageTemplateListProps) { - const components: ReactNode[] = msgs.map((msg, index) => { - const decoded = decodeMessages([msg])[0] - const data = messageTemplateAndValuesForDecodedCosmosMsg( - decoded, - fromCosmosMsgProps - ) - - return data ? ( - - ) : ( - // If no message template found, render raw message. - - ) - }) - - return <>{components} -} - export function ProposalDetails({ contractAddress, proposalId, @@ -251,139 +177,91 @@ export function ProposalDetails({ const weightPercent = (votingPower / totalPower) * 100 const [loading, setLoading] = useState(false) - - const [showRaw, setShowRaw] = useState(false) - const [showStaking, setShowStakng] = useState(false) + const [showStaking, setShowStaking] = useState(false) if (!proposal) { router.replace(`/${multisig ? 'multisig' : 'dao'}/${contractAddress}`) return
      Error
      } - const decodedMessages = decodeMessages(proposal.msgs) - return ( -
      -
      -

      {proposal.title}

      -
      -
      - -
      -

      Messages

      -
      - {decodedMessages?.length ? ( - showRaw ? ( - - ) : ( - - ) - ) : ( -
      []
      - )} -
      - {!!decodedMessages.length && ( -
      - -
      - )} - {proposal.status === 'passed' && ( - <> -

      Status

      - - executeProposalExecute( - proposalId, - contractAddress, - signingClient, - wallet, - () => { - setProposalUpdates((n) => n + 1) - setProposalsUpdated((p) => - p.includes(proposalId) ? p : p.concat([proposalId]) - ) - setTreasuryTokenListUpdates((n) => n + 1) - }, - setLoading - ) - } - /> - - )} -

      Vote

      - {proposal.status === 'open' && !walletVote && votingPower !== 0 && ( - - executeProposalVote( - position, - proposalId, - contractAddress, - signingClient, - wallet, - () => { - setProposalUpdates((n) => n + 1) - setProposalsUpdated((p) => - p.includes(proposalId) ? p : p.concat([proposalId]) - ) - }, - setLoading + { + const data = messageTemplateAndValuesForDecodedCosmosMsg( + message, + fromCosmosMsgProps + ) + if (data) { + const ThisIsAComponentBecauseReactIsAnnoying = () => { + // Can't call `useForm` in a callback. + const formMethods = useForm({ defaultValues: data.values }) + return ( + +
      + field} + multisig={multisig} + readOnly + /> + +
      ) } - voterWeight={weightPercent} + return + } + return ( + + ) + }} + onExecute={() => + executeProposalExecute( + proposalId, + contractAddress, + signingClient, + wallet, + () => { + setProposalUpdates((n) => n + 1) + setProposalsUpdated((p) => + p.includes(proposalId) ? p : p.concat([proposalId]) + ) + setTreasuryTokenListUpdates((n) => n + 1) + }, + setLoading + ) + } + onVote={(choice) => + executeProposalVote( + choice, + proposalId, + contractAddress, + signingClient, + wallet, + () => { + setProposalUpdates((n) => n + 1) + setProposalsUpdated((p) => + p.includes(proposalId) ? p : p.concat([proposalId]) + ) + }, + setLoading + ) + } + proposal={proposal} + setShowStaking={(s) => setShowStaking(s)} + showStaking={showStaking} + stakingModal={ + setTokenBalancesLoading(false)} + beforeExecute={() => setTokenBalancesLoading(true)} + claimableTokens={0} + contractAddress={contractAddress} + defaultMode={StakingMode.Stake} + onClose={() => setShowStaking(false)} /> - )} - {walletVote && ( -

      You voted {walletVote} on this proposal.

      - )} - {proposal.status !== 'open' && !walletVote && ( -

      You did not vote on this proposal.

      - )} - {votingPower === 0 && ( -

      - You must have voting power at the time of proposal creation to vote.{' '} - {!multisig && ( - - )} - {!multisig && showStaking && ( - setTokenBalancesLoading(false)} - beforeExecute={() => setTokenBalancesLoading(true)} - claimAmount={0} - contractAddress={contractAddress} - defaultMode={StakingMode.Stake} - onClose={() => setShowStakng(false)} - /> - )} -

      - )} -
      + } + walletVote={walletVote} + walletWeightPercent={weightPercent} + /> ) } diff --git a/apps/dapp/components/ProposalDetailsSidebar.tsx b/apps/dapp/components/ProposalDetailsSidebar.tsx index b0a29d9d7..5ae83427a 100644 --- a/apps/dapp/components/ProposalDetailsSidebar.tsx +++ b/apps/dapp/components/ProposalDetailsSidebar.tsx @@ -1,7 +1,10 @@ import { useRecoilValue, useRecoilValueLoadable } from 'recoil' -import { ExternalLinkIcon, CheckIcon, XIcon } from '@heroicons/react/outline' -import Tooltip from '@reach/tooltip' +import { + ProposalDetailsVoteStatus as StatelessProposalDetailsVoteStatus, + ProposalDetailsCard as StatelessProposalDetailsCard, + WalletVote as StatelessWalletVote, +} from '@dao-dao/ui' import { proposalSelector, @@ -10,39 +13,13 @@ import { walletVoteSelector, proposalStartBlockSelector, votingPowerAtHeightSelector, - WalletVote, } from 'selectors/proposals' -import { CHAIN_TXN_URL_PREFIX } from 'util/constants' import { contractConfigSelector, ContractConfigWrapper, } from 'util/contractConfigWrapper' -import { - convertMicroDenomToDenomWithDecimals, - expirationAtTimeToSecondsFromNow, - secondsToWdhms, -} from 'util/conversion' import { useThresholdQuorum } from 'util/proposal' -import { CopyToClipboard } from './CopyToClipboard' -import SvgAbstain from './icons/Abstain' -import { TriangleUp } from './icons/TriangleUp' -import { Progress } from './Progress' -import { ProposalStatus } from './ProposalStatus' - -const YouTooltip = ({ label }: { label: string }) => ( - -

      - ? -

      -
      -) - -const PASSING_THRESHOLD_TOOLTIP = - "A proposal must attain this proportion of 'Yes' votes to pass." -const QUORUM_TOOLTIP = - 'This proportion of voting weight must vote for a proposal to pass.' - interface ProposalDetailsProps { contractAddress: string multisig: boolean @@ -51,18 +28,12 @@ interface ProposalDetailsProps { export const ProposalDetailsCard = ({ contractAddress, - multisig, proposalId, + multisig, }: ProposalDetailsProps) => { const proposal = useRecoilValue( proposalSelector({ contractAddress, proposalId }) ) - const { state: proposalExecutionTXHashState, contents: txHashContents } = - useRecoilValueLoadable( - proposalExecutionTXHashSelector({ contractAddress, proposalId }) - ) - const proposalExecutionTXHash: string | null = - proposalExecutionTXHashState === 'hasValue' ? txHashContents : null const walletVote = useRecoilValue( walletVoteSelector({ contractAddress, proposalId }) @@ -80,115 +51,35 @@ export const ProposalDetailsCard = ({ ) const memberWhenProposalCreated = votingPower > 0 + const { state: proposalExecutionTXHashState, contents: txHashContents } = + useRecoilValueLoadable( + proposalExecutionTXHashSelector({ contractAddress, proposalId }) + ) + const proposalExecutionTXHash: string | undefined = + proposalExecutionTXHashState === 'hasValue' ? txHashContents : undefined + if (!proposal) { - return null + return ( +
      +

      Could not find proposal.

      +
      + ) } return ( -
      -
      -
      -

      - Proposal -

      - -

      - # {proposal.id.toString().padStart(6, '0')} -

      -
      - -
      - -
      -

      - Status -

      - -

      - -

      -
      - -
      - -
      -

      - You -

      - - {!memberWhenProposalCreated ? ( - - ) : walletVote === WalletVote.Yes ? ( -

      - Yes -

      - ) : walletVote === WalletVote.No ? ( -

      - No -

      - ) : walletVote === WalletVote.Abstain ? ( -

      - Abstain -

      - ) : walletVote === WalletVote.Veto ? ( -

      - Veto -

      - ) : walletVote ? ( -

      - Unknown: {walletVote} -

      - ) : proposal.status === 'open' ? ( - - ) : ( - - )} -
      -
      - -
      -

      Proposer

      - - - - {proposal.status === 'executed' && - proposalExecutionTXHashState === 'loading' ? ( - <> -

      TX

      -

      Loading...

      - - ) : !!proposalExecutionTXHash ? ( - <> - {CHAIN_TXN_URL_PREFIX ? ( - - TX - - - ) : ( -

      TX

      - )} - - - - ) : null} -
      -
      + ) } export const ProposalDetailsVoteStatus = ({ contractAddress, - multisig, proposalId, + multisig, }: ProposalDetailsProps) => { const proposal = useRecoilValue( proposalSelector({ contractAddress, proposalId }) @@ -210,469 +101,24 @@ export const ProposalDetailsVoteStatus = ({ const configWrapper = new ContractConfigWrapper(config) const tokenDecimals = configWrapper.gov_token_decimals - const localeOptions = { maximumSignificantDigits: 3 } - - const yesVotes = Number( - multisig - ? proposalTally.votes.yes - : convertMicroDenomToDenomWithDecimals( - proposalTally.votes.yes, - tokenDecimals - ) - ) - const noVotes = Number( - multisig - ? proposalTally.votes.no - : convertMicroDenomToDenomWithDecimals( - proposalTally.votes.no, - tokenDecimals - ) - ) - const abstainVotes = Number( - multisig - ? proposalTally.votes.abstain - : convertMicroDenomToDenomWithDecimals( - proposalTally.votes.abstain, - tokenDecimals - ) - ) - - const totalWeight = Number( - multisig - ? proposalTally.total_weight - : convertMicroDenomToDenomWithDecimals( - proposalTally.total_weight, - tokenDecimals - ) - ) - - const turnoutTotal = yesVotes + noVotes + abstainVotes - const turnoutYesPercent = turnoutTotal ? (yesVotes / turnoutTotal) * 100 : 0 - const turnoutNoPercent = turnoutTotal ? (noVotes / turnoutTotal) * 100 : 0 - const turnoutAbstainPercent = turnoutTotal - ? (abstainVotes / turnoutTotal) * 100 - : 0 - - const turnoutPercent = (turnoutTotal / totalWeight) * 100 - const totalYesPercent = (yesVotes / totalWeight) * 100 - const totalNoPercent = (noVotes / totalWeight) * 100 - const totalAbstainPercent = (abstainVotes / totalWeight) * 100 - - if (!proposal) { - return null - } - const maxVotingSeconds = 'time' in config.config.max_voting_period ? config.config.max_voting_period.time : undefined - const expiresInSeconds = - proposal.expires && 'at_time' in proposal.expires - ? expirationAtTimeToSecondsFromNow(proposal.expires) - : undefined - // TODO: Change this wen v1 contracts launch, since the conditions - // will change. In v1, all abstain fails instead of passes. - const thresholdReached = - !!threshold && - yesVotes >= - ((quorum ? turnoutTotal : totalWeight) - abstainVotes) * - (threshold.percent / 100) - const quorumMet = !!quorum && turnoutPercent >= quorum.percent - - const helpfulStatusText = - proposal.status === 'open' && threshold && quorum - ? thresholdReached && quorumMet - ? 'If the current vote stands, this proposal will pass.' - : !thresholdReached && quorumMet - ? "If the current vote stands, this proposal will fail because insufficient 'Yes' votes have been cast." - : thresholdReached && !quorumMet - ? 'If the current vote stands, this proposal will fail due to a lack of voter participation.' - : undefined - : undefined - - // When only abstain votes have been cast and there is no quorum, - // align the abstain progress bar to the right to line up with Abstain - // text. - const onlyAbstain = yesVotes === 0 && noVotes === 0 && abstainVotes > 0 + if (!proposal) { + return null + } return ( -
      - {helpfulStatusText && ( -

      - {helpfulStatusText} -

      - )} - - {threshold ? ( - quorum ? ( - <> -

      Ratio of votes

      - -
      - {[ -

      - Yes{' '} - {turnoutYesPercent.toLocaleString(undefined, localeOptions)}% -

      , -

      - No {turnoutNoPercent.toLocaleString(undefined, localeOptions)} - % -

      , - ] - .sort(() => yesVotes - noVotes) - .map((elem, idx) => ( -
      - {elem} -
      - ))} -

      - Abstain{' '} - {turnoutAbstainPercent.toLocaleString(undefined, localeOptions)} - % -

      -
      - -
      - b.value - a.value), - { - value: Number(turnoutAbstainPercent), - // Secondary is dark with 80% opacity. - color: 'rgba(var(--dark), 0.8)', - }, - ], - }, - ]} - verticalBars={ - threshold && [ - { - value: threshold.percent, - color: 'rgba(var(--dark), 0.5)', - }, - ] - } - /> -
      - -
      - 90 - ? 'calc(100% - 32px)' - : `calc(${threshold.percent}% - 17px)`, - }} - width="36px" - /> - - -
      -

      - Passing threshold:{' '} - {threshold.display} -

      - -

      - {thresholdReached ? ( - <> - Passing{' '} - - - ) : ( - <> - Failing{' '} - - - )} -

      -
      -
      -
      - -
      -

      - Turnout -

      - -

      - {turnoutPercent.toLocaleString(undefined, localeOptions)}% -

      -
      - -
      - -
      - -
      - 90 - ? 'calc(100% - 32px)' - : `calc(${quorum.percent}% - 17px)`, - }} - width="36px" - /> - - -
      -

      - Quorum: {quorum.display} -

      - -

      - {quorumMet ? ( - <> - Reached{' '} - - - ) : ( - <> - Not met{' '} - - - )} -

      -
      -
      -
      - - ) : ( - <> -

      - Turnout -

      - -
      - {[ -

      - Yes {totalYesPercent.toLocaleString(undefined, localeOptions)} - % -

      , -

      - No {totalNoPercent.toLocaleString(undefined, localeOptions)}% -

      , - ] - .sort(() => yesVotes - noVotes) - .map((elem, idx) => ( -
      - {elem} -
      - ))} -

      - Abstain{' '} - {totalAbstainPercent.toLocaleString(undefined, localeOptions)}% -

      -
      - -
      - b.value - a.value), - { - value: Number(totalAbstainPercent), - // Secondary is dark with 80% opacity. - color: 'rgba(var(--dark), 0.8)', - }, - ], - }, - ]} - verticalBars={[ - { - value: threshold.percent, - color: 'rgba(var(--dark), 0.5)', - }, - ]} - /> -
      - -
      - 90 - ? 'calc(100% - 32px)' - : `calc(${threshold.percent}% - 17px)`, - }} - width="36px" - /> - - -
      -

      - Passing threshold:{' '} - {threshold.display} -

      - -

      - {thresholdReached ? ( - <> - Reached{' '} - - - ) : ( - <> - Not met{' '} - - - )} -

      -
      -
      -
      - - ) - ) : null} - - {proposal.status === 'open' && - expiresInSeconds !== undefined && - expiresInSeconds > 0 && ( - <> -

      - Time left -

      - -

      - {secondsToWdhms(expiresInSeconds, 2)} -

      - - {maxVotingSeconds !== undefined && ( -
      - -
      - )} - - )} - - {threshold?.percent === 50 && yesVotes === noVotes && yesVotes > 0 && ( -
      -

      Tie clarification

      - -

      {"'Yes' will win a tie vote."}

      -
      - )} - - {turnoutTotal > 0 && abstainVotes === turnoutTotal && ( -
      -

      All abstain clarification

      - -

      - {/* TODO: Change this to fail wen v1 contracts. */} - When all abstain{quorum && ' and the quorum is met'}, a proposal - will pass. -

      -
      - )} -
      + ) } diff --git a/apps/dapp/components/ProposalForm.tsx b/apps/dapp/components/ProposalForm.tsx index 2f8dbbb17..fd52681f4 100644 --- a/apps/dapp/components/ProposalForm.tsx +++ b/apps/dapp/components/ProposalForm.tsx @@ -2,11 +2,22 @@ import { useState } from 'react' import { useRecoilValue } from 'recoil' +import { Airplane } from '@dao-dao/icons' import { CosmosMsgFor_Empty } from '@dao-dao/types/contracts/cw3-dao' +import { + Button, + Tooltip, + MarkdownPreview, + CosmosMessageDisplay, +} from '@dao-dao/ui' +import { + InputErrorMessage, + InputLabel, + TextareaInput, + TextInput, +} from '@dao-dao/ui' import { EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@heroicons/react/outline' -import Tooltip from '@reach/tooltip' import { FormProvider, useFieldArray, useForm } from 'react-hook-form' -import { Button } from 'ui' import { walletAddress } from 'selectors/treasury' import { @@ -22,13 +33,6 @@ import { import { validateRequired } from 'util/formValidation' import { decodedMessagesString } from 'util/messagehelpers' -import { CosmosMessageDisplay } from './CosmosMessageDisplay' -import SvgAirplane from './icons/Airplane' -import { InputErrorMessage } from './input/InputErrorMessage' -import { InputLabel } from './input/InputLabel' -import { TextareaInput } from './input/TextAreaInput' -import { TextInput } from './input/TextInput' -import { MarkdownPreview } from './MarkdownPreview' import { ProposalTemplateSelector } from './TemplateSelector' interface FormProposalData { @@ -222,7 +226,7 @@ export function ProposalForm({ > + )}
  • ) diff --git a/apps/dapp/components/SidebarLayout.tsx b/apps/dapp/components/SidebarLayout.tsx index a5e2a3436..0b094364b 100644 --- a/apps/dapp/components/SidebarLayout.tsx +++ b/apps/dapp/components/SidebarLayout.tsx @@ -3,10 +3,10 @@ import { useState } from 'react' import Link from 'next/link' +import { Logo } from '@dao-dao/ui' +import { SITE_TITLE } from '@dao-dao/utils' import { MenuIcon } from '@heroicons/react/outline' -import { SITE_TITLE } from '../util/constants' -import { Logo } from './Logo' import Nav from './Nav' const SmallScreenNav = ({ onMenuClick }: { onMenuClick: () => void }) => { diff --git a/apps/dapp/components/StakingModal.tsx b/apps/dapp/components/StakingModal.tsx index 150ea5e26..cb9c246ae 100644 --- a/apps/dapp/components/StakingModal.tsx +++ b/apps/dapp/components/StakingModal.tsx @@ -1,187 +1,35 @@ -import { - ChangeEventHandler, - Dispatch, - MouseEventHandler, - ReactNode, - SetStateAction, - useState, -} from 'react' +import { useState } from 'react' import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil' import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { TokenInfoResponse } from '@dao-dao/types/contracts/cw20-gov' -import { Duration } from '@dao-dao/types/contracts/cw3-dao' +import { StakingMode, StakingModal as StatelessStakingModal } from '@dao-dao/ui' import { - ChevronLeftIcon, - ChevronRightIcon, - XIcon, -} from '@heroicons/react/outline' -import Tooltip from '@reach/tooltip' + convertDenomToMicroDenomWithDecimals, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' import toast from 'react-hot-toast' -import { Button } from 'ui' import { cosmWasmSigningClient, walletAddress as walletAddressSelector, } from 'selectors/cosm' -import { daoSelector, unstakingDuration } from 'selectors/daos' +import { + daoSelector, + unstakingDuration as unstakingDurationSelector, +} from 'selectors/daos' import { cw20TokenInfo, walletStakedTokenBalance, walletTokenBalance, + walletTokenBalanceLoading, walletTokenBalanceUpdateCountAtom, } from 'selectors/treasury' import { cleanChainError } from 'util/cleanChainError' -import { - convertDenomToMicroDenomWithDecimals, - convertMicroDenomToDenomWithDecimals, - humanReadableDuration, -} from 'util/conversion' - -import { Modal } from './Modal' - -export enum StakingMode { - Stake, - Unstake, - Claim, -} - -function stakingModeString(mode: StakingMode) { - switch (mode) { - case StakingMode.Stake: - return 'stake' - case StakingMode.Unstake: - return 'unstake' - case StakingMode.Claim: - return 'claim' - default: - return 'internal error' - } -} - -function ModeButton({ - onClick, - active, - children, -}: { - onClick: MouseEventHandler - active: boolean - children: ReactNode -}) { - return ( - - ) -} - -function AmountSelector({ - onIncrease, - onDecrease, - onChange, - amount, - max, -}: { - onIncrease: MouseEventHandler - onDecrease: MouseEventHandler - onChange: ChangeEventHandler - amount: string - max: number -}) { - return ( -
    - - - -
    - ) -} - -function PercentSelector({ - max, - amount, - setAmount, -}: { - max: number - amount: number - setAmount: Dispatch> -}) { - const active = (p: number) => max * p == amount - const getClassName = (p: number) => - 'rounded-md transition hover:bg-secondary link-text font-normal px-2 py-1' + - (active(p) ? ' bg-secondary border border-inactive' : '') - const getOnClick = (p: number) => () => { - setAmount( - (p * max) - // Need to specify 'en' here or otherwise different langauges - // (ex german) will insert '.' as seperators which will mess - // with our replace logic :) - .toLocaleString('en', { maximumFractionDigits: 6 }) - .replaceAll(',', '') - ) - } - - return ( -
    - - - - - -
    - ) -} - -function durationIsNonZero(d: Duration) { - if ('height' in d) { - return d.height !== 0 - } - return d.time !== 0 -} function executeUnstakeAction( - denomAmount: string, + denomAmount: number, tokenInfo: TokenInfoResponse, stakingAddress: string, signingClient: SigningCosmWasmClient | null, @@ -192,10 +40,12 @@ function executeUnstakeAction( if (!signingClient) { toast.error('Please connect your wallet') } + const amount = convertDenomToMicroDenomWithDecimals( denomAmount, tokenInfo.decimals ) + setLoading(true) signingClient ?.execute( @@ -219,7 +69,7 @@ function executeUnstakeAction( } function executeStakeAction( - denomAmount: string, + denomAmount: number, tokenAddress: string, tokenInfo: TokenInfoResponse, stakingAddress: string, @@ -295,20 +145,19 @@ function executeClaimAction( export function StakingModal({ defaultMode, contractAddress, - claimAmount, + claimableTokens, onClose, beforeExecute, afterExecute, }: { defaultMode: StakingMode contractAddress: string - claimAmount: number - onClose: MouseEventHandler + claimableTokens: number + onClose: () => void beforeExecute: Function afterExecute: Function }) { - const [mode, setMode] = useState(defaultMode) - const [amount, setAmount] = useState('0') + const [amount, setAmount] = useState(0) const walletAddress = useRecoilValue(walletAddressSelector) const signingClient = useRecoilValue(cosmWasmSigningClient) @@ -320,215 +169,105 @@ export function StakingModal({ useRecoilValue(walletTokenBalance(daoInfo?.gov_token)).amount, tokenInfo.decimals ) + const tokenBalanceLoading = useRecoilValue( + walletTokenBalanceLoading(walletAddress) + ) const stakedGovTokenBalance = convertMicroDenomToDenomWithDecimals( useRecoilValue(walletStakedTokenBalance(daoInfo?.staking_contract)).amount, tokenInfo.decimals ) - const unstakeDuration = useRecoilValue( - unstakingDuration(daoInfo.staking_contract) + const unstakingDuration = useRecoilValue( + unstakingDurationSelector(daoInfo.staking_contract) ) const setWalletTokenBalanceUpdateCount = useSetRecoilState( walletTokenBalanceUpdateCountAtom(walletAddress) ) - const maxTx = Number( - mode === StakingMode.Stake ? govTokenBalance : stakedGovTokenBalance - ) - const canClaim = claimAmount !== 0 - - const invalidAmount = (): string => { - if (amount === '') { - return 'Unspecified amount' - } - if (Number(amount) > maxTx || Number(amount) <= 0) { - return `Invalid amount` - } - return '' - } - const walletDisconnected = (): string => { + const walletDisconnected = (): string | undefined => { if (!signingClient || !walletAddress) { return 'Please connect your wallet' } - return '' + return undefined } - const error = invalidAmount() || walletDisconnected() - const ready = !error + const error = walletDisconnected() - const ActionButton = ({ ready }: { ready: boolean }) => { - return ( - - ) + ) + break + case StakingMode.Unstake: + executeUnstakeAction( + amount, + tokenInfo, + daoInfo.staking_contract, + signingClient, + walletAddress, + setLoading, + () => { + setAmount(0) + // New staking balances will not appear until the next block has been added. + setTimeout(() => { + setWalletTokenBalanceUpdateCount((p) => p + 1) + afterExecute() + }, 6500) + } + ) + break + case StakingMode.Claim: + executeClaimAction( + convertMicroDenomToDenomWithDecimals(amount, tokenInfo.decimals), + daoInfo.staking_contract, + signingClient, + walletAddress, + setLoading, + () => { + setTimeout(() => { + setWalletTokenBalanceUpdateCount((p) => p + 1) + afterExecute() + }, 6500) + } + ) + break + default: + toast.error('Internal error while staking. Unrecognized mode.') + } } return ( - -
    - - -
    -

    Manage staking

    -
    -
    - setMode(StakingMode.Stake)} - > - Stake - - setMode(StakingMode.Unstake)} - > - Unstake - - {canClaim && ( - setMode(StakingMode.Claim)} - > - Claim - - )} -
    - {mode !== StakingMode.Claim && ( - <> -
    -

    Choose your token amount

    - setAmount(e?.target?.value)} - onDecrease={() => setAmount((a) => (Number(a) - 1).toString())} - onIncrease={() => setAmount((a) => (Number(a) + 1).toString())} - /> - {Number(amount) > maxTx && ( - - Can{"'"}t {stakingModeString(mode)} more ${tokenInfo.symbol}{' '} - than you own - - )} - - Max available {maxTx} ${tokenInfo.symbol} - -
    - -
    -
    - {mode === StakingMode.Unstake && - durationIsNonZero(unstakeDuration) && ( - <> -
    -
    -

    - Unstaking period: {humanReadableDuration(unstakeDuration)} -

    -

    - There will be {humanReadableDuration(unstakeDuration)}{' '} - between the time you decide to unstake your tokens and the - time you can redeem them. -

    -
    - - )} -
    - -
    - - )} - {mode === StakingMode.Claim && ( - <> -
    -

    - {convertMicroDenomToDenomWithDecimals( - claimAmount, - tokenInfo.decimals - )}{' '} - ${tokenInfo.symbol} avaliable -

    -

    - Claim them to increase your voting power. -

    -
    - - - -
    -
    - - )} -
    -
    + setAmount(newAmount)} + stakableTokens={govTokenBalance} + tokenDecimals={tokenInfo.decimals} + tokenSymbol={tokenInfo.symbol} + unstakableTokens={stakedGovTokenBalance} + unstakingDuration={unstakingDuration} + /> ) } diff --git a/apps/dapp/components/StatusIcons/StatusIcons.tsx b/apps/dapp/components/StatusIcons/StatusIcons.tsx deleted file mode 100644 index 88e1ed377..000000000 --- a/apps/dapp/components/StatusIcons/StatusIcons.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import SvgDraft from '../icons/Draft' -import SvgExecuted from '../icons/Executed' -import SvgOpen from '../icons/Open' -import SvgPassed from '../icons/Passed' -import SvgRejected from '../icons/Rejected' - -export const StatusIcons: { [key: string]: JSX.Element } = { - draft: , - open: , - executed: , - passed: , - rejected: , -} diff --git a/apps/dapp/components/TemplateSelector.tsx b/apps/dapp/components/TemplateSelector.tsx index acfb00e6d..45495971b 100644 --- a/apps/dapp/components/TemplateSelector.tsx +++ b/apps/dapp/components/TemplateSelector.tsx @@ -1,10 +1,9 @@ +import { Modal } from '@dao-dao/ui' import { XIcon } from '@heroicons/react/outline' import { ContractSupport, MessageTemplate } from 'templates/templateList' import { Config } from 'util/contractConfigWrapper' -import { Modal } from './Modal' - export function MessageTemplateDisplayItem({ template, onClick, diff --git a/apps/dapp/components/ThemeToggle.tsx b/apps/dapp/components/ThemeToggle.tsx index 74ce42149..42b958e90 100644 --- a/apps/dapp/components/ThemeToggle.tsx +++ b/apps/dapp/components/ThemeToggle.tsx @@ -1,5 +1,5 @@ +import { useThemeContext } from '@dao-dao/ui' import { MoonIcon, SunIcon } from '@heroicons/react/outline' -import { useThemeContext } from 'ui' export const defaultTheme = 'dark' diff --git a/apps/dapp/components/Transfers.tsx b/apps/dapp/components/Transfers.tsx index 994c0a6c1..4187dfebe 100644 --- a/apps/dapp/components/Transfers.tsx +++ b/apps/dapp/components/Transfers.tsx @@ -1,13 +1,13 @@ import { useRecoilValueLoadable } from 'recoil' import { IndexedTx } from '@cosmjs/stargate' - -import { transactions } from 'selectors/treasury' -import { NATIVE_DECIMALS } from 'util/constants' import { convertMicroDenomToDenomWithDecimals, nativeTokenLabel, -} from 'util/conversion' + NATIVE_DECIMALS, +} from '@dao-dao/utils' + +import { transactions } from 'selectors/treasury' interface TxEventAttribute { key: string diff --git a/apps/dapp/components/icons/Abstain.tsx b/apps/dapp/components/icons/Abstain.tsx deleted file mode 100644 index 0c62b8834..000000000 --- a/apps/dapp/components/icons/Abstain.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgAbstain = (props: SVGProps) => ( - - - -) - -export default SvgAbstain diff --git a/apps/dapp/components/icons/Airplane.tsx b/apps/dapp/components/icons/Airplane.tsx deleted file mode 100644 index be081ede9..000000000 --- a/apps/dapp/components/icons/Airplane.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgAirplane = (props: SVGProps) => ( - - - -) - -export default SvgAirplane diff --git a/apps/dapp/components/icons/ArrowUpRight.tsx b/apps/dapp/components/icons/ArrowUpRight.tsx deleted file mode 100644 index fc45ef151..000000000 --- a/apps/dapp/components/icons/ArrowUpRight.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgArrowUpRight = (props: SVGProps) => ( - - - -) - -export default SvgArrowUpRight diff --git a/apps/dapp/components/icons/Connect.tsx b/apps/dapp/components/icons/Connect.tsx deleted file mode 100644 index 508a52cf1..000000000 --- a/apps/dapp/components/icons/Connect.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgConnect = (props: SVGProps) => ( - - - -) - -export default SvgConnect diff --git a/apps/dapp/components/icons/Copy.tsx b/apps/dapp/components/icons/Copy.tsx deleted file mode 100644 index 4bbafa15c..000000000 --- a/apps/dapp/components/icons/Copy.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgCopy = (props: SVGProps) => ( - - - -) - -export default SvgCopy diff --git a/apps/dapp/components/icons/Dao.tsx b/apps/dapp/components/icons/Dao.tsx deleted file mode 100644 index 2f153afeb..000000000 --- a/apps/dapp/components/icons/Dao.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgDao = (props: SVGProps) => ( - - - -) - -export default SvgDao diff --git a/apps/dapp/components/icons/Discord.tsx b/apps/dapp/components/icons/Discord.tsx deleted file mode 100644 index 047430809..000000000 --- a/apps/dapp/components/icons/Discord.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgDiscord = (props: SVGProps) => ( - - - -) - -export default SvgDiscord diff --git a/apps/dapp/components/icons/Draft.tsx b/apps/dapp/components/icons/Draft.tsx deleted file mode 100644 index ce0dad7e7..000000000 --- a/apps/dapp/components/icons/Draft.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgDraft = (props: SVGProps) => ( - - - -) - -export default SvgDraft diff --git a/apps/dapp/components/icons/Executed.tsx b/apps/dapp/components/icons/Executed.tsx deleted file mode 100644 index 549b71e32..000000000 --- a/apps/dapp/components/icons/Executed.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgExecuted = (props: SVGProps) => ( - - - -) - -export default SvgExecuted diff --git a/apps/dapp/components/icons/Github.tsx b/apps/dapp/components/icons/Github.tsx deleted file mode 100644 index a4ce2069c..000000000 --- a/apps/dapp/components/icons/Github.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgGithub = (props: SVGProps) => ( - - - -) - -export default SvgGithub diff --git a/apps/dapp/components/icons/MemberCheck.tsx b/apps/dapp/components/icons/MemberCheck.tsx deleted file mode 100644 index fad6c2662..000000000 --- a/apps/dapp/components/icons/MemberCheck.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgMemberCheck = (props: SVGProps) => ( - - - - -) - -export default SvgMemberCheck diff --git a/apps/dapp/components/icons/Message.tsx b/apps/dapp/components/icons/Message.tsx deleted file mode 100644 index 4c07e55a1..000000000 --- a/apps/dapp/components/icons/Message.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgMessage = (props: SVGProps) => ( - - - -) - -export default SvgMessage diff --git a/apps/dapp/components/icons/Open.tsx b/apps/dapp/components/icons/Open.tsx deleted file mode 100644 index 0cc20a9e8..000000000 --- a/apps/dapp/components/icons/Open.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgOpen = (props: SVGProps) => ( - - - -) - -export default SvgOpen diff --git a/apps/dapp/components/icons/Passed.tsx b/apps/dapp/components/icons/Passed.tsx deleted file mode 100644 index 029b19669..000000000 --- a/apps/dapp/components/icons/Passed.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgPassed = (props: SVGProps) => ( - - - -) - -export default SvgPassed diff --git a/apps/dapp/components/icons/Pencil.tsx b/apps/dapp/components/icons/Pencil.tsx deleted file mode 100644 index 868be97d4..000000000 --- a/apps/dapp/components/icons/Pencil.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgPencil = (props: SVGProps) => ( - - - -) - -export default SvgPencil diff --git a/apps/dapp/components/icons/Rejected.tsx b/apps/dapp/components/icons/Rejected.tsx deleted file mode 100644 index 4e23a67ee..000000000 --- a/apps/dapp/components/icons/Rejected.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgRejected = (props: SVGProps) => ( - - - -) - -export default SvgRejected diff --git a/apps/dapp/components/icons/Twitter.tsx b/apps/dapp/components/icons/Twitter.tsx deleted file mode 100644 index c759bb35f..000000000 --- a/apps/dapp/components/icons/Twitter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgTwitter = (props: SVGProps) => ( - - - -) - -export default SvgTwitter diff --git a/apps/dapp/components/icons/Votes.tsx b/apps/dapp/components/icons/Votes.tsx deleted file mode 100644 index cb0c7918d..000000000 --- a/apps/dapp/components/icons/Votes.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgVotes = (props: SVGProps) => ( - - - -) - -export default SvgVotes diff --git a/apps/dapp/components/icons/Wallet.tsx b/apps/dapp/components/icons/Wallet.tsx deleted file mode 100644 index 4eeaaffa7..000000000 --- a/apps/dapp/components/icons/Wallet.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -const SvgWallet = (props: SVGProps) => ( - - - -) - -export default SvgWallet diff --git a/apps/dapp/components/index.ts b/apps/dapp/components/index.ts deleted file mode 100644 index 80d03e816..000000000 --- a/apps/dapp/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { Button } from 'ui/Button' -export { ProposalStatus } from './ProposalStatus' -export { StatusIcons } from './StatusIcons' diff --git a/apps/dapp/models/proposal/proposal.ts b/apps/dapp/models/proposal/proposal.ts index 881b39bbd..b1b23a316 100644 --- a/apps/dapp/models/proposal/proposal.ts +++ b/apps/dapp/models/proposal/proposal.ts @@ -9,10 +9,6 @@ import { Votes, } from '@dao-dao/types/contracts/cw3-dao' -import { ProposalMapItem } from 'types/proposals' - -import { labelForMessage } from '../../util/messagehelpers' - export const MEMO_MAX_LEN = 255 const EmptyThreshold: Threshold = { @@ -45,12 +41,6 @@ export const EmptyProposal: Proposal = { votes: { ...EmptyVotes }, } -export const EmptyProposalItem: ProposalMapItem = { - proposal: EmptyProposal, - id: '', - draft: true, -} - export const EmptyThresholdResponse: ThresholdResponse = { absolute_percentage: { percentage: '0', @@ -76,13 +66,3 @@ export const EmptyProposalResponse: ProposalResponse = { threshold: { ...EmptyThresholdResponse }, total_weight: '0', } - -export function memoForProposal(proposal: Proposal): string { - const messagesMemo = proposal.msgs - ? proposal.msgs.map((msg) => labelForMessage(msg)).join(', ') - : '' - return `${proposal.title}\n${proposal.description}\n\n${messagesMemo}`.slice( - 0, - MEMO_MAX_LEN - ) -} diff --git a/apps/dapp/next.config.js b/apps/dapp/next.config.js index d9e81317d..9556fc36d 100644 --- a/apps/dapp/next.config.js +++ b/apps/dapp/next.config.js @@ -2,7 +2,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) const withTM = require('next-transpile-modules')([ - 'ui', + '@dao-dao/ui', '@dao-dao/icons', '@dao-dao/utils', ]) diff --git a/apps/dapp/package.json b/apps/dapp/package.json index 453ecb6e2..7fd2fb517 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -25,6 +25,7 @@ "@dao-dao/icons": "*", "@dao-dao/types": "^0.0.8", "@dao-dao/utils": "*", + "@dao-dao/ui": "*", "@headlessui/react": "^1.4.2", "@heroicons/react": "^1.0.5", "@keplr-wallet/types": "^0.9.10", @@ -53,8 +54,7 @@ "react-markdown": "^8.0.0", "recoil": "^0.5.2", "rich-markdown-editor": "^11.21.3", - "styled-components": "^5.3.3", - "ui": "*" + "styled-components": "^5.3.3" }, "devDependencies": { "@babel/core": "^7.16.7", diff --git a/apps/dapp/pages/_app.tsx b/apps/dapp/pages/_app.tsx index acc42ec58..a55485092 100644 --- a/apps/dapp/pages/_app.tsx +++ b/apps/dapp/pages/_app.tsx @@ -1,4 +1,4 @@ -import 'ui/globals.css' +import '@dao-dao/ui/globals.css' import 'styles/app.css' import { useState, useEffect } from 'react' import { Suspense } from 'react' @@ -8,12 +8,11 @@ import { useRouter } from 'next/router' import { RecoilRoot } from 'recoil' -import { DEFAULT_THEME_NAME, ThemeProvider } from 'ui' +import { DEFAULT_THEME_NAME, ThemeProvider, LoadingScreen } from '@dao-dao/ui' import ErrorBoundary from 'components/ErrorBoundary' import { HomepageLayout } from 'components/HomepageLayout' import SidebarLayout from 'components/Layout' -import LoadingScreen from 'components/LoadingScreen' import Notifications from 'components/Notifications' function MyApp({ Component, pageProps }: AppProps) { diff --git a/apps/dapp/pages/_document.tsx b/apps/dapp/pages/_document.tsx index aa5248b45..f50ae8ead 100644 --- a/apps/dapp/pages/_document.tsx +++ b/apps/dapp/pages/_document.tsx @@ -2,7 +2,7 @@ import React from 'react' import Document, { Html, Head, Main, NextScript } from 'next/document' -import { SITE_TITLE } from 'util/constants' +import { SITE_TITLE } from '@dao-dao/utils' class MyDocument extends Document { static async getInitialProps(ctx: any) { diff --git a/apps/dapp/pages/dao/[contractAddress]/index.tsx b/apps/dapp/pages/dao/[contractAddress]/index.tsx index 5e0d28035..8cbca66ce 100644 --- a/apps/dapp/pages/dao/[contractAddress]/index.tsx +++ b/apps/dapp/pages/dao/[contractAddress]/index.tsx @@ -3,28 +3,33 @@ import React, { useEffect, useState } from 'react' import type { GetStaticPaths, GetStaticProps, NextPage } from 'next' import { useRouter } from 'next/router' -import { useRecoilState, useRecoilValue } from 'recoil' +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' +import { MemberCheck, Pencil } from '@dao-dao/icons' +import { + useThemeContext, + StakingMode, + GradientHero, + HorizontalInfo, + ContractHeader, + StarButton, + HorizontalInfoSection, + BalanceCard, + Breadcrumbs, +} from '@dao-dao/ui' +import { + convertMicroDenomToDenomWithDecimals, + claimAvaliable, +} from '@dao-dao/utils' import { LibraryIcon, PlusSmIcon, UsersIcon } from '@heroicons/react/outline' -import { useThemeContext } from 'ui' -import { claimAvaliable, ClaimsPendingList } from '@components/Claims' +import { ClaimsPendingList } from '@components/Claims' import { DaoContractInfo } from '@components/DaoContractInfo' -import SvgMemberCheck from '@components/icons/MemberCheck' -import SvgPencil from '@components/icons/Pencil' import { pinnedDaosAtom } from 'atoms/pinned' -import { Breadcrumbs } from 'components/Breadcrumbs' -import { - BalanceCard, - ContractProposalsDispaly, - GradientHero, - HeroContractHorizontalInfo, - HeroContractHeader, - StarButton, - HeroContractHorizontalInfoSection, -} from 'components/ContractView' +import { ContractProposalsDispaly } from 'components/ContractView' import ErrorBoundary from 'components/ErrorBoundary' -import { StakingModal, StakingMode } from 'components/StakingModal' +import { StakingModal } from 'components/StakingModal' +import { contractInstantiateTime } from 'selectors/contracts' import { CHAIN_RPC_ENDPOINT, isMemberSelector } from 'selectors/cosm' import { daoSelector, @@ -39,11 +44,11 @@ import { walletStakedTokenBalance, walletTokenBalance, walletTokenBalanceLoading, + walletTokenBalanceUpdateCountAtom, } from 'selectors/treasury' import { addToken } from 'util/addToken' import { cosmWasmClientRouter } from 'util/chainClientRouter' import { getFastAverageColor } from 'util/colors' -import { convertMicroDenomToDenomWithDecimals } from 'util/conversion' function DaoHome() { const router = useRouter() @@ -51,6 +56,9 @@ function DaoHome() { const daoInfo = useRecoilValue(daoSelector(contractAddress)) const tokenInfo = useRecoilValue(tokenConfig(daoInfo?.gov_token)) + const establishedDate = useRecoilValue( + contractInstantiateTime(contractAddress) + ) const stakedTotal = useRecoilValue(totalStaked(daoInfo?.staking_contract)) const proposalsTotal = useRecoilValue(proposalCount(contractAddress)) const { member } = useRecoilValue(isMemberSelector(contractAddress)) @@ -62,18 +70,17 @@ function DaoHome() { ) const blockHeight = useRecoilValue(getBlockHeight) const stuff = useRecoilValue(walletClaims(daoInfo.staking_contract)) - const initialClaimsAvaliable = stuff.claims + const claimsAvaliable = stuff.claims .filter((c) => claimAvaliable(c, blockHeight)) .reduce((p, n) => p + Number(n.amount), 0) - // If a claim becomes avaliable while the page is open we need a way to update - // the number of claims avaliable. - const [claimsAvaliable, setClaimsAvaliable] = useState(initialClaimsAvaliable) - const wallet = useRecoilValue(walletAddress) const [tokenBalanceLoading, setTokenBalancesLoading] = useRecoilState( walletTokenBalanceLoading(wallet) ) + const setWalletTokenBalanceUpdateCount = useSetRecoilState( + walletTokenBalanceUpdateCountAtom(wallet) + ) const [showStaking, setShowStaking] = useState(false) @@ -106,7 +113,7 @@ function DaoHome() {
    {member && (
    - +

    You{"'"}re a member

    )} @@ -124,32 +131,32 @@ function DaoHome() {
    -
    - - + + {convertMicroDenomToDenomWithDecimals( tokenInfo.total_supply, tokenInfo.decimals ).toLocaleString()}{' '} ${tokenInfo?.symbol} total supply - - + + {stakedPercent}% ${tokenInfo?.symbol} staked - - - + + + {proposalsTotal} proposals created - - + +
    @@ -248,7 +255,9 @@ function DaoHome() {
    ) : null} setClaimsAvaliable((a) => a + n)} + incrementClaimsAvaliable={(_) => + setWalletTokenBalanceUpdateCount((n) => n + 1) + } stakingAddress={daoInfo.staking_contract} tokenInfo={tokenInfo} /> @@ -256,7 +265,7 @@ function DaoHome() { setTokenBalancesLoading(false)} beforeExecute={() => setTokenBalancesLoading(true)} - claimAmount={claimsAvaliable} + claimableTokens={claimsAvaliable} contractAddress={contractAddress} defaultMode={StakingMode.Stake} onClose={() => setShowStaking(false)} diff --git a/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx b/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx index 9105722a6..9f2c22b1b 100644 --- a/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx +++ b/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx @@ -3,12 +3,13 @@ import { useRouter } from 'next/router' import { useRecoilValue } from 'recoil' -import { Breadcrumbs } from 'components/Breadcrumbs' +import { Breadcrumbs } from '@dao-dao/ui' + import { ProposalDetails } from 'components/ProposalDetails' import { ProposalDetailsSidebar, - ProposalDetailsCard, ProposalDetailsVoteStatus, + ProposalDetailsCard, } from 'components/ProposalDetailsSidebar' import { daoSelector } from 'selectors/daos' import { cw20TokenInfo } from 'selectors/treasury' diff --git a/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx b/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx index 93b7c6642..8ac8602da 100644 --- a/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx +++ b/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx @@ -7,10 +7,9 @@ import { useRecoilValue, useSetRecoilState } from 'recoil' import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { findAttribute } from '@cosmjs/stargate/build/logs' +import { CopyToClipboard, Breadcrumbs } from '@dao-dao/ui' import toast from 'react-hot-toast' -import { Breadcrumbs } from '@components/Breadcrumbs' -import { CopyToClipboard } from '@components/CopyToClipboard' import { ProposalData, ProposalForm } from '@components/ProposalForm' import { proposalsCreatedAtom } from 'atoms/proposals' import { diff --git a/apps/dapp/pages/dao/create.tsx b/apps/dapp/pages/dao/create.tsx index 0d8a0446b..b1ca0f948 100644 --- a/apps/dapp/pages/dao/create.tsx +++ b/apps/dapp/pages/dao/create.tsx @@ -6,31 +6,38 @@ import { useRouter } from 'next/router' import { useRecoilValue, useSetRecoilState } from 'recoil' import { InstantiateResult } from '@cosmjs/cosmwasm-stargate' +import { Airplane } from '@dao-dao/icons' import { TokenInfoResponse } from '@dao-dao/types/contracts/cw20-gov' import { InstantiateMsg } from '@dao-dao/types/contracts/cw3-dao' +import { Button, Tooltip } from '@dao-dao/ui' +import { + AddressInput, + Breadcrumbs, + ImageSelector, + ImageSelectorModal, + InputErrorMessage, + InputLabel, + NumberInput, + TextareaInput, + TextInput, + ToggleInput, + TokenAmountInput, + GradientHero, +} from '@dao-dao/ui' +import { + DAO_CODE_ID, + NATIVE_DECIMALS, + convertDenomToMicroDenomWithDecimals, + secondsToWdhms, +} from '@dao-dao/utils' import { PlusIcon } from '@heroicons/react/outline' -import Tooltip from '@reach/tooltip' import { useFieldArray, useForm } from 'react-hook-form' -import { Button } from 'ui' -import { GradientHero } from '@components/ContractView' import { FormCard } from '@components/FormCard' -import SvgAirplane from '@components/icons/Airplane' -import { AddressInput } from '@components/input/AddressInput' -import { ImageSelector } from '@components/input/ImageSelector' -import { ImageSelectorModal } from '@components/input/ImageSelector' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { InputLabel } from '@components/input/InputLabel' -import { NumberInput } from '@components/input/NumberInput' -import { TextareaInput } from '@components/input/TextAreaInput' -import { TextInput } from '@components/input/TextInput' -import { ToggleInput } from '@components/input/ToggleInput' -import { TokenAmountInput } from '@components/input/TokenAmountInput' import TooltipsDisplay, { useTooltipsRegister, } from '@components/TooltipsDisplay' import { pinnedDaosAtom } from 'atoms/pinned' -import { Breadcrumbs } from 'components/Breadcrumbs' import { daoCreateTooltipsGetter, daoCreateTooltipsDefault, @@ -40,11 +47,6 @@ import { walletAddress as walletAddressSelector, } from 'selectors/cosm' import { cleanChainError } from 'util/cleanChainError' -import { DAO_CODE_ID, NATIVE_DECIMALS } from 'util/constants' -import { - convertDenomToMicroDenomWithDecimals, - secondsToWdhms, -} from 'util/conversion' import { validateContractAddress, validateNonNegative, @@ -651,7 +653,7 @@ const CreateDao: NextPage = () => { >
    diff --git a/apps/dapp/pages/dao/list.tsx b/apps/dapp/pages/dao/list.tsx index 1b55a5ddb..95cdd455d 100644 --- a/apps/dapp/pages/dao/list.tsx +++ b/apps/dapp/pages/dao/list.tsx @@ -12,6 +12,12 @@ import { Loadable, } from 'recoil' +import { Button } from '@dao-dao/ui' +import { + DAO_CODE_ID, + LEGACY_DAO_CODE_ID, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' import { LibraryIcon, PlusIcon, @@ -19,8 +25,6 @@ import { UserIcon, } from '@heroicons/react/outline' -import { Button } from '@components' - import { EmptyDaoCard } from '@components/EmptyDaoCard' import { pinnedDaosAtom } from 'atoms/pinned' import CodeIdSelect from 'components/CodeIdSelect' @@ -29,8 +33,6 @@ import Paginator from 'components/Paginator' import { pagedContractsByCodeId } from 'selectors/contracts' import { DaoListType, memberDaoSelector } from 'selectors/daos' import { addToken } from 'util/addToken' -import { DAO_CODE_ID, LEGACY_DAO_CODE_ID } from 'util/constants' -import { convertMicroDenomToDenomWithDecimals } from 'util/conversion' export function DaoCard({ dao, diff --git a/apps/dapp/pages/index.tsx b/apps/dapp/pages/index.tsx index 595ac6f37..3ae0ebf1f 100644 --- a/apps/dapp/pages/index.tsx +++ b/apps/dapp/pages/index.tsx @@ -3,6 +3,9 @@ import { ReactNode } from 'react' import type { NextPage } from 'next' import Link from 'next/link' +import { ArrowUpRight, Discord, Github, Twitter } from '@dao-dao/icons' +import { Button, GradientWrapper, Logo } from '@dao-dao/ui' +import { SITE_TITLE } from '@dao-dao/utils' import { ScaleIcon } from '@heroicons/react/outline' import { ArrowNarrowRightIcon, @@ -10,25 +13,15 @@ import { StarIcon, } from '@heroicons/react/solid' -import { Button } from '@components' - -import SvgArrowUpRight from '@components/icons/ArrowUpRight' -import SvgDiscord from '@components/icons/Discord' -import { GradientWrapper } from 'components/GradientWrapper' -import SvgGithub from 'components/icons/Github' -import SvgTwitter from 'components/icons/Twitter' -import { Logo } from 'components/Logo' import ThemeToggle from 'components/ThemeToggle' -import { SITE_TITLE } from '../util/constants' - function EnterAppButton({ small }: { small?: boolean }) { return ( @@ -85,11 +78,7 @@ const Home: NextPage = () => { href="https://docs.daodao.zone" > Documentation - +
    @@ -151,7 +140,7 @@ const Home: NextPage = () => { rel="noreferrer" target="_blank" > - + { rel="noreferrer" target="_blank" > - + { rel="noreferrer" target="_blank" > - +
    diff --git a/apps/dapp/pages/multisig/[contractAddress]/index.tsx b/apps/dapp/pages/multisig/[contractAddress]/index.tsx index d33e59740..9e237aee3 100644 --- a/apps/dapp/pages/multisig/[contractAddress]/index.tsx +++ b/apps/dapp/pages/multisig/[contractAddress]/index.tsx @@ -6,27 +6,28 @@ import { useRouter } from 'next/router' import { useRecoilState, useRecoilValue } from 'recoil' import { Threshold } from '@dao-dao/types/contracts/cw3-multisig' +import { + CopyToClipboard, + useThemeContext, + GradientHero, + HorizontalInfo, + HorizontalInfoSection, + ContractHeader, + StarButton, + BalanceIcon, + Breadcrumbs, +} from '@dao-dao/ui' import { ScaleIcon, UserGroupIcon, VariableIcon, } from '@heroicons/react/outline' -import { useThemeContext } from 'ui' -import { CopyToClipboard } from '@components/CopyToClipboard' import { MultisigContractInfo } from '@components/MultisigContractInfo' import { pinnedMultisigsAtom } from 'atoms/pinned' -import { Breadcrumbs } from 'components/Breadcrumbs' -import { - ContractProposalsDispaly, - GradientHero, - HeroContractHorizontalInfo, - HeroContractHeader, - StarButton, - BalanceIcon, - HeroContractHorizontalInfoSection, -} from 'components/ContractView' +import { ContractProposalsDispaly } from 'components/ContractView' import ErrorBoundary from 'components/ErrorBoundary' +import { contractInstantiateTime } from 'selectors/contracts' import { CHAIN_RPC_ENDPOINT } from 'selectors/cosm' import { listMembers, @@ -90,6 +91,7 @@ function MultisigHome() { const contractAddress = router.query.contractAddress as string const sigInfo = useRecoilValue(sigSelector(contractAddress)) + const established = useRecoilValue(contractInstantiateTime(contractAddress)) const weightTotal = useRecoilValue(totalWeight(contractAddress)) const visitorWeight = useRecoilValue(memberWeight(contractAddress)) @@ -121,28 +123,28 @@ function MultisigHome() { />
    -
    - - + + {thresholdString(sigInfo.config.threshold)} - - + + Total votes: {weightTotal} - - + + Total members: {memberList.length} - - + +
    @@ -207,7 +209,7 @@ const MultisigHomePage: NextPage = ({ accentColor }) => { }, [accentColor, setAccentColor, isReady, isFallback]) // Trigger Suspense. - if (!isReady || isFallback) throw new Promise((resolve) => {}) + if (!isReady || isFallback) throw new Promise((_) => {}) return ( diff --git a/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx b/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx index ace15b90e..583c82170 100644 --- a/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx +++ b/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx @@ -3,7 +3,8 @@ import { useRouter } from 'next/router' import { useRecoilValue } from 'recoil' -import { Breadcrumbs } from 'components/Breadcrumbs' +import { Breadcrumbs } from '@dao-dao/ui' + import { ProposalDetails } from 'components/ProposalDetails' import { ProposalDetailsSidebar, diff --git a/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx b/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx index d88deb06d..9202c4bc2 100644 --- a/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx +++ b/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx @@ -7,9 +7,9 @@ import { useRecoilValue } from 'recoil' import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { findAttribute } from '@cosmjs/stargate/build/logs' +import { Breadcrumbs } from '@dao-dao/ui' import toast from 'react-hot-toast' -import { Breadcrumbs } from '@components/Breadcrumbs' import { ProposalData, ProposalForm } from '@components/ProposalForm' import { cosmWasmSigningClient, diff --git a/apps/dapp/pages/multisig/create.tsx b/apps/dapp/pages/multisig/create.tsx index 2e210e3e8..3b5ea6863 100644 --- a/apps/dapp/pages/multisig/create.tsx +++ b/apps/dapp/pages/multisig/create.tsx @@ -6,26 +6,28 @@ import { useRouter } from 'next/router' import { useSetRecoilState, useRecoilValue } from 'recoil' import { InstantiateResult } from '@cosmjs/cosmwasm-stargate' +import { Airplane } from '@dao-dao/icons' +import { Button, Tooltip } from '@dao-dao/ui' +import { + GradientHero, + Breadcrumbs, + ImageSelector, + InputErrorMessage, + InputLabel, + NumberInput, + TextareaInput, + TextInput, + TokenAmountInput, +} from '@dao-dao/ui' +import { MULTISIG_CODE_ID, secondsToWdhms } from '@dao-dao/utils' import { PlusIcon } from '@heroicons/react/outline' -import Tooltip from '@reach/tooltip' import { useFieldArray, useForm, Validate } from 'react-hook-form' -import { Button } from 'ui' -import { GradientHero } from '@components/ContractView' import { FormCard } from '@components/FormCard' -import SvgAirplane from '@components/icons/Airplane' -import { ImageSelector } from '@components/input/ImageSelector' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { InputLabel } from '@components/input/InputLabel' -import { NumberInput } from '@components/input/NumberInput' -import { TextareaInput } from '@components/input/TextAreaInput' -import { TextInput } from '@components/input/TextInput' -import { TokenAmountInput } from '@components/input/TokenAmountInput' import TooltipsDisplay, { useTooltipsRegister, } from '@components/TooltipsDisplay' import { pinnedMultisigsAtom } from 'atoms/pinned' -import { Breadcrumbs } from 'components/Breadcrumbs' import { multisigCreateTooltipsDefault, multisigCreateTooltipsGetter, @@ -35,8 +37,6 @@ import { walletAddress as walletAddressSelector, } from 'selectors/cosm' import { cleanChainError } from 'util/cleanChainError' -import { MULTISIG_CODE_ID } from 'util/constants' -import { secondsToWdhms } from 'util/conversion' import { validatePercent, validatePositive, @@ -296,7 +296,7 @@ const CreateMultisig: NextPage = () => { >
diff --git a/apps/dapp/pages/multisig/list.tsx b/apps/dapp/pages/multisig/list.tsx index aff3a6a7f..e4c4f74ef 100644 --- a/apps/dapp/pages/multisig/list.tsx +++ b/apps/dapp/pages/multisig/list.tsx @@ -12,13 +12,18 @@ import { Loadable, } from 'recoil' +import { Button } from '@dao-dao/ui' +import { + LEGACY_MULTISIG_CODE_ID, + MULTISIG_CODE_ID, + NATIVE_DENOM, +} from '@dao-dao/utils' import { LibraryIcon, PlusIcon, SparklesIcon, UserIcon, } from '@heroicons/react/outline' -import { Button } from 'ui' import { EmptyMultisigCard } from '@components/EmptyMultisigCard' import { pinnedMultisigsAtom } from 'atoms/pinned' @@ -29,11 +34,6 @@ import { pagedContractsByCodeId } from 'selectors/contracts' import { proposalCount } from 'selectors/daos' import { MultisigListType, sigMemberSelector } from 'selectors/multisigs' import { nativeBalance } from 'selectors/treasury' -import { - LEGACY_MULTISIG_CODE_ID, - MULTISIG_CODE_ID, - NATIVE_DENOM, -} from 'util/constants' export function MultisigCard({ multisig, diff --git a/apps/dapp/pages/starred.tsx b/apps/dapp/pages/starred.tsx index 2b428bbb1..7e272c60d 100644 --- a/apps/dapp/pages/starred.tsx +++ b/apps/dapp/pages/starred.tsx @@ -3,6 +3,10 @@ import Link from 'next/link' import { useRecoilState, useRecoilValue } from 'recoil' +import { + NATIVE_DENOM, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' import { MapIcon, PlusIcon, StarIcon } from '@heroicons/react/outline' import { EmptyDaoCard } from '@components/EmptyDaoCard' @@ -14,8 +18,6 @@ import { memberDaoSelector, proposalCount } from 'selectors/daos' import { sigSelector } from 'selectors/multisigs' import { cw20TokenInfo, nativeBalance } from 'selectors/treasury' import { addToken } from 'util/addToken' -import { NATIVE_DENOM } from 'util/constants' -import { convertMicroDenomToDenomWithDecimals } from 'util/conversion' function PinnedDaoCard({ address }: { address: string }) { const listInfo = useRecoilValue(memberDaoSelector(address)) diff --git a/apps/dapp/selectors/cosm.ts b/apps/dapp/selectors/cosm.ts index 97b7a89ff..fe70827a8 100644 --- a/apps/dapp/selectors/cosm.ts +++ b/apps/dapp/selectors/cosm.ts @@ -2,12 +2,12 @@ import { selector, selectorFamily, atom } from 'recoil' import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { GasPrice } from '@cosmjs/stargate' +import { NATIVE_DENOM, GAS_PRICE } from '@dao-dao/utils' import { cosmWasmClientRouter, stargateClientRouter, } from 'util/chainClientRouter' -import { NATIVE_DENOM, GAS_PRICE } from 'util/constants' import { localStorageEffect } from '../atoms/localStorageEffect' import { connectKeplrWithoutAlerts } from '../services/keplr' @@ -125,6 +125,9 @@ export const walletChainBalanceSelector = selector({ return 0 } const address = get(walletAddress) + if (!address) { + return 0 + } const balance = await client.getBalance(address, NATIVE_DENOM) return Number(balance.amount) }, diff --git a/apps/dapp/selectors/daos.ts b/apps/dapp/selectors/daos.ts index 50ecd6aac..73e7d8312 100644 --- a/apps/dapp/selectors/daos.ts +++ b/apps/dapp/selectors/daos.ts @@ -6,10 +6,10 @@ import { ConfigResponse, Duration, } from '@dao-dao/types/contracts/cw3-dao' +import { DAO_CODE_ID, NATIVE_DENOM } from '@dao-dao/utils' import { contractsByCodeId } from 'selectors/contracts' import { cosmWasmClient, isMemberSelector } from 'selectors/cosm' -import { DAO_CODE_ID, NATIVE_DENOM } from 'util/constants' import { nativeBalance, diff --git a/apps/dapp/selectors/multisigs.ts b/apps/dapp/selectors/multisigs.ts index b9714873b..aeff7ce61 100644 --- a/apps/dapp/selectors/multisigs.ts +++ b/apps/dapp/selectors/multisigs.ts @@ -1,10 +1,10 @@ import { selectorFamily } from 'recoil' import { ConfigResponse } from '@dao-dao/types/contracts/cw3-multisig' +import { MULTISIG_CODE_ID } from '@dao-dao/utils' import { contractsByCodeId } from 'selectors/contracts' import { cosmWasmClient, isMemberSelector } from 'selectors/cosm' -import { MULTISIG_CODE_ID } from 'util/constants' import { walletAddress } from './treasury' diff --git a/apps/dapp/selectors/proposals.ts b/apps/dapp/selectors/proposals.ts index 5429b7252..f2a2906bb 100644 --- a/apps/dapp/selectors/proposals.ts +++ b/apps/dapp/selectors/proposals.ts @@ -8,25 +8,11 @@ import { } from '@dao-dao/types/contracts/cw3-dao' import { - contractProposalMapAtom, proposalsRequestIdAtom, proposalUpdateCountAtom, } from 'atoms/proposals' -import { MessageMap, MessageMapEntry } from 'models/proposal/messageMap' -import { - EmptyProposalResponse, - EmptyProposalTallyResponse, - EmptyThresholdResponse, -} from 'models/proposal/proposal' -import { - ContractProposalMap, - ExtendedProposalResponse, - ProposalKey, - ProposalMap, - ProposalMapItem, - ProposalMessageKey, -} from 'types/proposals' +import { EmptyProposalTallyResponse } from '../models/proposal/proposal' import { cosmWasmClient } from './cosm' import { daoSelector } from './daos' import { sigSelector } from './multisigs' @@ -60,7 +46,7 @@ export enum WalletVote { } type WalletVoteValue = `${WalletVote}` -export const onChainProposalsSelector = selectorFamily< +export const proposalsSelector = selectorFamily< ProposalResponse[], { contractAddress: string @@ -90,7 +76,7 @@ export const onChainProposalsSelector = selectorFamily< export const proposalSelector = selectorFamily< ProposalResponse | undefined, - { contractAddress: string; proposalId: string | number } + { contractAddress: string; proposalId: number } >({ key: 'proposalSelector', get: @@ -99,20 +85,10 @@ export const proposalSelector = selectorFamily< proposalId, }: { contractAddress: string - proposalId: string | number + proposalId: number }) => async ({ get }) => { - if (typeof proposalId === 'string') { - const draftProposal = get( - draftProposalSelector({ contractAddress, proposalId }) - ) - if (draftProposal?.proposal) { - return draftProposal?.proposal - } - } - if (typeof proposalId === 'number') { - get(proposalUpdateCountAtom({ contractAddress, proposalId })) - } + get(proposalUpdateCountAtom({ contractAddress, proposalId })) const client = get(cosmWasmClient) try { @@ -272,9 +248,7 @@ export const proposalTallySelector = selectorFamily< return tally } catch (e) { console.error(e) - return { - ...EmptyProposalTallyResponse, - } + return { ...EmptyProposalTallyResponse } } }, }) @@ -319,144 +293,3 @@ export const votingPowerAtHeightSelector = selectorFamily< return balance }, }) - -export const draftProposalsSelector = selectorFamily({ - key: 'draftProposals', - get: - (contractAddress) => - ({ get }) => { - return get(proposalMapSelector(contractAddress)) - }, - set: - (contractAddress) => - ({ set }, newValue) => { - set(proposalMapSelector(contractAddress), newValue) - }, -}) - -export const proposalsSelector = selectorFamily< - ExtendedProposalResponse[], - { - contractAddress: string - startBefore: number - limit: number - } ->({ - key: 'proposals', - get: - ({ contractAddress, startBefore, limit }) => - async ({ get }) => { - let draftProposalItems: ExtendedProposalResponse[] = [] - // Add in draft proposals: - const draftProposals = get(draftProposalsSelector(contractAddress)) - if (draftProposals) { - draftProposalItems = Object.values(draftProposals).map((draft) => { - const proposalResponse: ExtendedProposalResponse = { - ...EmptyProposalResponse, - ...draft.proposal, - status: 'draft' as any, - draftId: draft.id, - threshold: { ...EmptyThresholdResponse }, - total_weight: 0, - } - return proposalResponse - }) - } - - const onChainProposalList = get( - onChainProposalsSelector({ - contractAddress, - startBefore, - limit, - }) - ) - - return (draftProposalItems ?? []).concat(onChainProposalList) - }, -}) - -export const proposalMapSelector = selectorFamily({ - key: 'proposalMap', - get: - (contractAddress) => - ({ get }) => { - const contractProposalMap = get(contractProposalMapAtom) - return contractProposalMap[contractAddress] - }, - set: - (contractAddress) => - ({ get, set }, newValue) => { - const contractProposalMap = get(contractProposalMapAtom) - const updatedMap: ContractProposalMap = { - ...contractProposalMap, - [contractAddress]: newValue, - } as unknown as ContractProposalMap - set(contractProposalMapAtom, updatedMap) - }, -}) - -export const draftProposalSelector = selectorFamily< - ProposalMapItem | undefined, - ProposalKey ->({ - key: 'draftProposal', - get: - ({ contractAddress, proposalId }) => - ({ get }) => { - const draftProposals = get(proposalMapSelector(contractAddress)) - return draftProposals ? draftProposals[proposalId] : undefined - }, - set: - ({ contractAddress, proposalId }) => - ({ set, get }, newValue) => { - if (newValue) { - const draftProposals = get(proposalMapSelector(contractAddress)) - const updatedDraftProposals: ProposalMap = { - ...draftProposals, - [proposalId]: newValue as any, - } - set(proposalMapSelector(contractAddress), updatedDraftProposals) - } - }, -}) - -export const draftProposalMessageSelector = selectorFamily< - MessageMapEntry | undefined, - ProposalMessageKey ->({ - key: 'draftProposalMessage', - get: - ({ contractAddress, proposalId, messageId }) => - ({ get }) => { - const draftProposal = get( - draftProposalSelector({ contractAddress, proposalId }) - ) - return draftProposal?.messages - ? draftProposal.messages[messageId] - : undefined - }, - set: - ({ contractAddress, proposalId, messageId }) => - ({ set, get }, newValue) => { - if (!newValue) { - return - } - const draftProposal = get( - draftProposalSelector({ contractAddress, proposalId }) - ) - if (draftProposal) { - const messages: MessageMap = { - ...draftProposal?.messages, - [messageId]: newValue, - } as any - const updatedProposal: ProposalMapItem = { - ...draftProposal, - messages, - } - set( - draftProposalSelector({ contractAddress, proposalId }), - updatedProposal - ) - } - }, -}) diff --git a/apps/dapp/services/keplr.tsx b/apps/dapp/services/keplr.tsx index 6e575a034..356a294bf 100644 --- a/apps/dapp/services/keplr.tsx +++ b/apps/dapp/services/keplr.tsx @@ -1,7 +1,6 @@ +import { convertFromMicroDenom } from '@dao-dao/utils' import { Keplr, Window as KeplrWindow } from '@keplr-wallet/types' -import { convertFromMicroDenom } from '../util/conversion' - declare global { interface Window extends KeplrWindow {} } diff --git a/apps/dapp/tailwind.config.js b/apps/dapp/tailwind.config.js index a9d7350f6..e338a2fba 100644 --- a/apps/dapp/tailwind.config.js +++ b/apps/dapp/tailwind.config.js @@ -1,4 +1,4 @@ -let config = require('ui/tailwind.config') +let config = require('@dao-dao/ui/tailwind.config') module.exports = { ...config, diff --git a/apps/dapp/templates/addToken.tsx b/apps/dapp/templates/addToken.tsx index 7f86d0fca..89d2a25b9 100644 --- a/apps/dapp/templates/addToken.tsx +++ b/apps/dapp/templates/addToken.tsx @@ -2,13 +2,15 @@ import { useEffect } from 'react' import { useRecoilValueLoadable } from 'recoil' +import { + AddressInput, + InputErrorMessage, + InputLabel, + LogoNoBorder, +} from '@dao-dao/ui' import { XIcon } from '@heroicons/react/outline' import { useFormContext } from 'react-hook-form' -import { AddressInput } from '@components/input/AddressInput' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { InputLabel } from '@components/input/InputLabel' -import { LogoNoBorder } from '@components/Logo' import { tokenConfig } from 'selectors/daos' import { Config } from 'util/contractConfigWrapper' import { validateContractAddress, validateRequired } from 'util/formValidation' @@ -116,9 +118,9 @@ export const TokenSelector = ({
clearErrors(getLabel('to'))} + clearError={() => clearErrors(getLabel('address'))} setError={(message) => - setError(getLabel('to'), { type: 'manual', message }) + setError(getLabel('address'), { type: 'manual', message }) } />
diff --git a/apps/dapp/templates/changeMembers.tsx b/apps/dapp/templates/changeMembers.tsx index 2bd66387e..279e6db04 100644 --- a/apps/dapp/templates/changeMembers.tsx +++ b/apps/dapp/templates/changeMembers.tsx @@ -1,13 +1,11 @@ import { useRecoilValue } from 'recoil' +import { Button } from '@dao-dao/ui' +import { AddressInput, InputErrorMessage, TokenAmountInput } from '@dao-dao/ui' import { PlusIcon, UserIcon, XIcon } from '@heroicons/react/outline' import { useFieldArray, useFormContext } from 'react-hook-form' -import { Button } from 'ui/Button' import { FormCard } from '@components/FormCard' -import { AddressInput } from '@components/input/AddressInput' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { TokenAmountInput } from '@components/input/TokenAmountInput' import { listMembers } from 'selectors/multisigs' import { Config } from 'util/contractConfigWrapper' import { validateAddress, validateRequired } from 'util/formValidation' diff --git a/apps/dapp/templates/configUpdate.tsx b/apps/dapp/templates/configUpdate.tsx index b4e12e38e..d9e34eec1 100644 --- a/apps/dapp/templates/configUpdate.tsx +++ b/apps/dapp/templates/configUpdate.tsx @@ -1,20 +1,22 @@ import { Config as DAOConfig } from '@dao-dao/types/contracts/cw3-dao' -import { InformationCircleIcon, XIcon } from '@heroicons/react/outline' -import { useFormContext } from 'react-hook-form' - -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { InputLabel } from '@components/input/InputLabel' -import { NumberInput } from '@components/input/NumberInput' -import { TextInput } from '@components/input/TextInput' -import { ToggleInput } from '@components/input/ToggleInput' -import { DEFAULT_MAX_VOTING_PERIOD_SECONDS } from 'pages/dao/create' -import { Config } from 'util/contractConfigWrapper' +import { + InputErrorMessage, + InputLabel, + NumberInput, + TextInput, + ToggleInput, +} from '@dao-dao/ui' import { secondsToWdhms, convertDenomToMicroDenomWithDecimals, convertMicroDenomToDenomWithDecimals, getDaoThresholdAndQuorum, -} from 'util/conversion' +} from '@dao-dao/utils' +import { InformationCircleIcon, XIcon } from '@heroicons/react/outline' +import { useFormContext } from 'react-hook-form' + +import { DEFAULT_MAX_VOTING_PERIOD_SECONDS } from 'pages/dao/create' +import { Config } from 'util/contractConfigWrapper' import { validatePercent, validatePositive, @@ -29,11 +31,6 @@ import { ToCosmosMsgProps, } from './templateList' -enum ThresholdMode { - Threshold, - ThresholdQuorum, -} - export interface DAOConfigUpdateData { name: string description: string diff --git a/apps/dapp/templates/custom.tsx b/apps/dapp/templates/custom.tsx index ff8a86f6d..07969a58f 100644 --- a/apps/dapp/templates/custom.tsx +++ b/apps/dapp/templates/custom.tsx @@ -1,8 +1,8 @@ +import { CodeMirrorInput } from '@dao-dao/ui' import { CheckIcon, XIcon } from '@heroicons/react/outline' import JSON5 from 'json5' import { useFormContext } from 'react-hook-form' -import { CodeMirrorInput } from '@components/input/CodeMirrorInput' import { Config } from 'util/contractConfigWrapper' import { makeWasmMessage } from 'util/messagehelpers' import { validateCosmosMsg } from 'util/validateWasmMsg' diff --git a/apps/dapp/templates/mint.tsx b/apps/dapp/templates/mint.tsx index 4af58d6e8..758d7ed24 100644 --- a/apps/dapp/templates/mint.tsx +++ b/apps/dapp/templates/mint.tsx @@ -1,20 +1,18 @@ import { useRecoilValue } from 'recoil' +import { AddressInput, InputErrorMessage, NumberInput } from '@dao-dao/ui' +import { + convertDenomToMicroDenomWithDecimals, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' import { XIcon } from '@heroicons/react/outline' import { useFormContext } from 'react-hook-form' -import { AddressInput } from '@components/input/AddressInput' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { NumberInput } from '@components/input/NumberInput' import { Config, contractConfigSelector, ContractConfigWrapper, } from 'util/contractConfigWrapper' -import { - convertDenomToMicroDenomWithDecimals, - convertMicroDenomToDenomWithDecimals, -} from 'util/conversion' import { validateAddress, validatePositive, diff --git a/apps/dapp/templates/removeToken.tsx b/apps/dapp/templates/removeToken.tsx index d11792f00..f0d8f72f4 100644 --- a/apps/dapp/templates/removeToken.tsx +++ b/apps/dapp/templates/removeToken.tsx @@ -1,12 +1,10 @@ import { useRecoilValue, waitForAll } from 'recoil' +import { Button } from '@dao-dao/ui' +import { AddressInput, InputErrorMessage, InputLabel } from '@dao-dao/ui' import { XIcon } from '@heroicons/react/outline' import { useFormContext } from 'react-hook-form' -import { Button } from 'ui/Button' -import { AddressInput } from '@components/input/AddressInput' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { InputLabel } from '@components/input/InputLabel' import { cw20TokenInfo, cw20TokensList } from 'selectors/treasury' import { Config } from 'util/contractConfigWrapper' import { validateContractAddress, validateRequired } from 'util/formValidation' diff --git a/apps/dapp/templates/spend.tsx b/apps/dapp/templates/spend.tsx index 71725bf5d..028c71f7b 100644 --- a/apps/dapp/templates/spend.tsx +++ b/apps/dapp/templates/spend.tsx @@ -1,27 +1,30 @@ import { useRecoilValue, waitForAll } from 'recoil' +import { + AddressInput, + InputErrorMessage, + NumberInput, + SelectInput, +} from '@dao-dao/ui' +import { + NATIVE_DECIMALS, + NATIVE_DENOM, + convertDenomToHumanReadableDenom, + convertDenomToMicroDenomWithDecimals, + convertMicroDenomToDenomWithDecimals, + nativeTokenDecimals, + nativeTokenLabel, +} from '@dao-dao/utils' import { XIcon } from '@heroicons/react/outline' import { useFormContext } from 'react-hook-form' -import { AddressInput } from '@components/input/AddressInput' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { NumberInput } from '@components/input/NumberInput' -import { SelectInput } from '@components/input/SelectInput' import { cw20TokensList, cw20TokenInfo, nativeBalance as nativeBalanceSelector, cw20Balances as cw20BalancesSelector, } from 'selectors/treasury' -import { NATIVE_DECIMALS, NATIVE_DENOM } from 'util/constants' import { Config } from 'util/contractConfigWrapper' -import { - convertDenomToHumanReadableDenom, - convertDenomToMicroDenomWithDecimals, - convertMicroDenomToDenomWithDecimals, - nativeTokenDecimals, - nativeTokenLabel, -} from 'util/conversion' import { validateAddress, validatePositive, diff --git a/apps/dapp/templates/stake.tsx b/apps/dapp/templates/stake.tsx index b04f8f2e5..59754aad5 100644 --- a/apps/dapp/templates/stake.tsx +++ b/apps/dapp/templates/stake.tsx @@ -1,22 +1,25 @@ import { useRecoilValue } from 'recoil' -import { InformationCircleIcon, XIcon } from '@heroicons/react/outline' -import { useFormContext } from 'react-hook-form' - -import { AddressInput } from '@components/input/AddressInput' -import { InputErrorMessage } from '@components/input/InputErrorMessage' -import { NumberInput } from '@components/input/NumberInput' -import { SelectInput } from '@components/input/SelectInput' -import { nativeBalance as nativeBalanceSelector } from 'selectors/treasury' -import { NATIVE_DECIMALS, NATIVE_DENOM } from 'util/constants' -import { Config } from 'util/contractConfigWrapper' import { + AddressInput, + InputErrorMessage, + NumberInput, + SelectInput, +} from '@dao-dao/ui' +import { + NATIVE_DECIMALS, + NATIVE_DENOM, convertDenomToHumanReadableDenom, convertDenomToMicroDenomWithDecimals, convertMicroDenomToDenomWithDecimals, nativeTokenDecimals, nativeTokenLabel, -} from 'util/conversion' +} from '@dao-dao/utils' +import { InformationCircleIcon, XIcon } from '@heroicons/react/outline' +import { useFormContext } from 'react-hook-form' + +import { nativeBalance as nativeBalanceSelector } from 'selectors/treasury' +import { Config } from 'util/contractConfigWrapper' import { validateValidatorAddress, validatePositive, diff --git a/apps/dapp/templates/templateList.tsx b/apps/dapp/templates/templateList.tsx index 7b02a6cbe..bd0473ea6 100644 --- a/apps/dapp/templates/templateList.tsx +++ b/apps/dapp/templates/templateList.tsx @@ -158,7 +158,8 @@ export const messageTemplateAndValuesForDecodedCosmosMsg = ( msg: Record, props: FromCosmosMsgProps ) => { - // Ensure custom is the last message template since it will match most proposals and we return the first successful message match. + // Ensure custom is the last message template since it will match most + // proposals and we return the first successful message match. for (const template of messageTemplates) { const values = template.fromCosmosMsg(msg, props) if (values) { @@ -180,6 +181,7 @@ export interface TemplateComponentProps { multisig?: boolean readOnly?: boolean } + export type TemplateComponent = React.FunctionComponent // Defines a new template. diff --git a/apps/dapp/types/proposals.ts b/apps/dapp/types/proposals.ts deleted file mode 100644 index 0a08882e8..000000000 --- a/apps/dapp/types/proposals.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Proposal, ProposalResponse } from '@dao-dao/types/contracts/cw3-dao' - -import { MessageMap } from 'models/proposal/messageMap' - -export type ProposalKey = { - contractAddress: string - proposalId: string -} - -export type ProposalMessageKey = { - contractAddress: string - proposalId: string - messageId: string -} - -export interface ProposalMapItem { - proposal: Proposal - id: string - activeMessageIndex?: number - draft: boolean - messages?: MessageMap -} - -// Maps from a proposal id (a stringified number) -// to a ProposalMapItem -export type ProposalMap = { - [key: string]: ProposalMapItem -} - -// Maps from a contract address to a map of its draft -// proposals -export type ContractProposalMap = { - [key: string]: ProposalMap -} - -export interface ExtendedProposalResponse extends ProposalResponse { - draftId?: string -} diff --git a/apps/dapp/util/formValidation.ts b/apps/dapp/util/formValidation.ts index bc062f312..c6fcffc3c 100644 --- a/apps/dapp/util/formValidation.ts +++ b/apps/dapp/util/formValidation.ts @@ -42,7 +42,7 @@ export const validateContractAddress = (v: string) => export const validateJSON = (v: string) => { try { - const o = JSON5.parse(v) + JSON5.parse(v) return true } catch (e: any) { return e?.message as string diff --git a/apps/dapp/util/messagehelpers.ts b/apps/dapp/util/messagehelpers.ts index 10e7b01c9..3eb3e53fa 100644 --- a/apps/dapp/util/messagehelpers.ts +++ b/apps/dapp/util/messagehelpers.ts @@ -1,5 +1,3 @@ -import { useRecoilValue } from 'recoil' - import { toBase64, toAscii } from '@cosmjs/encoding' import { ExecuteMsg as MintExecuteMsg } from '@dao-dao/types/contracts/cw20-gov' import { @@ -8,7 +6,6 @@ import { StakingMsg, DistributionMsg, CosmosMsgFor_Empty, - ExecuteMsg, InstantiateMsg as DaoInstantiateMsg, Cw20Coin, Duration, @@ -20,21 +17,19 @@ import { InstantiateMsg as MultisigInstantiateMsg, Member, } from '@dao-dao/types/contracts/cw3-multisig' +import { + C4_GROUP_CODE_ID, + CW20_CODE_ID, + STAKE_CODE_ID, + convertDenomToHumanReadableDenom, +} from '@dao-dao/utils' import { MintMsg } from 'types/messages' -import { ProposalMapItem } from 'types/proposals' import { MessageMapEntry, ProposalMessageType, } from '../models/proposal/messageMap' -import { cw20TokenInfo } from '../selectors/treasury' -import { C4_GROUP_CODE_ID, CW20_CODE_ID, STAKE_CODE_ID } from './constants' -import { convertDenomToContractReadableDenom } from './conversion' -import { - convertDenomToHumanReadableDenom, - convertDenomToMicroDenomWithDecimals, -} from './conversion' const DENOM = convertDenomToHumanReadableDenom( process.env.NEXT_PUBLIC_STAKING_DENOM || '' @@ -355,45 +350,6 @@ export interface MessageAction { isEnabled: () => boolean } -export function labelForAmount(amount: Coin[]): string { - if (!amount?.length) { - return '' - } - return amount - .map((coin) => `${coin.amount !== '' ? coin.amount : '0'} ${coin.denom}`) - .join(', ') -} - -export function labelForMessage( - msg?: CosmosMsgFor_Empty | ExecuteMsg | MintExecuteMsg, - defaultMessage = '' -): string { - if (!msg) { - return defaultMessage - } - // TODO(gavin.doughtie): i18n - const anyMsg: any = msg - let messageString = '' - if (anyMsg.bank) { - if (anyMsg.bank.send) { - messageString = `${labelForAmount(anyMsg.bank.send.amount)} -> ${ - anyMsg.bank.send.to_address - }` - } else if (anyMsg.bank.burn) { - messageString = `${labelForAmount(anyMsg.bank.burn.amount)} -> 🔥` - } - } else if (anyMsg.mint) { - messageString = `${anyMsg.mint.amount} -> ${anyMsg.mint.recipient}` - } else if (anyMsg.custom) { - const customMap: { [k: string]: any } = anyMsg.custom - messageString = Object.entries(customMap) - .map(([key, value]) => `${key}: ${JSON.stringify(value)}`) - .join(', ') - messageString = messageString.slice(0, MAX_LABEL_LEN) || '' - } - return messageString -} - export function parseEncodedMessage(base64String?: string) { if (base64String) { const jsonMessage = decodeURIComponent(escape(atob(base64String))) @@ -531,80 +487,6 @@ export function isMintMsg(msg: any): msg is MintMsg { return false } -export function useMessageForDraftProposal( - draftProposal: ProposalMapItem, - govTokenAddress?: string -) { - const govTokenInfo = useRecoilValue(cw20TokenInfo(govTokenAddress as string)) - const msgs = draftProposal.messages - ? Object.values(draftProposal.messages).map((mapEntry) => { - // Spend proposals are inputted in human readable form (ex: - // junox). Contracts expect things in the micro form (ex: ujunox) - // so we, painfully, do some conversions: - if (mapEntry.messageType === ProposalMessageType.Spend) { - // Without doing a deep copy here we run the risk of modifying - // fields of the message which are displayed in the UI. - let microMessage = JSON.parse(JSON.stringify(mapEntry.message)) - const bank = (microMessage as any).bank as BankMsg - if (!bank) { - return - } - - let amounts: Coin[] - let variant: string - if ('send' in bank) { - amounts = (bank as any).send.amount - variant = 'send' - } else if ('burn' in bank) { - amounts = (bank as any).burn.amount - variant = 'burn' - } else { - console.error(`unexpected bank message: (${JSON.stringify(bank)})`) - return - } - - const microAmounts = amounts.map((coin) => { - const microCoin = coin - microCoin.amount = convertDenomToMicroDenomWithDecimals( - coin.amount, - govTokenInfo.decimals - ) - microCoin.denom = convertDenomToContractReadableDenom(coin.denom) - return microCoin - }) as Coin[] - - ;(((microMessage as any).bank as any)[variant] as any).amount = - microAmounts - - return microMessage - } - if (mapEntry.messageType === ProposalMessageType.Mint) { - const mintMessage = JSON.parse(JSON.stringify(mapEntry.message)) - console.log(mintMessage) - if (mintMessage?.mint?.amount) { - mintMessage.mint.amount = convertDenomToMicroDenomWithDecimals( - mintMessage.mint.amount, - govTokenInfo.decimals - ) - } - return makeExecutableMintMessage( - mintMessage, - govTokenAddress as string - ) - } - return mapEntry.message - }) - : [] - const proposal = draftProposal.proposal - - const msg: Record = { - title: proposal.title, - description: proposal.description, - msgs, - } - return msg as any -} - export const getMintRecipient = (mintMsg?: MessageMapEntry) => { return mintMsg?.message?.mint?.recipient } diff --git a/apps/dapp/util/proposal.ts b/apps/dapp/util/proposal.ts index 90c56d353..e47f7b6ed 100644 --- a/apps/dapp/util/proposal.ts +++ b/apps/dapp/util/proposal.ts @@ -1,156 +1,13 @@ -import { TransactionInterface_UNSTABLE, useRecoilValue } from 'recoil' +import { useRecoilValue } from 'recoil' -import { Proposal, ProposalResponse } from '@dao-dao/types/contracts/cw3-dao' +import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' -import { - contractProposalMapAtom, - nextDraftProposalIdAtom, -} from 'atoms/proposals' -import { - EmptyProposalResponse, - EmptyThresholdResponse, -} from 'models/proposal/proposal' -import { - draftProposalsSelector, - proposalTallySelector, -} from 'selectors/proposals' -import { - ContractProposalMap, - ExtendedProposalResponse, - ProposalMap, - ProposalMapItem, -} from 'types/proposals' +import { proposalTallySelector } from 'selectors/proposals' import { contractConfigSelector, ContractConfigWrapper, } from './contractConfigWrapper' -import { convertMicroDenomToDenomWithDecimals } from './conversion' - -// Prefix used in IDs for draft proposals -const DRAFT_PROPOSAL_PREFFIX = 'draft:' - -export function isProposal( - proposal: Proposal | ProposalResponse | ProposalMapItem | undefined -): proposal is Proposal { - if (!proposal) { - return false - } - if ((proposal as ProposalMapItem)?.draft === false) { - return false - } - return (proposal as Proposal).proposer !== undefined -} - -export function draftProposalKey(proposalId: number): string { - return `${DRAFT_PROPOSAL_PREFFIX}${proposalId}` -} - -// Convenience function to create a draft proposal entry -export function draftProposalItem( - proposal: Proposal, - id: string -): ProposalMapItem { - return { - proposal, - id, - draft: true, - } -} - -export function isDraftProposalKey(proposalKey: string): boolean { - return proposalKey.startsWith(DRAFT_PROPOSAL_PREFFIX) -} - -export function draftProposalKeyNumber(proposalKey: string): number { - if (isDraftProposalKey(proposalKey)) { - const str = proposalKey.substring( - proposalKey.indexOf(DRAFT_PROPOSAL_PREFFIX) - ) - try { - const num = parseInt(str) - return num - } catch (e) { - console.error(e) - } - } - return -1 -} - -export const createDraftProposalTransaction = - (contractAddress: string, draftProposals: ProposalMap) => - ({ get, set }: TransactionInterface_UNSTABLE) => { - const contractProposalMap = get(contractProposalMapAtom) - const setContractProposalMap = ( - contractProposalMap: ContractProposalMap - ) => { - set(contractProposalMapAtom, contractProposalMap) - } - const nextDraftProposalId = get(nextDraftProposalIdAtom) - const incrementDraftProposalId = () => - set(nextDraftProposalIdAtom, nextDraftProposalId + 1) - - return ({ draftProposal }: { draftProposal: Proposal }) => { - const proposalKey = draftProposalKey(nextDraftProposalId) - setContractProposalMap({ - ...contractProposalMap, - [contractAddress]: { - ...draftProposals, - [proposalKey]: draftProposalItem(draftProposal, proposalKey), - }, - }) - incrementDraftProposalId() - } - } - -export const deleteDraftProposal = ( - draftProposals: ProposalMap, - proposalId: string -) => { - const updatedProposals = { ...draftProposals } - delete updatedProposals[proposalId + ''] - return updatedProposals -} - -export const deleteDraftProposalTransaction = - ({ - contractAddress, - proposalId, - }: { - contractAddress: string - proposalId: string - }) => - ({ get, set }: TransactionInterface_UNSTABLE) => { - const draftProposals = get(draftProposalsSelector(contractAddress)) - - return () => { - const updatedProposals = { ...draftProposals } - delete updatedProposals[proposalId + ''] - set(draftProposalsSelector(contractAddress), updatedProposals) - } - } - -export const draftProposalToExtendedResponse = (draft: ProposalMapItem) => { - const proposalResponse: ExtendedProposalResponse = { - ...EmptyProposalResponse, - ...draft.proposal, - status: 'draft' as any, - draftId: draft.id, - threshold: { ...EmptyThresholdResponse }, - total_weight: 0, - } - return proposalResponse -} - -export const draftProposalsToExtendedResponses = ( - draftProposals: ProposalMap -) => { - return draftProposals - ? Object.values(draftProposals).map((draft) => - draftProposalToExtendedResponse(draft) - ) - : [] -} export const useThresholdQuorum = ( contractAddress: string, diff --git a/apps/dapp/assets/icons/Abstain.svg b/packages/icons/assets/Abstain.svg similarity index 100% rename from apps/dapp/assets/icons/Abstain.svg rename to packages/icons/assets/Abstain.svg diff --git a/apps/dapp/assets/icons/Discord.svg b/packages/icons/assets/Discord.svg similarity index 100% rename from apps/dapp/assets/icons/Discord.svg rename to packages/icons/assets/Discord.svg diff --git a/apps/dapp/assets/icons/MemberCheck.svg b/packages/icons/assets/MemberCheck.svg similarity index 100% rename from apps/dapp/assets/icons/MemberCheck.svg rename to packages/icons/assets/MemberCheck.svg diff --git a/apps/dapp/components/icons/TriangleUp.tsx b/packages/icons/assets/TriangleUp.svg similarity index 58% rename from apps/dapp/components/icons/TriangleUp.tsx rename to packages/icons/assets/TriangleUp.svg index 0cc29e53b..b79e42af7 100644 --- a/apps/dapp/components/icons/TriangleUp.tsx +++ b/packages/icons/assets/TriangleUp.svg @@ -1,17 +1,11 @@ -import * as React from 'react' -import { SVGProps } from 'react' - -export const TriangleUp = (props: SVGProps) => ( -) diff --git a/apps/dapp/assets/icons/airplane.svg b/packages/icons/assets/airplane.svg similarity index 100% rename from apps/dapp/assets/icons/airplane.svg rename to packages/icons/assets/airplane.svg diff --git a/apps/dapp/assets/icons/arrowUpRight.svg b/packages/icons/assets/arrowUpRight.svg similarity index 100% rename from apps/dapp/assets/icons/arrowUpRight.svg rename to packages/icons/assets/arrowUpRight.svg diff --git a/apps/dapp/assets/icons/copy.svg b/packages/icons/assets/copy.svg similarity index 87% rename from apps/dapp/assets/icons/copy.svg rename to packages/icons/assets/copy.svg index 59e7dc3c1..fe675c746 100644 --- a/apps/dapp/assets/icons/copy.svg +++ b/packages/icons/assets/copy.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/apps/dapp/assets/icons/dao.svg b/packages/icons/assets/dao.svg similarity index 100% rename from apps/dapp/assets/icons/dao.svg rename to packages/icons/assets/dao.svg diff --git a/apps/dapp/assets/icons/executed.svg b/packages/icons/assets/executed.svg similarity index 100% rename from apps/dapp/assets/icons/executed.svg rename to packages/icons/assets/executed.svg diff --git a/apps/dapp/assets/icons/github.svg b/packages/icons/assets/github.svg similarity index 100% rename from apps/dapp/assets/icons/github.svg rename to packages/icons/assets/github.svg diff --git a/apps/dapp/assets/icons/message.svg b/packages/icons/assets/message.svg similarity index 100% rename from apps/dapp/assets/icons/message.svg rename to packages/icons/assets/message.svg diff --git a/apps/dapp/assets/icons/open.svg b/packages/icons/assets/open.svg similarity index 100% rename from apps/dapp/assets/icons/open.svg rename to packages/icons/assets/open.svg diff --git a/apps/dapp/assets/icons/passed.svg b/packages/icons/assets/passed.svg similarity index 100% rename from apps/dapp/assets/icons/passed.svg rename to packages/icons/assets/passed.svg diff --git a/apps/dapp/assets/icons/pencil.svg b/packages/icons/assets/pencil.svg similarity index 100% rename from apps/dapp/assets/icons/pencil.svg rename to packages/icons/assets/pencil.svg diff --git a/apps/dapp/assets/icons/rejected.svg b/packages/icons/assets/rejected.svg similarity index 100% rename from apps/dapp/assets/icons/rejected.svg rename to packages/icons/assets/rejected.svg diff --git a/apps/dapp/assets/icons/twitter.svg b/packages/icons/assets/twitter.svg similarity index 95% rename from apps/dapp/assets/icons/twitter.svg rename to packages/icons/assets/twitter.svg index 1cb8a6447..5ec228814 100644 --- a/apps/dapp/assets/icons/twitter.svg +++ b/packages/icons/assets/twitter.svg @@ -1,3 +1,3 @@ - + diff --git a/apps/dapp/assets/icons/votes.svg b/packages/icons/assets/votes.svg similarity index 100% rename from apps/dapp/assets/icons/votes.svg rename to packages/icons/assets/votes.svg diff --git a/packages/ui/components/Breadcrumbs.tsx b/packages/ui/components/Breadcrumbs.tsx new file mode 100644 index 000000000..1fc99ac00 --- /dev/null +++ b/packages/ui/components/Breadcrumbs.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' + +import Link from 'next/link' + +import { ArrowNarrowLeftIcon } from '@heroicons/react/outline' + +export interface BreadcrumbsProps { + crumbs: Array<[string, string]> +} + +export const Breadcrumbs: FC = ({ crumbs }) => ( +
    +
  • + + + + + +
  • + {crumbs.map(([link, name], idx) => ( +
  • + + {name} + + {idx != crumbs.length - 1 && '/'} +
  • + ))} +
+) diff --git a/packages/ui/Button/Button.stories.tsx b/packages/ui/components/Button/Button.stories.tsx similarity index 100% rename from packages/ui/Button/Button.stories.tsx rename to packages/ui/components/Button/Button.stories.tsx diff --git a/packages/ui/Button/Button.tsx b/packages/ui/components/Button/Button.tsx similarity index 98% rename from packages/ui/Button/Button.tsx rename to packages/ui/components/Button/Button.tsx index 23e6a2287..b30243efd 100644 --- a/packages/ui/Button/Button.tsx +++ b/packages/ui/components/Button/Button.tsx @@ -5,7 +5,7 @@ import { ForwardedRef, } from 'react' -import { Logo } from '@dao-dao/dapp/components/Logo' +import { Logo } from '../Logo' export interface ButtonProps extends ComponentPropsWithoutRef<'button'> { children: ReactNode diff --git a/packages/ui/Button/index.ts b/packages/ui/components/Button/index.ts similarity index 100% rename from packages/ui/Button/index.ts rename to packages/ui/components/Button/index.ts diff --git a/packages/ui/components/Claims/ClaimsAvaliableCard.tsx b/packages/ui/components/Claims/ClaimsAvaliableCard.tsx new file mode 100644 index 000000000..4f44ed2aa --- /dev/null +++ b/packages/ui/components/Claims/ClaimsAvaliableCard.tsx @@ -0,0 +1,45 @@ +import { FC } from 'react' + +import { TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20' + +import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils' + +import { LogoNoBorder } from '../Logo' + +export interface ClaimsAvaliableCardProps { + avaliable: number + tokenInfo: TokenInfoResponse + onClaim: () => void + loading: boolean +} + +export const ClaimsAvaliableCard: FC = ({ + avaliable, + tokenInfo, + onClaim, + loading, +}) => ( +
+

+ Unclaimed (unstaked ${tokenInfo.symbol}) +

+ {loading ? ( +
+ +
+ ) : ( +

+ {convertMicroDenomToDenomWithDecimals(avaliable, tokenInfo.decimals)}$ + {tokenInfo.symbol} +

+ )} +
+ +
+
+) diff --git a/packages/ui/components/Claims/ClaimsListItem.tsx b/packages/ui/components/Claims/ClaimsListItem.tsx new file mode 100644 index 000000000..92ebe26dc --- /dev/null +++ b/packages/ui/components/Claims/ClaimsListItem.tsx @@ -0,0 +1,109 @@ +import { FC, useEffect, useState } from 'react' + +import { CheckIcon } from '@heroicons/react/outline' + +import { + convertMicroDenomToDenomWithDecimals, + humanReadableDuration, + claimAvaliable, +} from '@dao-dao/utils' + +import { BalanceIcon } from '../ContractView/BalanceIcon' + +import { Duration } from '@dao-dao/types/contracts/cw3-dao' +import { Claim, TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20' + +function claimDurationRemaining(claim: Claim, blockHeight: number): Duration { + if (claimAvaliable(claim, blockHeight)) { + return { time: 0 } + } + if ('at_height' in claim.release_at) { + const releaseBlock = claim.release_at.at_height + return { height: releaseBlock - blockHeight } + } else if ('at_time' in claim.release_at) { + const currentTimeNs = new Date().getTime() * 1000000 + return { + time: + (Number(claim.release_at.at_time) - currentTimeNs) / 1000000000 || 0, // To seconds. + } + } + + // Unreachable. + return { time: 0 } +} + +export interface ClaimsListItemProps { + claim: Claim + unstakingDuration: Duration + blockHeight: number + tokenInfo: TokenInfoResponse + incrementClaimsAvaliable: (n: number) => void +} + +export const ClaimsListItem: FC = ({ + claim, + unstakingDuration, + blockHeight, + tokenInfo, + incrementClaimsAvaliable, +}: { + claim: Claim + unstakingDuration: Duration + blockHeight: number + tokenInfo: TokenInfoResponse + incrementClaimsAvaliable: (n: number) => void +}) => { + const avaliable = claimAvaliable(claim, blockHeight) + + const durationForHumans = humanReadableDuration(unstakingDuration) + const durationRemaining = claimDurationRemaining(claim, blockHeight) + + // Once the claim expires increment claims avaliable. + useEffect(() => { + if ('time' in durationRemaining) { + const id = setTimeout( + () => incrementClaimsAvaliable(Number(claim.amount)), + durationRemaining.time * 1000 + ) + return () => clearTimeout(id) + } + }, [claim.amount, durationRemaining, incrementClaimsAvaliable]) + + const [durationRemainingForHumans, setDurationRemainingForHumans] = useState( + humanReadableDuration(durationRemaining) + ) + + useEffect(() => { + const id = setInterval(() => { + setDurationRemainingForHumans((_) => + humanReadableDuration(claimDurationRemaining(claim, blockHeight)) + ) + }, 1000) + return () => clearInterval(id) + }, [claim, blockHeight, setDurationRemainingForHumans]) + + return ( +
+

+ + {convertMicroDenomToDenomWithDecimals( + claim.amount, + tokenInfo.decimals + )}{' '} + ${tokenInfo.symbol} +

+ + {avaliable ? ( +

+ Avaliable + +

+ ) : ( +
+

{durationRemainingForHumans || '0'} left

+

/ {durationForHumans}

+
+ )} +
+ ) +} diff --git a/packages/ui/components/Claims/index.ts b/packages/ui/components/Claims/index.ts new file mode 100644 index 000000000..4eaa05b4e --- /dev/null +++ b/packages/ui/components/Claims/index.ts @@ -0,0 +1,2 @@ +export * from './ClaimsListItem' +export * from './ClaimsAvaliableCard' diff --git a/packages/ui/components/ContractView/BalanceCard.tsx b/packages/ui/components/ContractView/BalanceCard.tsx new file mode 100644 index 000000000..40262b69d --- /dev/null +++ b/packages/ui/components/ContractView/BalanceCard.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react' + +import { LogoNoBorder } from '../Logo' +import { Button } from '../Button' +import { BalanceIcon } from './BalanceIcon' + +export interface BalanceCardProps { + denom: string + title: string + amount: string + onManage: () => void + loading: boolean +} + +export const BalanceCard: FC = ({ + denom, + title, + amount, + onManage, + loading, +}) => ( +
+

{title}

+ {loading ? ( +
+ +
+ ) : ( +
+ + {amount} ${denom} +
+ )} +
+ +
+
+) diff --git a/packages/ui/components/ContractView/BalanceIcon.tsx b/packages/ui/components/ContractView/BalanceIcon.tsx new file mode 100644 index 000000000..c3f59834b --- /dev/null +++ b/packages/ui/components/ContractView/BalanceIcon.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react' + +import { useThemeContext } from '../../theme' + +export interface BalanceIconProps { + iconURI?: string +} + +export const BalanceIcon: FC = ({ iconURI }) => { + const { accentColor } = useThemeContext() + + return ( +
+ ) +} diff --git a/packages/ui/components/ContractView/BalanceListItem.tsx b/packages/ui/components/ContractView/BalanceListItem.tsx new file mode 100644 index 000000000..3b3a69fc0 --- /dev/null +++ b/packages/ui/components/ContractView/BalanceListItem.tsx @@ -0,0 +1,11 @@ +import { FC, ReactNode } from 'react' + +export interface BalanceListItemProps { + children: ReactNode +} + +export const BalanceListItem: FC = ({ children }) => ( +
  • + {children} +
  • +) diff --git a/packages/ui/components/ContractView/ContractHeader.tsx b/packages/ui/components/ContractView/ContractHeader.tsx new file mode 100644 index 000000000..6de39d674 --- /dev/null +++ b/packages/ui/components/ContractView/ContractHeader.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react' + +import { EstablishedDate } from './EstablishedDate' +import { Logo } from '../Logo' + +import { HEADER_IMAGES_ENABLED } from '@dao-dao/utils' + +export interface ContractHeaderProps { + name: string + description: string + established: Date + imgUrl?: string +} + +export const ContractHeader: FC = ({ + name, + description, + established, + imgUrl, +}) => ( +
    + {imgUrl && HEADER_IMAGES_ENABLED ? ( +
    + ) : ( + + )} +
    +

    {name}

    + +
    +
    +

    {description}

    +
    +
    +) diff --git a/packages/ui/components/ContractView/EstablishedDate.tsx b/packages/ui/components/ContractView/EstablishedDate.tsx new file mode 100644 index 000000000..c4de28fd8 --- /dev/null +++ b/packages/ui/components/ContractView/EstablishedDate.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react' + +export interface EstablishedDateProps { + date: Date +} + +export const EstablishedDate: FC = ({ date }) => { + const formattedDate = date.toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + return

    Est. {formattedDate}

    +} diff --git a/packages/ui/components/ContractView/GovInfoListItem.tsx b/packages/ui/components/ContractView/GovInfoListItem.tsx new file mode 100644 index 000000000..e975f419c --- /dev/null +++ b/packages/ui/components/ContractView/GovInfoListItem.tsx @@ -0,0 +1,20 @@ +import { FC, ReactNode } from 'react' + +export interface GovInfoListItemProps { + icon: ReactNode + text: string + value: string +} + +export const GovInfoListItem: FC = ({ + icon, + text, + value, +}) => ( +
  • + + {icon} {text}: + + {value} +
  • +) diff --git a/packages/ui/components/ContractView/GradientHero.tsx b/packages/ui/components/ContractView/GradientHero.tsx new file mode 100644 index 000000000..9e84a6378 --- /dev/null +++ b/packages/ui/components/ContractView/GradientHero.tsx @@ -0,0 +1,31 @@ +import { FC, ReactNode } from 'react' + +import { useThemeContext } from '../../theme' + +export interface GradientHeroProps { + children: ReactNode +} + +export const GradientHero: FC = ({ children }) => { + const theme = useThemeContext() + const endStop = theme.theme === 'dark' ? '#111213' : '#FFFFFF' + const baseRgb = theme.accentColor + ? theme.accentColor.split('(')[1].split(')')[0] + : '73, 55, 192' + return ( +
    +
    + {children} +
    +
    + ) +} diff --git a/packages/ui/components/ContractView/HorizontalInfo.tsx b/packages/ui/components/ContractView/HorizontalInfo.tsx new file mode 100644 index 000000000..654d9a44b --- /dev/null +++ b/packages/ui/components/ContractView/HorizontalInfo.tsx @@ -0,0 +1,26 @@ +import { Children, FC, ReactNode } from 'react' + +export interface HorizontalInfoProps { + children: ReactNode +} + +export const HorizontalInfoSection: FC = ({ + children, +}) => ( +
    + {children} +
    +) + +export const HorizontalInfo: FC = ({ children }) => { + const childList = Children.toArray(children) + return ( +
    +
      + {Children.map(childList, (child) => ( +
    • {child}
    • + ))} +
    +
    + ) +} diff --git a/packages/ui/components/ContractView/ProposalList.tsx b/packages/ui/components/ContractView/ProposalList.tsx new file mode 100644 index 000000000..3aac7c0d2 --- /dev/null +++ b/packages/ui/components/ContractView/ProposalList.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react' + +import Link from 'next/link' + +import { ProposalResponse } from '@dao-dao/types/contracts/cw3-dao' + +import { getProposalEnd, zeroPad } from '@dao-dao/utils' + +import { ProposalStatus } from '../ProposalStatus' + +export interface ProposalLineProps { + proposal: ProposalResponse + proposalViewUrl: string +} + +export const ProposalLine: FC = ({ + proposal, + proposalViewUrl, +}) => ( + + +
    +
    +

    # {zeroPad(proposal.id, 6)}

    + +
    +

    {proposal.title}

    +

    + {getProposalEnd(proposal.expires, proposal.status)} +

    +
    +
    + +) diff --git a/packages/ui/components/ContractView/StarButton.tsx b/packages/ui/components/ContractView/StarButton.tsx new file mode 100644 index 000000000..8425acd27 --- /dev/null +++ b/packages/ui/components/ContractView/StarButton.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react' + +import { StarIcon as StarOutline } from '@heroicons/react/outline' +import { StarIcon as StarSolid } from '@heroicons/react/solid' + +import { useThemeContext } from '../../theme' + +export interface StarButtonProps { + pinned: boolean + onPin: () => void +} + +export const StarButton: FC = ({ pinned, onPin }) => { + const { accentColor } = useThemeContext() + + return ( + + ) +} diff --git a/packages/ui/components/ContractView/TreasuryView.tsx b/packages/ui/components/ContractView/TreasuryView.tsx new file mode 100644 index 000000000..3fa32a164 --- /dev/null +++ b/packages/ui/components/ContractView/TreasuryView.tsx @@ -0,0 +1,69 @@ +import { FC } from 'react' + +import { + nativeTokenLabel, + nativeTokenLogoURI, + convertMicroDenomToDenomWithDecimals, + convertDenomToHumanReadableDenom, + NATIVE_DENOM, +} from '@dao-dao/utils' + +import { BalanceListItem } from './BalanceListItem' +import { BalanceIcon } from './BalanceIcon' + +export interface TreasuryBalancesProps { + nativeTokens: { + denom: string + amount: string + decimals: number + }[] + cw20Tokens: { + symbol: string + amount: string + decimals: number + }[] +} + +export const TreasuryBalances: FC = ({ + nativeTokens, + cw20Tokens, +}) => ( +
      + {nativeTokens.map(({ denom, amount, decimals }) => { + const symbol = nativeTokenLabel(denom) + const icon = nativeTokenLogoURI(denom) + return ( + + + {convertMicroDenomToDenomWithDecimals( + amount, + decimals + ).toLocaleString(undefined, { + maximumFractionDigits: 20, + })}{' '} + ${symbol} + + ) + })} + {!nativeTokens.length && ( + + 0 $ + {convertDenomToHumanReadableDenom(NATIVE_DENOM).toUpperCase()} + + )} + {cw20Tokens.map(({ symbol, amount, decimals }) => { + return ( + + + {convertMicroDenomToDenomWithDecimals( + amount, + decimals + ).toLocaleString(undefined, { + maximumFractionDigits: decimals, + })}{' '} + ${symbol} + + ) + })} +
    +) diff --git a/packages/ui/components/ContractView/index.tsx b/packages/ui/components/ContractView/index.tsx new file mode 100644 index 000000000..ebb82331b --- /dev/null +++ b/packages/ui/components/ContractView/index.tsx @@ -0,0 +1,11 @@ +export * from './ContractHeader' +export * from './EstablishedDate' +export * from './GradientHero' +export * from './HorizontalInfo' +export * from './StarButton' +export * from './GovInfoListItem' +export * from './BalanceCard' +export * from './BalanceIcon' +export * from './BalanceListItem' +export * from './TreasuryView' +export * from './ProposalList' diff --git a/apps/dapp/components/CopyToClipboard.tsx b/packages/ui/components/CopyToClipboard.tsx similarity index 91% rename from apps/dapp/components/CopyToClipboard.tsx rename to packages/ui/components/CopyToClipboard.tsx index 7a1bca5f8..f114be947 100644 --- a/apps/dapp/components/CopyToClipboard.tsx +++ b/packages/ui/components/CopyToClipboard.tsx @@ -1,10 +1,9 @@ import { useState } from 'react' +import { Copy } from '@dao-dao/icons' +import { useThemeContext } from '@dao-dao/ui' import { CheckCircleIcon } from '@heroicons/react/outline' import toast from 'react-hot-toast' -import { useThemeContext } from 'ui' - -import SvgCopy from './icons/Copy' function concatAddressImpl( address: string, @@ -46,7 +45,7 @@ export function CopyToClipboard({ {copied ? ( ) : ( - + )} {concatAddress(value, takeN)} diff --git a/apps/dapp/components/CosmosMessageDisplay.tsx b/packages/ui/components/CosmosMessageDisplay.tsx similarity index 81% rename from apps/dapp/components/CosmosMessageDisplay.tsx rename to packages/ui/components/CosmosMessageDisplay.tsx index 65d369944..4c11cca21 100644 --- a/apps/dapp/components/CosmosMessageDisplay.tsx +++ b/packages/ui/components/CosmosMessageDisplay.tsx @@ -1,8 +1,10 @@ +import { FC } from 'react' +import { useThemeContext } from '../theme' + import 'codemirror/lib/codemirror.css' import 'codemirror/theme/material-ocean.css' import { UnControlled as CodeMirror } from 'react-codemirror2' -import { useThemeContext } from 'ui' import 'codemirror/lib/codemirror.css' import 'codemirror/theme/material.css' @@ -11,7 +13,13 @@ if (typeof window !== 'undefined' && typeof window.navigator !== 'undefined') { require('codemirror/mode/javascript/javascript.js') } -export const CosmosMessageDisplay = ({ value }: { value: string }) => { +export interface CosmosMessageDisplayProps { + value: string +} + +export const CosmosMessageDisplay: FC = ({ + value, +}) => { const themeCtx = useThemeContext() const editorTheme = themeCtx.theme !== 'dark' ? 'default' : 'material-ocean' return ( diff --git a/apps/dapp/components/Execute.tsx b/packages/ui/components/Execute.tsx similarity index 93% rename from apps/dapp/components/Execute.tsx rename to packages/ui/components/Execute.tsx index ad9d621e6..b6b20f7fa 100644 --- a/apps/dapp/components/Execute.tsx +++ b/packages/ui/components/Execute.tsx @@ -1,9 +1,9 @@ import { useState } from 'react' import { FC } from 'react' -import { Button } from 'ui' +import { Button } from './Button' -import SvgAirplane from './icons/Airplane' +import { Airplane } from '@dao-dao/icons' export interface ExecuteProps { onExecute: () => void @@ -11,6 +11,8 @@ export interface ExecuteProps { loading: boolean } +export const Hmm: FC<{}> = () => null + export const Execute: FC = ({ onExecute, messages, loading }) => { const [partyMode, setPartMode] = useState(false) const [partyPhase, setPartyPhase] = useState(1) @@ -29,7 +31,7 @@ export const Execute: FC = ({ onExecute, messages, loading }) => {

    )} @@ -93,7 +95,7 @@ export const Execute: FC = ({ onExecute, messages, loading }) => { loading={loading} onClick={() => onExecute()} > - Execute + Execute diff --git a/apps/dapp/components/GradientWrapper.tsx b/packages/ui/components/GradientWrapper.tsx similarity index 70% rename from apps/dapp/components/GradientWrapper.tsx rename to packages/ui/components/GradientWrapper.tsx index 0d2bbca50..bc36e74a5 100644 --- a/apps/dapp/components/GradientWrapper.tsx +++ b/packages/ui/components/GradientWrapper.tsx @@ -1,10 +1,14 @@ -import { ReactNode } from 'react' +import { ReactNode, FC } from 'react' -import { useThemeContext } from 'ui' +import { useThemeContext } from '../theme' import { LogoNoBorder } from './Logo' -export function GradientWrapper({ children }: { children: ReactNode }) { +export interface GradientWrapperProps { + children: ReactNode +} + +export const GradientWrapper: FC = ({ children }) => { const theme = useThemeContext() const bg = theme.theme === 'dark' @@ -12,10 +16,10 @@ export function GradientWrapper({ children }: { children: ReactNode }) { : 'url(/gradients/BG-Gradient-Light@2x.png)' return ( -
    +
    {CSS.supports('backdrop-filter', 'blur(5px)') && (
    diff --git a/apps/dapp/components/LoadingScreen.tsx b/packages/ui/components/LoadingScreen.tsx similarity index 68% rename from apps/dapp/components/LoadingScreen.tsx rename to packages/ui/components/LoadingScreen.tsx index ed4e0fcc9..fe8c710ee 100644 --- a/apps/dapp/components/LoadingScreen.tsx +++ b/packages/ui/components/LoadingScreen.tsx @@ -1,6 +1,6 @@ -import { Logo } from 'components/Logo' +import { Logo } from './Logo' -function LoadingScreen() { +export const LoadingScreen = () => { return (
    @@ -9,5 +9,3 @@ function LoadingScreen() {
    ) } - -export default LoadingScreen diff --git a/apps/dapp/components/Logo.tsx b/packages/ui/components/Logo.tsx similarity index 100% rename from apps/dapp/components/Logo.tsx rename to packages/ui/components/Logo.tsx diff --git a/packages/ui/components/MarkdownPreview.tsx b/packages/ui/components/MarkdownPreview.tsx new file mode 100644 index 000000000..db8e25d11 --- /dev/null +++ b/packages/ui/components/MarkdownPreview.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react' +import ReactMarkdown from 'react-markdown' + +export interface MarkdownPreviewProps { + markdown: string +} + +export const MarkdownPreview: FC = ({ markdown }) => ( + + {markdown} + +) diff --git a/packages/ui/components/Modal.tsx b/packages/ui/components/Modal.tsx new file mode 100644 index 000000000..0ac60c223 --- /dev/null +++ b/packages/ui/components/Modal.tsx @@ -0,0 +1,11 @@ +import { ReactNode, FC } from 'react' + +export interface ModalProps { + children: ReactNode +} + +export const Modal: FC = ({ children }) => ( +
    + {children} +
    +) diff --git a/apps/dapp/components/Progress.tsx b/packages/ui/components/Progress.tsx similarity index 100% rename from apps/dapp/components/Progress.tsx rename to packages/ui/components/Progress.tsx diff --git a/packages/ui/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/components/ProposalDetails/ProposalDetails.tsx new file mode 100644 index 000000000..dd8b394bd --- /dev/null +++ b/packages/ui/components/ProposalDetails/ProposalDetails.tsx @@ -0,0 +1,129 @@ +import { ProposalResponse } from '@dao-dao/types/contracts/cw3-dao' +import { FC, ReactNode, useState } from 'react' +import { MarkdownPreview } from '../MarkdownPreview' +import { Button } from '../Button' +import { Execute } from '../Execute' +import { Vote, VoteChoice } from '../Vote' +import { EyeOffIcon, EyeIcon } from '@heroicons/react/outline' +import { ProposalMessageTemplateList } from './ProposalMessageTemplateList' +import { CosmosMessageDisplay } from '../CosmosMessageDisplay' +import { decodedMessagesString, decodeMessages } from '@dao-dao/utils' + +export interface ProposalDetailsProps { + proposal: ProposalResponse + walletVote: string | undefined + walletWeightPercent: number + loading: boolean + showStaking: boolean + setShowStaking: (value: boolean) => void + stakingModal?: ReactNode + // Transformer to convert a decoded message into a displayable ReactNode. The + // caller will likely use this to transform these messages into template + // components. Once we have a state package we will want to move + // templates into their own package and then this can likely be removed. + messageToDisplay: (message: { [key: string]: any }) => ReactNode + onExecute: () => void + onVote: (choice: VoteChoice) => void +} + +export const ProposalDetails: FC = ({ + proposal, + walletVote, + walletWeightPercent, + loading, + stakingModal, + showStaking, + setShowStaking, + messageToDisplay, + onExecute, + onVote, +}) => { + const decodedMessages = decodeMessages(proposal.msgs) + const [showRaw, setShowRaw] = useState(false) + + return ( +
    +
    +

    {proposal.title}

    +
    +
    + +
    +
    Messages
    +
    + {decodedMessages?.length ? ( + showRaw ? ( + + ) : ( + + ) + ) : ( +
    []
    + )} +
    + {!!decodedMessages.length && ( +
    + +
    + )} + {proposal.status === 'passed' && ( + <> +

    Status

    + + + )} +

    Vote

    + {proposal.status === 'open' && + !walletVote && + walletWeightPercent !== 0 && ( + + )} + {walletVote && ( +

    You voted {walletVote} on this proposal.

    + )} + {proposal.status !== 'open' && !walletVote && ( +

    You did not vote on this proposal.

    + )} + {walletWeightPercent === 0 && ( +

    + You must have voting power at the time of proposal creation to vote.{' '} + {stakingModal && ( + + )} + {stakingModal && showStaking && stakingModal} +

    + )} +
    + ) +} diff --git a/packages/ui/components/ProposalDetails/ProposalDetailsSidebar.tsx b/packages/ui/components/ProposalDetails/ProposalDetailsSidebar.tsx new file mode 100644 index 000000000..383ab7855 --- /dev/null +++ b/packages/ui/components/ProposalDetails/ProposalDetailsSidebar.tsx @@ -0,0 +1,609 @@ +import { FC } from 'react' +import { + ProposalResponse, + ProposalTallyResponse, +} from '@dao-dao/types/contracts/cw3-dao' +import { ProposalStatus } from '../ProposalStatus' +import { Tooltip } from '../Tooltip' +import { Progress } from '../Progress' +import { CopyToClipboard } from '../CopyToClipboard' +import { TriangleUp, Abstain } from '@dao-dao/icons' +import { ExternalLinkIcon, CheckIcon, XIcon } from '@heroicons/react/outline' +import { + CHAIN_TXN_URL_PREFIX, + convertMicroDenomToDenomWithDecimals, + expirationAtTimeToSecondsFromNow, + secondsToWdhms, +} from '@dao-dao/utils' + +// Need this for now as the `WalletVote` enum is stored in dapp state. TODO: +// remove once we break state into a package. +export enum WalletVote { + Yes = 'yes', + No = 'no', + Abstain = 'abstain', + Veto = 'veto', +} + +export interface ProposalDetailsCardProps { + proposal: ProposalResponse + memberWhenProposalCreated: boolean + walletVote: WalletVote + proposalExecutionTXHash: string | undefined +} + +export interface ProposalDetailsVoteStatusProps { + proposal: ProposalResponse + tally: ProposalTallyResponse + tokenDecimals: number + // Undefined if max voting period is in blocks. + maxVotingSeconds?: number + threshold?: { + absolute?: number + percent: number + display: string + } + quorum?: { + percent: number + display: string + } +} + +export interface YouTooltipProps { + label: string +} + +export const YouTooltip: FC = ({ label }) => ( + +

    + ? +

    +
    +) + +export const ProposalDetailsCard: FC = ({ + proposal, + memberWhenProposalCreated, + walletVote, + proposalExecutionTXHash, +}) => ( +
    +
    +
    +

    + Proposal +

    + +

    + # {proposal.id.toString().padStart(6, '0')} +

    +
    + +
    + +
    +

    + Status +

    + +

    + +

    +
    + +
    + +
    +

    + You +

    + + {!memberWhenProposalCreated ? ( + + ) : walletVote === WalletVote.Yes ? ( +

    + Yes +

    + ) : walletVote === WalletVote.No ? ( +

    + No +

    + ) : walletVote === WalletVote.Abstain ? ( +

    + Abstain +

    + ) : walletVote === WalletVote.Veto ? ( +

    + Veto +

    + ) : walletVote ? ( +

    + Unknown: {walletVote} +

    + ) : proposal.status === 'open' ? ( + + ) : ( + + )} +
    +
    +
    +

    Proposer

    + + + + {proposal.status === 'executed' && !proposalExecutionTXHash ? ( + <> +

    TX

    +

    Loading...

    + + ) : !!proposalExecutionTXHash ? ( + <> + {CHAIN_TXN_URL_PREFIX ? ( + + TX + + + ) : ( +

    TX

    + )} + + + + ) : null} +
    +
    +) + +export const ProposalDetailsVoteStatus: FC = ({ + proposal, + tally, + tokenDecimals, + maxVotingSeconds, + threshold, + quorum, +}) => { + const localeOptions = { maximumSignificantDigits: 3 } + + const yesVotes = Number( + convertMicroDenomToDenomWithDecimals(tally.votes.yes, tokenDecimals) + ) + const noVotes = Number( + convertMicroDenomToDenomWithDecimals(tally.votes.no, tokenDecimals) + ) + const abstainVotes = Number( + convertMicroDenomToDenomWithDecimals(tally.votes.abstain, tokenDecimals) + ) + + const totalWeight = Number( + convertMicroDenomToDenomWithDecimals(tally.total_weight, tokenDecimals) + ) + + const turnoutTotal = yesVotes + noVotes + abstainVotes + const turnoutYesPercent = turnoutTotal ? (yesVotes / turnoutTotal) * 100 : 0 + const turnoutNoPercent = turnoutTotal ? (noVotes / turnoutTotal) * 100 : 0 + const turnoutAbstainPercent = turnoutTotal + ? (abstainVotes / turnoutTotal) * 100 + : 0 + + const turnoutPercent = (turnoutTotal / totalWeight) * 100 + const totalYesPercent = (yesVotes / totalWeight) * 100 + const totalNoPercent = (noVotes / totalWeight) * 100 + const totalAbstainPercent = (abstainVotes / totalWeight) * 100 + + // When only abstain votes have been cast and there is no quorum, + // align the abstain progress bar to the right to line up with Abstain + // text. + const onlyAbstain = yesVotes === 0 && noVotes === 0 && abstainVotes > 0 + + const expiresInSeconds = + proposal.expires && 'at_time' in proposal.expires + ? expirationAtTimeToSecondsFromNow(proposal.expires) + : undefined + + // TODO: Change this wen v1 contracts launch, since the conditions + // will change. In v1, all abstain fails instead of passes. + const thresholdReached = + !!threshold && + yesVotes >= + ((quorum ? turnoutTotal : totalWeight) - abstainVotes) * + (threshold.percent / 100) + const quorumMet = !!quorum && turnoutPercent >= quorum.percent + + const helpfulStatusText = + proposal.status === 'open' && threshold && quorum + ? thresholdReached && quorumMet + ? 'If the current vote stands, this proposal will pass.' + : !thresholdReached && quorumMet + ? "If the current vote stands, this proposal will fail because insufficient 'Yes' votes have been cast." + : thresholdReached && !quorumMet + ? 'If the current vote stands, this proposal will fail due to a lack of voter participation.' + : undefined + : undefined + return ( +
    + {helpfulStatusText && ( +

    + {helpfulStatusText} +

    + )} + + {threshold ? ( + quorum ? ( + <> +

    Ratio of votes

    + +
    + {[ +

    + Yes{' '} + {turnoutYesPercent.toLocaleString(undefined, localeOptions)}% +

    , +

    + No {turnoutNoPercent.toLocaleString(undefined, localeOptions)} + % +

    , + ] + .sort(() => yesVotes - noVotes) + .map((elem, idx) => ( +
    + {elem} +
    + ))} +

    + Abstain{' '} + {turnoutAbstainPercent.toLocaleString(undefined, localeOptions)} + % +

    +
    + +
    + b.value - a.value), + { + value: Number(turnoutAbstainPercent), + // Secondary is dark with 80% opacity. + color: 'rgba(var(--dark), 0.8)', + }, + ], + }, + ]} + verticalBars={ + threshold && [ + { + value: threshold.percent, + color: 'rgba(var(--dark), 0.5)', + }, + ] + } + /> +
    + +
    + 90 + ? 'calc(100% - 32px)' + : `calc(${threshold.percent}% - 17px)`, + }} + width="36px" + /> + + +
    +

    + Passing threshold:{' '} + {threshold.display} +

    + +

    + {thresholdReached ? ( + <> + Passing{' '} + + + ) : ( + <> + Failing{' '} + + + )} +

    +
    +
    +
    + +
    +

    + Turnout +

    + +

    + {turnoutPercent.toLocaleString(undefined, localeOptions)}% +

    +
    + +
    + +
    + +
    + 90 + ? 'calc(100% - 32px)' + : `calc(${quorum.percent}% - 17px)`, + }} + width="36px" + /> + + +
    +

    + Quorum: {quorum.display} +

    + +

    + {quorumMet ? ( + <> + Reached{' '} + + + ) : ( + <> + Not met{' '} + + + )} +

    +
    +
    +
    + + ) : ( + <> +

    + Turnout +

    + +
    + {[ +

    + Yes {totalYesPercent.toLocaleString(undefined, localeOptions)} + % +

    , +

    + No {totalNoPercent.toLocaleString(undefined, localeOptions)}% +

    , + ] + .sort(() => yesVotes - noVotes) + .map((elem, idx) => ( +
    + {elem} +
    + ))} +

    + Abstain{' '} + {totalAbstainPercent.toLocaleString(undefined, localeOptions)}% +

    +
    + +
    + b.value - a.value), + { + value: Number(totalAbstainPercent), + // Secondary is dark with 80% opacity. + color: 'rgba(var(--dark), 0.8)', + }, + ], + }, + ]} + verticalBars={[ + { + value: threshold.percent, + color: 'rgba(var(--dark), 0.5)', + }, + ]} + /> +
    + +
    + 90 + ? 'calc(100% - 32px)' + : `calc(${threshold.percent}% - 17px)`, + }} + width="36px" + /> + + +
    +

    + Passing threshold:{' '} + {threshold.display} +

    + +

    + {thresholdReached ? ( + <> + Reached{' '} + + + ) : ( + <> + Not met{' '} + + + )} +

    +
    +
    +
    + + ) + ) : null} + + {proposal.status === 'open' && + expiresInSeconds !== undefined && + expiresInSeconds > 0 && ( + <> +

    + Time left +

    + +

    + {secondsToWdhms(expiresInSeconds, 2)} +

    + + {maxVotingSeconds !== undefined && ( +
    + +
    + )} + + )} + + {threshold?.percent === 50 && yesVotes === noVotes && ( +
    +

    Tie clarification

    + +

    {"'Yes' will win a tie vote."}

    +
    + )} + + {turnoutTotal > 0 && abstainVotes === turnoutTotal && ( +
    +

    All abstain clarification

    + +

    + {/* TODO: Change this to fail wen v1 contracts. */} + When all abstain, a proposal will pass. +

    +
    + )} +
    + ) +} diff --git a/packages/ui/components/ProposalDetails/ProposalMessageTemplateList.tsx b/packages/ui/components/ProposalDetails/ProposalMessageTemplateList.tsx new file mode 100644 index 000000000..92e43a56c --- /dev/null +++ b/packages/ui/components/ProposalDetails/ProposalMessageTemplateList.tsx @@ -0,0 +1,19 @@ +import { CosmosMsgFor_Empty } from '@dao-dao/types/contracts/cw3-dao' +import { FC, ReactNode } from 'react' +import { decodeMessages } from '@dao-dao/utils' + +interface ProposalMessageTemplateListProps { + msgs: CosmosMsgFor_Empty[] + messageToDisplay: (message: { [key: string]: any }) => ReactNode +} + +export const ProposalMessageTemplateList: FC< + ProposalMessageTemplateListProps +> = ({ msgs, messageToDisplay }: ProposalMessageTemplateListProps) => { + const components: ReactNode[] = msgs.map((msg, index) => { + const decoded = decodeMessages([msg])[0] + return
    {messageToDisplay(decoded)}
    + }) + + return <>{components} +} diff --git a/packages/ui/components/ProposalDetails/index.tsx b/packages/ui/components/ProposalDetails/index.tsx new file mode 100644 index 000000000..9bc49cf7a --- /dev/null +++ b/packages/ui/components/ProposalDetails/index.tsx @@ -0,0 +1,2 @@ +export * from './ProposalDetails' +export * from './ProposalDetailsSidebar' diff --git a/apps/dapp/components/ProposalStatus/ProposalStatus.stories.tsx b/packages/ui/components/ProposalStatus/ProposalStatus.stories.tsx similarity index 100% rename from apps/dapp/components/ProposalStatus/ProposalStatus.stories.tsx rename to packages/ui/components/ProposalStatus/ProposalStatus.stories.tsx diff --git a/apps/dapp/components/ProposalStatus/ProposalStatus.tsx b/packages/ui/components/ProposalStatus/ProposalStatus.tsx similarity index 84% rename from apps/dapp/components/ProposalStatus/ProposalStatus.tsx rename to packages/ui/components/ProposalStatus/ProposalStatus.tsx index 0919fcd4c..d096e176c 100644 --- a/apps/dapp/components/ProposalStatus/ProposalStatus.tsx +++ b/packages/ui/components/ProposalStatus/ProposalStatus.tsx @@ -1,4 +1,4 @@ -import { StatusIcons } from '@components' +import { StatusIcons } from '../StatusIcons' type ProposalStatusProps = { status: string } diff --git a/apps/dapp/components/ProposalStatus/index.ts b/packages/ui/components/ProposalStatus/index.ts similarity index 100% rename from apps/dapp/components/ProposalStatus/index.ts rename to packages/ui/components/ProposalStatus/index.ts diff --git a/packages/ui/components/StakingModal/ActionButton.tsx b/packages/ui/components/StakingModal/ActionButton.tsx new file mode 100644 index 000000000..ca09daf0e --- /dev/null +++ b/packages/ui/components/StakingModal/ActionButton.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react' +import { StakingMode, stakingModeString } from './StakingModal' +import { Button } from '../Button' +import { Tooltip } from '../Tooltip' + +export interface ActionButtonProps { + error: string | undefined + loading: boolean + mode: StakingMode + onClick: () => void +} + +export const ActionButton: FC = ({ + error, + mode, + loading, + onClick, +}) => ( + + + +) diff --git a/packages/ui/components/StakingModal/AmountSelector.tsx b/packages/ui/components/StakingModal/AmountSelector.tsx new file mode 100644 index 000000000..dedbad9b0 --- /dev/null +++ b/packages/ui/components/StakingModal/AmountSelector.tsx @@ -0,0 +1,44 @@ +import { ChevronRightIcon, ChevronLeftIcon } from '@heroicons/react/outline' + +import { ChangeEvent, FC } from 'react' + +export interface AmountSelectorProps { + setAmount: (newValue: number) => void + amount: number + max: number +} + +export const AmountSelector: FC = ({ + setAmount, + amount, + max, +}) => ( +
    + + ) => + setAmount(e.target.valueAsNumber) + } + type="number" + value={amount} + /> + +
    +) diff --git a/packages/ui/components/StakingModal/ModeButton.tsx b/packages/ui/components/StakingModal/ModeButton.tsx new file mode 100644 index 000000000..fa7b592fe --- /dev/null +++ b/packages/ui/components/StakingModal/ModeButton.tsx @@ -0,0 +1,24 @@ +import { FC, ReactNode } from 'react' + +export interface ModeButtonProps { + onClick: () => void + active: boolean + children: ReactNode +} + +export const ModeButton: FC = ({ + onClick, + active, + children, +}) => ( + +) diff --git a/packages/ui/components/StakingModal/PercentSelector.tsx b/packages/ui/components/StakingModal/PercentSelector.tsx new file mode 100644 index 000000000..a775d0583 --- /dev/null +++ b/packages/ui/components/StakingModal/PercentSelector.tsx @@ -0,0 +1,43 @@ +import { FC } from 'react' + +export interface PercentSelectorProps { + max: number + amount: number + tokenDecimals: number + setAmount: (newAmount: number) => void +} + +export const PercentSelector: FC = ({ + max, + amount, + setAmount, + tokenDecimals, +}) => { + const active = (p: number) => max * p === amount + const getClassName = (p: number) => + 'rounded-md transition hover:bg-secondary link-text font-normal px-2 py-1' + + (active(p) ? ' bg-secondary border border-inactive' : '') + const getOnClick = (p: number) => () => { + setAmount(Number((p * max).toFixed(tokenDecimals))) + } + + return ( +
    + + + + + +
    + ) +} diff --git a/packages/ui/components/StakingModal/StakingModal.tsx b/packages/ui/components/StakingModal/StakingModal.tsx new file mode 100644 index 000000000..33973f798 --- /dev/null +++ b/packages/ui/components/StakingModal/StakingModal.tsx @@ -0,0 +1,257 @@ +import { FC, useState } from 'react' +import { Modal } from '../Modal' +import { ModeButton } from './ModeButton' +import { AmountSelector } from './AmountSelector' +import { PercentSelector } from './PercentSelector' +import { ActionButton } from './ActionButton' +import { Duration } from '@dao-dao/types/contracts/cw3-dao' +import { + durationIsNonZero, + humanReadableDuration, + convertMicroDenomToDenomWithDecimals, +} from '@dao-dao/utils' + +import { XIcon } from '@heroicons/react/outline' + +export enum StakingMode { + Stake, + Unstake, + Claim, +} + +export const stakingModeString = (mode: StakingMode) => { + switch (mode) { + case StakingMode.Stake: + return 'stake' + case StakingMode.Unstake: + return 'unstake' + case StakingMode.Claim: + return 'claim' + default: + return 'internal error' + } +} + +export interface StakingModalProps { + // The mode to start the staking modal in. + defaultMode: StakingMode + // The number of tokens in question. + amount: number + // Sets the number of tokens in question. + setAmount: (newAmount: number) => void + // Called when the staking modal is closed. + onClose: () => void + // The number of tokens that are currently claimable. + claimableTokens: number + // The number of tokens that are unstakable. + unstakableTokens: number + // The number of tokens that are stakable. + stakableTokens: number + // The duration for unstaking. + unstakingDuration: Duration + // Symbol for the token that is being staked. + tokenSymbol: string + // Decimals for the token that is being staked. + tokenDecimals: number + // Is there an error? + error: string | undefined + // Are we ready to stake? Ex: is wallet connected? + loading: boolean + // Triggered when the stake / unstake / claim button is pressed. + onAction: (mode: StakingMode, amount: number) => void +} + +export const StakingModal: FC = ({ + defaultMode, + amount, + setAmount, + onClose, + claimableTokens, + stakableTokens, + unstakableTokens, + unstakingDuration, + tokenSymbol, + tokenDecimals, + loading, + error, + onAction, +}) => { + const [mode, setMode] = useState(defaultMode) + const canClaim = !!claimableTokens + + const maxTx = mode === StakingMode.Stake ? stakableTokens : unstakableTokens + + const invalidAmount = (): string | undefined => { + if (mode === StakingMode.Claim) { + return claimableTokens > 0 ? undefined : "Can't claim zero tokens" + } + if (amount <= 0) { + return `Can't ${stakingModeString(mode)} zero tokens.` + } + if (amount > maxTx) { + return "Can't ${stakingModeString(mode)} more tokens than you own." + } + return undefined + } + error = error || invalidAmount() + + return ( + +
    + + +
    +

    Manage staking

    +
    + +
    + setMode(StakingMode.Stake)} + > + Stake + + setMode(StakingMode.Unstake)} + > + Unstake + + {canClaim && ( + setMode(StakingMode.Claim)} + > + Claim + + )} +
    + {mode === StakingMode.Stake && ( + setAmount(amount)} + max={stakableTokens} + tokenDecimals={tokenDecimals} + /> + )} + {mode === StakingMode.Unstake && ( + setAmount(amount)} + max={unstakableTokens} + unstakingDuration={unstakingDuration} + tokenDecimals={tokenDecimals} + /> + )} + {mode === StakingMode.Claim && ( + + )} +
    + + onAction( + mode, + mode === StakingMode.Claim ? claimableTokens : amount + ) + } + /> +
    +
    +
    + ) +} + +interface AmountSelectionProps { + amount: number + max: number + setAmount: (newAmount: number) => void + tokenDecimals: number +} + +const AmountSelectionBody: FC = ({ + amount, + setAmount, + max, + tokenDecimals, +}) => ( +
    +

    Choose your token amount

    + + {amount > max && ( + + Can{"'"}t stake more tokens than you own. + + )} + Max available {max} +
    + +
    +
    +) + +const UnstakingModeBody: FC< + AmountSelectionProps & { unstakingDuration: Duration } +> = ({ amount, setAmount, max, unstakingDuration, tokenDecimals }) => ( + <> + + {durationIsNonZero(unstakingDuration) && ( + <> +
    +
    +

    + Unstaking period: {humanReadableDuration(unstakingDuration)} +

    +

    + There will be {humanReadableDuration(unstakingDuration)} between the + time you decide to unstake your tokens and the time you can redeem + them. +

    +
    + + )} + +) + +interface ClaimBodyProps { + amount: number + tokenDecimals: number + tokenSymbol: string +} + +const ClaimBody: FC = ({ + amount, + tokenSymbol, + tokenDecimals, +}) => ( +
    +

    + {convertMicroDenomToDenomWithDecimals(amount, tokenDecimals)} $ + {tokenSymbol} avaliable +

    +

    + Claim them to receive your unstaked tokens. +

    +
    +) diff --git a/packages/ui/components/StakingModal/index.tsx b/packages/ui/components/StakingModal/index.tsx new file mode 100644 index 000000000..92f4237c9 --- /dev/null +++ b/packages/ui/components/StakingModal/index.tsx @@ -0,0 +1 @@ +export { StakingMode, StakingModal } from './StakingModal' diff --git a/packages/ui/components/StatusIcons/StatusIcons.tsx b/packages/ui/components/StatusIcons/StatusIcons.tsx new file mode 100644 index 000000000..4e77cb0a2 --- /dev/null +++ b/packages/ui/components/StatusIcons/StatusIcons.tsx @@ -0,0 +1,8 @@ +import { Executed, Open, Passed, Rejected } from '@dao-dao/icons' + +export const StatusIcons: { [key: string]: JSX.Element } = { + open: , + executed: , + passed: , + rejected: , +} diff --git a/apps/dapp/components/StatusIcons/index.ts b/packages/ui/components/StatusIcons/index.ts similarity index 100% rename from apps/dapp/components/StatusIcons/index.ts rename to packages/ui/components/StatusIcons/index.ts diff --git a/packages/ui/components/Tooltip.tsx b/packages/ui/components/Tooltip.tsx new file mode 100644 index 000000000..897dcf631 --- /dev/null +++ b/packages/ui/components/Tooltip.tsx @@ -0,0 +1,15 @@ +import { FC, ReactNode } from 'react' + +import ReachTooltip from '@reach/tooltip' + +export interface TooltipProps { + label: ReactNode | undefined + children: ReactNode +} + +export const Tooltip: FC = ({ label, children }) => + !!label ? ( + {children} + ) : ( + <>{children} + ) diff --git a/apps/dapp/components/TooltipsDisplay/TooltipIcon.tsx b/packages/ui/components/TooltipIcon.tsx similarity index 100% rename from apps/dapp/components/TooltipsDisplay/TooltipIcon.tsx rename to packages/ui/components/TooltipIcon.tsx diff --git a/apps/dapp/components/Vote.tsx b/packages/ui/components/Vote.tsx similarity index 92% rename from apps/dapp/components/Vote.tsx rename to packages/ui/components/Vote.tsx index bd933d706..3cc542d91 100644 --- a/apps/dapp/components/Vote.tsx +++ b/packages/ui/components/Vote.tsx @@ -1,10 +1,9 @@ import { FC, useState } from 'react' +import { Button } from '@dao-dao/ui' import { CheckIcon, XIcon } from '@heroicons/react/outline' -import { Button } from 'ui' -import SvgAbstain from './icons/Abstain' -import SvgAirplane from './icons/Airplane' +import { Abstain, Airplane } from '@dao-dao/icons' export enum VoteChoice { Yes, @@ -64,7 +63,7 @@ export const Vote: FC = ({ onVote, voterWeight, loading }) => { } variant="secondary" > - + Abstain
    ) diff --git a/packages/ui/components/WalletConnect.tsx b/packages/ui/components/WalletConnect.tsx new file mode 100644 index 000000000..dcab6cbfd --- /dev/null +++ b/packages/ui/components/WalletConnect.tsx @@ -0,0 +1,89 @@ +import { FC, useState } from 'react' + +import { Wallet, Copy } from '@dao-dao/icons' +import { CheckCircleIcon, LogoutIcon } from '@heroicons/react/outline' +import { Button } from './Button' +import { Tooltip } from './Tooltip' + +export interface WalletConnectProps { + walletAddress: string + walletName: string | undefined + walletBalance: number + walletBalanceDenom: string + handleConnect: () => void +} + +export const WalletConnect: FC = ({ + walletAddress, + walletName, + walletBalance, + walletBalanceDenom, + handleConnect, +}) => + walletAddress ? ( +
    +
    + +
    + {walletName} +
    + + {walletBalance} {walletBalanceDenom} + +
    +
    +
    + + +
    +
    + ) : ( +
    + +
    + ) + +interface CopyButtonProps { + text: string +} + +const CopyButton: FC = ({ text }) => { + const [copied, setCopied] = useState(false) + return ( + + + + ) +} + +interface DisconnectButtonProps { + onClick: () => void +} + +const DisconnectButton: FC = ({ onClick }) => ( + + + +) diff --git a/apps/dapp/components/input/AddressInput.tsx b/packages/ui/components/input/AddressInput.tsx similarity index 91% rename from apps/dapp/components/input/AddressInput.tsx rename to packages/ui/components/input/AddressInput.tsx index 6360054ca..58807b622 100644 --- a/apps/dapp/components/input/AddressInput.tsx +++ b/packages/ui/components/input/AddressInput.tsx @@ -8,7 +8,7 @@ import { Validate, } from 'react-hook-form' -import SvgWallet from '@components/icons/Wallet' +import { Wallet } from '@dao-dao/icons' export function AddressInput>({ label, @@ -34,7 +34,7 @@ export function AddressInput>({ className={`flex items-center gap-1 bg-transparent rounded-lg px-3 py-2 transition focus-within:ring-1 focus-within:outline-none ring-brand ring-offset-0 border-default border border-default text-sm font-mono ${error ? ' ring-error ring-1' : ''}`} > - +
    diff --git a/apps/dapp/components/input/InputErrorMessage.tsx b/packages/ui/components/input/InputErrorMessage.tsx similarity index 100% rename from apps/dapp/components/input/InputErrorMessage.tsx rename to packages/ui/components/input/InputErrorMessage.tsx diff --git a/apps/dapp/components/input/InputLabel.tsx b/packages/ui/components/input/InputLabel.tsx similarity index 91% rename from apps/dapp/components/input/InputLabel.tsx rename to packages/ui/components/input/InputLabel.tsx index c3b1094c7..2aa3d3675 100644 --- a/apps/dapp/components/input/InputLabel.tsx +++ b/packages/ui/components/input/InputLabel.tsx @@ -2,7 +2,7 @@ import { ComponentProps, ReactNode } from 'react' import clsx from 'clsx' -import { TooltipIcon } from '@components/TooltipsDisplay/TooltipIcon' +import { TooltipIcon } from '../TooltipIcon' export type InputLabelProps = Omit, 'children'> & { mono?: boolean diff --git a/apps/dapp/components/input/NumberInput.tsx b/packages/ui/components/input/NumberInput.tsx similarity index 100% rename from apps/dapp/components/input/NumberInput.tsx rename to packages/ui/components/input/NumberInput.tsx diff --git a/apps/dapp/components/input/SelectInput.tsx b/packages/ui/components/input/SelectInput.tsx similarity index 100% rename from apps/dapp/components/input/SelectInput.tsx rename to packages/ui/components/input/SelectInput.tsx diff --git a/apps/dapp/components/input/TextAreaInput.tsx b/packages/ui/components/input/TextAreaInput.tsx similarity index 97% rename from apps/dapp/components/input/TextAreaInput.tsx rename to packages/ui/components/input/TextAreaInput.tsx index 635b6b447..59b73b0eb 100644 --- a/apps/dapp/components/input/TextAreaInput.tsx +++ b/packages/ui/components/input/TextAreaInput.tsx @@ -14,7 +14,6 @@ export function TextareaInput< register, error, validation, - border = true, }: { label: FieldName register: UseFormRegister diff --git a/apps/dapp/components/input/TextInput.tsx b/packages/ui/components/input/TextInput.tsx similarity index 100% rename from apps/dapp/components/input/TextInput.tsx rename to packages/ui/components/input/TextInput.tsx diff --git a/apps/dapp/components/input/ToggleInput.tsx b/packages/ui/components/input/ToggleInput.tsx similarity index 100% rename from apps/dapp/components/input/ToggleInput.tsx rename to packages/ui/components/input/ToggleInput.tsx diff --git a/apps/dapp/components/input/TokenAmountInput.tsx b/packages/ui/components/input/TokenAmountInput.tsx similarity index 100% rename from apps/dapp/components/input/TokenAmountInput.tsx rename to packages/ui/components/input/TokenAmountInput.tsx diff --git a/packages/ui/components/input/index.tsx b/packages/ui/components/input/index.tsx new file mode 100644 index 000000000..965b3dc90 --- /dev/null +++ b/packages/ui/components/input/index.tsx @@ -0,0 +1,11 @@ +export * from './AddressInput' +export * from './CodeMirrorInput' +export * from './ImageSelector' +export * from './InputErrorMessage' +export * from './InputLabel' +export * from './NumberInput' +export * from './SelectInput' +export * from './TextAreaInput' +export * from './TextInput' +export * from './ToggleInput' +export * from './TokenAmountInput' diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index bf3c22fc2..014f579a8 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -1,3 +1,22 @@ -import * as React from 'react' -export * from './Button' +export * from './components/Button/index' export * from './theme' +export * from './components/StakingModal' +export * from './components/Modal' +export * from './components/Tooltip' +export * from './components/ProposalDetails' +export * from './components/MarkdownPreview' +export * from './components/CosmosMessageDisplay' +export * from './components/input/index' +export * from './components/Execute' +export * from './components/Vote' +export * from './components/WalletConnect' +export * from './components/Progress' +export * from './components/ProposalStatus' +export * from './components/CopyToClipboard' +export * from './components/TooltipIcon' +export * from './components/Logo' +export * from './components/ContractView' +export * from './components/GradientWrapper' +export * from './components/LoadingScreen' +export * from './components/Breadcrumbs' +export * from './components/Claims' diff --git a/packages/ui/package.json b/packages/ui/package.json index d1028c79e..6d3063772 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,5 +1,5 @@ { - "name": "ui", + "name": "@dao-dao/ui", "version": "0.0.0", "main": "./index.tsx", "types": "./index.tsx", @@ -13,6 +13,16 @@ "storybook": "start-storybook --ci -p 6006 --no-manager-cache", "build-storybook": "build-storybook" }, + "dependencies": { + "@dao-dao/utils": "*", + "@dao-dao/icons": "*", + "@heroicons/react": "^1.0.5", + "@dao-dao/types": "^0.0.8", + "react-markdown": "^8.0.0", + "react-hook-form": "^7.20.4", + "clsx": "^1.1.1", + "codemirror": "^5.65.0" + }, "devDependencies": { "@babel/core": "^7.17.5", "@dao-dao/config": "*", diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index cd6c94d6e..b702b31a9 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,5 +1,6 @@ { - "extends": "tsconfig/react-library.json", + "extends": "tsconfig/nextjs.json", "include": ["."], - "exclude": ["dist", "build", "node_modules"] + "exclude": ["dist", "build", "node_modules"], + "moduleResolution": "node" } diff --git a/packages/utils/claims.ts b/packages/utils/claims.ts new file mode 100644 index 000000000..d77c36a9d --- /dev/null +++ b/packages/utils/claims.ts @@ -0,0 +1,13 @@ +import { Claim } from '@dao-dao/types/contracts/stake-cw20' + +export function claimAvaliable(claim: Claim, blockHeight: number) { + if ('at_height' in claim.release_at) { + return blockHeight >= claim.release_at.at_height + } else if ('at_time' in claim.release_at) { + const currentTimeNs = new Date().getTime() * 1000000 + return currentTimeNs >= Number(claim.release_at.at_time) + } + + // Unreachable. + return true +} diff --git a/apps/dapp/util/constants.ts b/packages/utils/constants.ts similarity index 100% rename from apps/dapp/util/constants.ts rename to packages/utils/constants.ts diff --git a/apps/dapp/util/conversion.ts b/packages/utils/conversion.ts similarity index 58% rename from apps/dapp/util/conversion.ts rename to packages/utils/conversion.ts index 512084f3e..2b183723b 100644 --- a/apps/dapp/util/conversion.ts +++ b/packages/utils/conversion.ts @@ -1,13 +1,11 @@ import { - Duration, Expiration, + Status, Threshold as DaoThreshold, ThresholdResponse, } from '@dao-dao/types/contracts/cw3-dao' import { Threshold as SigThreshold } from '@dao-dao/types/contracts/cw3-multisig' - -import { NATIVE_DECIMALS, NATIVE_DENOM } from './constants' -import ibcAssets from './ibc_assets.json' +import { secondsToWdhms } from './time' export function convertMicroDenomToDenomWithDecimals( amount: number | string, @@ -27,23 +25,11 @@ export function convertDenomToMicroDenomWithDecimals( if (typeof amount === 'string') { amount = Number(amount) } - amount = amount * Math.pow(10, decimals) + // Need to round. Example: `8.029409 * Math.pow(10, 6)`. + amount = Math.round(amount * Math.pow(10, decimals)) return isNaN(amount) ? '0' : String(amount) } -export function convertFromMicroDenom(denom: string) { - return denom?.substring(1).toUpperCase() -} - -export function convertToFixedDecimals(amount: number | string): string { - if (typeof amount === 'string') { - amount = Number(amount) - } - if (amount > 0.01) { - return amount.toFixed(2) - } else return String(amount) -} - export function convertDenomToHumanReadableDenom(denom: string): string { if (denom.startsWith('u')) { return denom.substring(1) @@ -58,14 +44,17 @@ export function convertDenomToContractReadableDenom(denom: string): string { return 'u' + denom } -export const zeroVotingCoin = { - amount: '0', - denom: 'ucredits', +export function convertFromMicroDenom(denom: string) { + return denom?.substring(1).toUpperCase() } -export const zeroStakingCoin = { - amount: '0', - denom: process.env.NEXT_PUBLIC_STAKING_DENOM || 'ujuno', +export function convertToFixedDecimals(amount: number | string): string { + if (typeof amount === 'string') { + amount = Number(amount) + } + if (amount > 0.01) { + return amount.toFixed(2) + } else return String(amount) } export const getDaoThresholdAndQuorum = ( @@ -134,73 +123,6 @@ export const thresholdString = ( } } -export function humanReadableDuration(d: Duration) { - if ('height' in d) { - return `${d.height} blocks` - } - if (d.time == 0) { - return '0 seconds' - } - return `${secondsToWdhms(d.time.toString())}` -} - -const secPerDay = 24 * 60 * 60 -export function secondsToWdhms(seconds: string | number, numUnits = 5): string { - const secondsInt = Number(seconds) - const w = Math.floor(secondsInt / (secPerDay * 7)) - const d = Math.floor((secondsInt % (secPerDay * 7)) / secPerDay) - const h = Math.floor((secondsInt % secPerDay) / 3600) - const m = Math.floor((secondsInt % 3600) / 60) - const s = Math.floor(secondsInt % 60) - - const wDisplay = w ? w + (w === 1 ? ' wk' : ' wks') : null - const dDisplay = d ? d + (d === 1 ? ' day' : ' days') : null - const hDisplay = h ? h + (h === 1 ? ' hr' : ' hrs') : null - const mDisplay = m ? m + (m === 1 ? ' min' : ' mins') : null - const sDisplay = s ? s + (s === 1 ? ' sec' : ' secs') : null - - return ( - [wDisplay, dDisplay, hDisplay, mDisplay, sDisplay] - // Ignore empty values. - .filter(Boolean) - // Only keep certain precision of units. - .slice(0, numUnits) - // Separate with commas. - .join(', ') - ) -} - -export function nativeTokenLabel(denom: string): string { - // Search IBC asset strings (junoDenom) if denom is in IBC format. - // Otherwise just check microdenoms. - const asset = denom.startsWith('ibc') - ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom) - : ibcAssets.tokens.find(({ denom: d }) => d === denom) - // If no asset, assume it's already a microdenom. - return asset?.symbol || convertFromMicroDenom(denom) -} - -export function nativeTokenLogoURI(denom: string): string | undefined { - if (denom === 'ujuno' || denom == 'ujunox') { - return '/juno-symbol.png' - } - - const asset = denom.startsWith('ibc') - ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom) - : ibcAssets.tokens.find(({ denom: d }) => d === denom) - return asset?.logoURI -} - -export function nativeTokenDecimals(denom: string): number | undefined { - if (denom === NATIVE_DENOM) { - return NATIVE_DECIMALS - } - const asset = denom.startsWith('ibc') - ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom) - : ibcAssets.tokens.find(({ denom: d }) => d === denom) - return asset?.decimals -} - export const expirationAtTimeToSecondsFromNow = (exp: Expiration) => { if (!('at_time' in exp)) { return undefined @@ -212,3 +134,33 @@ export const expirationAtTimeToSecondsFromNow = (exp: Expiration) => { return endSeconds - nowSeconds } + +export const zeroPad = (num: number, target: number) => { + const s = num.toString() + if (s.length > target) { + return s + } + return '0'.repeat(target - s.length) + s +} + +export const getProposalEnd = (exp: Expiration, status: Status) => { + if (status != 'open' && status != 'pending') { + return 'Completed' + } + if (exp && 'at_time' in exp) { + const secondsFromNow = expirationAtTimeToSecondsFromNow(exp) + // Type check, but should never happen. + if (secondsFromNow === undefined) { + return '' + } + + if (secondsFromNow <= 0) { + return 'Completed' + } else { + return secondsToWdhms(secondsFromNow) + } + } + // Not much we can say about proposals that expire at a block + // height / never. + return '' +} diff --git a/packages/utils/duration.ts b/packages/utils/duration.ts new file mode 100644 index 000000000..187d795ac --- /dev/null +++ b/packages/utils/duration.ts @@ -0,0 +1,19 @@ +import { Duration } from '@dao-dao/types/contracts/cw3-dao' +import { secondsToWdhms } from './time' + +export const durationIsNonZero = (d: Duration) => { + if ('height' in d) { + return d.height !== 0 + } + return d.time !== 0 +} + +export const humanReadableDuration = (d: Duration) => { + if ('height' in d) { + return `${d.height} blocks` + } + if (d.time == 0) { + return '0 seconds' + } + return `${secondsToWdhms(d.time.toString())}` +} diff --git a/packages/utils/ibc.ts b/packages/utils/ibc.ts new file mode 100644 index 000000000..d7d52f729 --- /dev/null +++ b/packages/utils/ibc.ts @@ -0,0 +1,34 @@ +import ibcAssets from './ibc_assets.json' +import { convertFromMicroDenom } from './conversion' +import { NATIVE_DENOM, NATIVE_DECIMALS } from './constants' + +export function nativeTokenLabel(denom: string): string { + // Search IBC asset strings (junoDenom) if denom is in IBC format. + // Otherwise just check microdenoms. + const asset = denom.startsWith('ibc') + ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom) + : ibcAssets.tokens.find(({ denom: d }) => d === denom) + // If no asset, assume it's already a microdenom. + return asset?.symbol || convertFromMicroDenom(denom) +} + +export function nativeTokenLogoURI(denom: string): string | undefined { + if (denom === 'ujuno' || denom == 'ujunox') { + return '/juno-symbol.png' + } + + const asset = denom.startsWith('ibc') + ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom) + : ibcAssets.tokens.find(({ denom: d }) => d === denom) + return asset?.logoURI +} + +export function nativeTokenDecimals(denom: string): number | undefined { + if (denom === NATIVE_DENOM) { + return NATIVE_DECIMALS + } + const asset = denom.startsWith('ibc') + ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom) + : ibcAssets.tokens.find(({ denom: d }) => d === denom) + return asset?.decimals +} diff --git a/apps/dapp/util/ibc_assets.json b/packages/utils/ibc_assets.json similarity index 100% rename from apps/dapp/util/ibc_assets.json rename to packages/utils/ibc_assets.json diff --git a/packages/utils/index.tsx b/packages/utils/index.tsx index cb877561a..31a1eda0e 100644 --- a/packages/utils/index.tsx +++ b/packages/utils/index.tsx @@ -1 +1,8 @@ export * from './isValidAddress' +export * from './duration' +export * from './time' +export * from './conversion' +export * from './ibc' +export * from './constants' +export * from './messages' +export * from './claims' diff --git a/packages/utils/messages.ts b/packages/utils/messages.ts new file mode 100644 index 000000000..cfbec4f60 --- /dev/null +++ b/packages/utils/messages.ts @@ -0,0 +1,102 @@ +import { + CosmosMsgFor_Empty, + WasmMsg, + ProposalResponse, +} from '@dao-dao/types/contracts/cw3-dao' + +export function parseEncodedMessage(base64String?: string) { + if (base64String) { + const jsonMessage = decodeURIComponent(escape(atob(base64String))) + if (jsonMessage) { + return JSON.parse(jsonMessage) + } + } + return undefined +} + +export type WasmMsgType = + | 'execute' + | 'instantiate' + | 'migrate' + | 'update_admin' + | 'clear_admin' + +const WASM_TYPES: WasmMsgType[] = [ + 'execute', + 'instantiate', + 'migrate', + 'update_admin', + 'clear_admin', +] + +const BINARY_WASM_TYPES: { [key: string]: boolean } = { + execute: true, + instantiate: true, + migrate: true, +} + +export function isWasmMsg(msg?: CosmosMsgFor_Empty): msg is { wasm: WasmMsg } { + if (msg) { + return (msg as any).wasm !== undefined + } + return false +} + +function getWasmMsgType(wasm: WasmMsg): WasmMsgType | undefined { + for (const wasmType of WASM_TYPES) { + if (!!(wasm as any)[wasmType]) { + return wasmType + } + } + return undefined +} + +function isBinaryType(msgType?: WasmMsgType): boolean { + if (msgType) { + return !!BINARY_WASM_TYPES[msgType] + } + return false +} + +export function decodeMessages( + msgs: ProposalResponse['msgs'] +): { [key: string]: any }[] { + const decodedMessageArray: any[] = [] + const proposalMsgs = Object.values(msgs) + for (const msgObj of proposalMsgs) { + if (isWasmMsg(msgObj)) { + const msgType = getWasmMsgType(msgObj.wasm) + if (msgType && isBinaryType(msgType)) { + const base64Msg = (msgObj.wasm as any)[msgType] + if (base64Msg) { + const msg = parseEncodedMessage(base64Msg.msg) + if (msg) { + decodedMessageArray.push({ + ...msgObj, + wasm: { + ...msgObj.wasm, + [msgType]: { + ...base64Msg, + msg, + }, + }, + }) + } + } + } + } else { + decodedMessageArray.push(msgObj) + } + } + + const decodedMessages = decodedMessageArray.length + ? decodedMessageArray + : proposalMsgs + + return decodedMessages +} + +export function decodedMessagesString(msgs: ProposalResponse['msgs']): string { + const decodedMessageArray = decodeMessages(msgs) + return JSON.stringify(decodedMessageArray, undefined, 2) +} diff --git a/packages/utils/time.ts b/packages/utils/time.ts new file mode 100644 index 000000000..74f4184a1 --- /dev/null +++ b/packages/utils/time.ts @@ -0,0 +1,28 @@ +const secPerDay = 24 * 60 * 60 +export const secondsToWdhms = ( + seconds: string | number, + numUnits = 5 +): string => { + const secondsInt = Number(seconds) + const w = Math.floor(secondsInt / (secPerDay * 7)) + const d = Math.floor((secondsInt % (secPerDay * 7)) / secPerDay) + const h = Math.floor((secondsInt % secPerDay) / 3600) + const m = Math.floor((secondsInt % 3600) / 60) + const s = Math.floor(secondsInt % 60) + + const wDisplay = w ? w + (w === 1 ? ' wk' : ' wks') : null + const dDisplay = d ? d + (d === 1 ? ' day' : ' days') : null + const hDisplay = h ? h + (h === 1 ? ' hr' : ' hrs') : null + const mDisplay = m ? m + (m === 1 ? ' min' : ' mins') : null + const sDisplay = s ? s + (s === 1 ? ' sec' : ' secs') : null + + return ( + [wDisplay, dDisplay, hDisplay, mDisplay, sDisplay] + // Ignore empty values. + .filter(Boolean) + // Only keep certain precision of units. + .slice(0, numUnits) + // Separate with commas. + .join(', ') + ) +}