diff --git a/package.json b/package.json index 478fb11c87..a820805642 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@babel/parser": "~7.21.0", "@babel/traverse": "~7.21.0", "@babel/types": "~7.21.0", - "@joystream/types": "4.5.0", + "@joystream/types": "4.6.0", "@polkadot/api": "10.7.1", "@polkadot/api-contract": "10.7.1", "@polkadot/api-derive": "10.7.1", diff --git a/packages/ui/src/accounts/components/AccountInfo.stories.tsx b/packages/ui/src/accounts/components/AccountInfo.stories.tsx new file mode 100644 index 0000000000..a5c92da0bd --- /dev/null +++ b/packages/ui/src/accounts/components/AccountInfo.stories.tsx @@ -0,0 +1,19 @@ +import { Meta } from '@storybook/react' + +import { AccountInfo } from './AccountInfo' + +export default { + title: 'Accounts/AccountInfo', + component: AccountInfo, + args: { + account: { + name: 'Alice', + address: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT', + }, + lockType: 'Invitation', + }, +} as Meta + +export const Default = { + name: 'AccountInfo', +} diff --git a/packages/ui/src/accounts/components/AnonymousAccount.stories.tsx b/packages/ui/src/accounts/components/AnonymousAccount.stories.tsx new file mode 100644 index 0000000000..4d575020d6 --- /dev/null +++ b/packages/ui/src/accounts/components/AnonymousAccount.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from '@storybook/react' +import BN from 'bn.js' +import React from 'react' + +import { Row } from '@/common/components/storybookParts/previewStyles' +import { joy } from '@/mocks/helpers' + +import { AnonymousAccount } from './AnonymousAccount' + +type Args = { + address: string + amount?: number +} + +export default { + title: 'Accounts/AnonymousAccount', + component: AnonymousAccount, + args: { + address: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT', + amount: 10, + }, +} as Meta + +export const Default: StoryObj = { + name: 'AnonymousAccount', + render: ({ address, amount }) => ( + + + + + ), +} diff --git a/packages/ui/src/accounts/components/AnonymousAccount.tsx b/packages/ui/src/accounts/components/AnonymousAccount.tsx new file mode 100644 index 0000000000..b6a082213c --- /dev/null +++ b/packages/ui/src/accounts/components/AnonymousAccount.tsx @@ -0,0 +1,59 @@ +import { Identicon } from '@polkadot/react-identicon' +import BN from 'bn.js' +import React from 'react' +import styled from 'styled-components' + +import { CopyComponent } from '@/common/components/CopyComponent' +import { AccountRow, InfoTitle, InfoValue } from '@/common/components/Modal' +import { TokenValue } from '@/common/components/typography' +import { Colors } from '@/common/constants' +import { shortenAddress } from '@/common/model/formatters' + +type Props = { + address: string + amount?: BN + addressLength?: number +} + +export const AnonymousAccount = ({ address, amount, addressLength }: Props) => { + return ( + + + + + {amount && ( + + Total Balance: + + + + + )} + + + ) +} + +const StyledAccountRow = styled(AccountRow)` + display: grid; + grid-template-columns: 40px 1fr; + gap: 12px; + align-items: center; +` + +const Info = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +` + +const AccountCopyAddress = styled(CopyComponent)` + font-size: 16px; + color: ${Colors.Black[900]}; +` + +const BalanceInfo = styled.div` + display: flex; + align-items: center; + gap: 4px; +` diff --git a/packages/ui/src/accounts/components/SelectAccount/SelectAccount.tsx b/packages/ui/src/accounts/components/SelectAccount/SelectAccount.tsx index 349848ce5c..bb3ef044eb 100644 --- a/packages/ui/src/accounts/components/SelectAccount/SelectAccount.tsx +++ b/packages/ui/src/accounts/components/SelectAccount/SelectAccount.tsx @@ -9,7 +9,6 @@ import { isValidAddress } from '@/accounts/model/isValidAddress' import { RecoveryConditions } from '@/accounts/model/lockTypes' import { Account, AccountOption, LockType } from '@/accounts/types' import { Select, SelectedOption, SelectProps } from '@/common/components/selects' -import { useKeyring } from '@/common/hooks/useKeyring' import { Address } from '@/common/types' import { filterByText } from './helpers' @@ -41,17 +40,22 @@ interface BaseSelectAccountProps extends SelectAccountProps { const BaseSelectAccount = React.memo( ({ id, onChange, accounts, filter, selected, disabled, onBlur, isForStaking, variant }: BaseSelectAccountProps) => { - const options = accounts.filter(filter || (() => true)) + const options = !filter + ? accounts + : accounts.filter( + (account) => + account.address === selected?.address || // Always keep the selected account (otherwise the select behavior is strange) + filter(account) + ) const [search, setSearch] = useState('') const filteredOptions = useMemo(() => filterByText(options, search), [search, options]) - const keyring = useKeyring() const notSelected = !selected || selected.address !== search useEffect(() => { - if (filteredOptions.length === 0 && isValidAddress(search, keyring) && notSelected) { + if (filteredOptions.length === 0 && isValidAddress(search) && notSelected) { onChange?.(accountOrNamed(accounts, search, 'Unsaved account')) } }, [filteredOptions, search, notSelected]) diff --git a/packages/ui/src/accounts/modals/ClaimVestingModal/components/SelectVestingAccount.tsx b/packages/ui/src/accounts/modals/ClaimVestingModal/components/SelectVestingAccount.tsx index 7cd8e76ca3..6a852440fa 100644 --- a/packages/ui/src/accounts/modals/ClaimVestingModal/components/SelectVestingAccount.tsx +++ b/packages/ui/src/accounts/modals/ClaimVestingModal/components/SelectVestingAccount.tsx @@ -14,7 +14,6 @@ import { BalanceInfoInRow, InfoValue } from '@/common/components/Modal' import { ColumnGapBlock } from '@/common/components/page/PageContent' import { Option, OptionsListComponent, Select, SelectedOption } from '@/common/components/selects' import { TextMedium, TokenValue } from '@/common/components/typography' -import { useKeyring } from '@/common/hooks/useKeyring' interface SelectVestingAccountProps { selected?: Account @@ -29,12 +28,11 @@ export const SelectVestingAccount = ({ selected, onChange, id, disabled }: Selec const [search, setSearch] = useState('') const filteredOptions = useMemo(() => filterByText(options, search), [search, options]) - const keyring = useKeyring() const notSelected = !selected || selected?.address !== search useEffect(() => { - if (filteredOptions.length === 0 && isValidAddress(search, keyring) && notSelected) { + if (filteredOptions.length === 0 && isValidAddress(search) && notSelected) { onChange?.(accountOrNamed(options, search, 'Unsaved account')) } }, [filteredOptions, search, notSelected]) diff --git a/packages/ui/src/accounts/model/isValidAddress.ts b/packages/ui/src/accounts/model/isValidAddress.ts index ca48bb451a..be50a8c3eb 100644 --- a/packages/ui/src/accounts/model/isValidAddress.ts +++ b/packages/ui/src/accounts/model/isValidAddress.ts @@ -1,11 +1,10 @@ -import { KeyringInstance } from '@polkadot/keyring/types' -import { KeyringStruct } from '@polkadot/ui-keyring/types' +import { encodeAddress, decodeAddress } from '@polkadot/util-crypto' import { Address } from '../../common/types' -export function isValidAddress(address: Address, keyring: KeyringInstance | KeyringStruct) { +export function isValidAddress(address: Address) { try { - keyring.encodeAddress(keyring.decodeAddress(address)) + encodeAddress(decodeAddress(address)) } catch (e) { return false } diff --git a/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccount.tsx b/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccount.tsx deleted file mode 100644 index e727630377..0000000000 --- a/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccount.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Identicon } from '@polkadot/react-identicon' -import BN from 'bn.js' -import React from 'react' -import styled from 'styled-components' - -import { AccountCopyAddress, AccountInfoWrap, AccountPhoto, PhotoWrapper } from '@/accounts/components/AccountInfo' -import { AccountRow, BalanceInfoInRow, InfoTitle, InfoValue } from '@/common/components/Modal' -import { TokenValue } from '@/common/components/typography' -import { Colors } from '@/common/constants' -import { shortenAddress } from '@/common/model/formatters' - -interface Prop { - account: { address: string; balance: BN | undefined } -} - -export const BlacklistedAccount = ({ account }: Prop) => { - return ( - - - - - - - - - - Total Balance: - - - - - - - ) -} -const BlacklistedAccountInfoWrap = styled(AccountInfoWrap)` - grid-template-rows: 24px 18px; - grid-row-gap: 4px; - grid-template-areas: - 'accountphoto accountaddress' - 'accountphoto accountbalance'; -` -const BlacklistedAccountBalance = styled(BalanceInfoInRow)` - grid-area: accountbalance; - display: flex; - justify-self: start; -` -const BlacklistedAccountCopyAddress = styled(AccountCopyAddress)` - font-size: 16px; - color: ${Colors.Black[900]}; -` -const BlacklistedAccountRow = styled(AccountRow)` - max-width: 378px; -` diff --git a/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccounts.tsx b/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccounts.tsx index 91493a60b7..62f157fb0d 100644 --- a/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccounts.tsx +++ b/packages/ui/src/app/pages/Election/BlacklistedAccounts/BlacklistedAccounts.tsx @@ -2,6 +2,7 @@ import { BN_ZERO } from '@polkadot/util' import React, { useMemo, useState } from 'react' import styled from 'styled-components' +import { AnonymousAccount } from '@/accounts/components/AnonymousAccount' import { useBalances } from '@/accounts/hooks/useBalance' import { useVotingOptOutAccounts } from '@/accounts/hooks/useVotingOptOutAccounts' import { PageHeaderRow, PageHeaderWrapper, PageLayout } from '@/app/components/PageLayout' @@ -16,8 +17,6 @@ import { Warning } from '@/common/components/Warning' import { ElectionTabs } from '../components/ElectionTabs' -import { BlacklistedAccount } from './BlacklistedAccount' - export const BlacklistedAccounts = () => { const ACCOUNTS_PER_PAGE = 18 const [page, setPage] = useState(1) @@ -75,7 +74,7 @@ export const BlacklistedAccounts = () => {
Accounts ({votingOptOutAccounts?.length})
{paginatedAccounts.map((account, i) => ( - + ))} ): MocksParameters => { const alice = member('alice', { isCouncilMember: args.isCouncilMember }) + const aliceAddress = alice.controllerAccount + const bobAddress = member('bob').controllerAccount + const charlieAddress = member('charlie').controllerAccount + const forumWG = { id: 'forumWorkingGroup', name: 'forumWorkingGroup', @@ -166,6 +171,7 @@ export default { members: { stakingAccountIdMemberStatus: parameters.stakingAccountIdMemberStatus, }, + projectToken: { palletFrozen: args.palletFrozen, @@ -179,6 +185,14 @@ export default { minSaleDuration: 300, salePlatformFee: 30_000, }, + + argoBridge: { + operatorAccount: aliceAddress, + pauserAccounts: [bobAddress, charlieAddress], + bridgingFee: joy(0.1), + thawnDuration: 200, + remoteChains: [37, 42], + }, }, tx: { proposalsEngine: { @@ -1682,3 +1696,54 @@ export const SpecificUpdateTokenPalletTokenConstraints: Story = { } ), } + +export const SpecificParametersUpdateArgoBridgeConstraints: Story = { + play: specificParametersTest('Update Argo Bridge Constraints', async ({ args, createProposal, modal, step }) => { + const aliceAddress = alice.controllerAccount + const bobAddress = member('bob').controllerAccount + const daveAddress = member('dave').controllerAccount + + await createProposal(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Set Bob as operator + const operatorAccountSelector = await modal.findByTestId('operatorAccount') + await userEvent.click(await findBySelector(operatorAccountSelector, '.ui-toggle')) + await userEvent.type(await modal.findByTestId('operatorAccount-input'), bobAddress) + + // Set Alice and Dave as pausers, and remove Charlie + const pauserAccountSection = await modal.findByTestId('pauserAccounts') + await selectFromDropdown(modal, await modal.findByTestId('pauserAccounts-0'), 'alice') + const addPauserAccountButton = await within(pauserAccountSection).findByRole('button', { name: 'Add account' }) + await userEvent.click(addPauserAccountButton) + await userEvent.type(await modal.findByTestId('pauserAccounts-2-input'), daveAddress) + await userEvent.click(await within(pauserAccountSection).findByTestId('pauserAccounts-1-remove')) + + // Set bridging fee to 0.2 JOY + const bridgingFeeAmount = await modal.findByLabelText('Bridging fee') + await userEvent.clear(bridgingFeeAmount) + await userEvent.type(bridgingFeeAmount, '0.2') + + // Set thawn duration to 100 blocks + const thawnDuration = await modal.findByLabelText('Thawn duration') + await userEvent.clear(thawnDuration) + await userEvent.type(thawnDuration, '100') + + // Add remote chain 123 (leave the rest) + const addRemoteChainButton = await modal.findByRole('button', { name: 'Add chain' }) + await userEvent.click(addRemoteChainButton) + await userEvent.type(await modal.findByTestId('remoteChains-2'), '123') + }) + + await step('Transaction parameters', () => { + const [, , specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON().updateArgoBridgeConstraints).toEqual({ + operatorAccount: bobAddress, + pauserAccounts: [aliceAddress, daveAddress], + bridgingFee: Number(joy(0.2)), + thawnDuration: 100, + remoteChains: [37, 42, 123], + }) + }) + }), +} diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx index 83c3710b9d..8a16b300fa 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx @@ -317,6 +317,9 @@ export const DecreaseCouncilBudget: Story = { export const UpdateTokenPalletTokenConstraints: Story = { args: { type: 'UpdateTokenPalletTokenConstraintsProposalDetails' }, } +export const UpdateArgoBridgeConstraints: Story = { + args: { type: 'UpdateArgoBridgeConstraintsProposalDetails' }, +} // Disabled proposals export const Veto: Story = { diff --git a/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts b/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts index 569c2219c0..446c1e10fd 100644 --- a/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts +++ b/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts @@ -21029,6 +21029,7 @@ export type ProposalDetails = | SignalProposalDetails | SlashWorkingGroupLeadProposalDetails | TerminateWorkingGroupLeadProposalDetails + | UpdateArgoBridgeConstraintsProposalDetails | UpdateChannelPayoutsProposalDetails | UpdateGlobalNftLimitProposalDetails | UpdatePalletFrozenStatusProposalDetails @@ -32216,6 +32217,15 @@ export type UpcomingWorkingGroupOpeningWhereUniqueInput = { id: Scalars['ID'] } +export type UpdateArgoBridgeConstraintsProposalDetails = { + __typename: 'UpdateArgoBridgeConstraintsProposalDetails' + bridgingFee?: Maybe + operatorAccount?: Maybe + pauserAccounts?: Maybe> + remoteChains?: Maybe> + thawnDuration?: Maybe +} + export type UpdateChannelPayoutsProposalDetails = { __typename: 'UpdateChannelPayoutsProposalDetails' /** Can channel cashout the rewards */ diff --git a/packages/ui/src/common/api/schemas/schema.graphql b/packages/ui/src/common/api/schemas/schema.graphql index 595af2cd58..f332db590f 100644 --- a/packages/ui/src/common/api/schemas/schema.graphql +++ b/packages/ui/src/common/api/schemas/schema.graphql @@ -2596,6 +2596,7 @@ union ProposalDetails = | DecreaseCouncilBudgetProposalDetails | UpdateTokenPalletTokenConstraintsProposalDetails | SetEraPayoutDampingFactorProposalDetails + | UpdateArgoBridgeConstraintsProposalDetails union ProposalStatus = ProposalStatusDeciding @@ -3522,6 +3523,14 @@ type UpcomingOpeningRemoved { upcomingOpeningId: String! } +type UpdateArgoBridgeConstraintsProposalDetails { + operatorAccount: String + pauserAccounts: [String!] + bridgingFee: BigInt + thawnDuration: Int + remoteChains: [Int!] +} + type UpdateChannelPayoutsProposalDetails { """ Merkle root of the channel payouts diff --git a/packages/ui/src/common/components/buttons/Buttons.tsx b/packages/ui/src/common/components/buttons/Buttons.tsx index 2144ec19e1..6125752d14 100644 --- a/packages/ui/src/common/components/buttons/Buttons.tsx +++ b/packages/ui/src/common/components/buttons/Buttons.tsx @@ -6,6 +6,7 @@ import { BorderRad, Colors, Fonts, Transitions } from '../../constants' export type ButtonSize = 'small' | 'medium' | 'large' export interface ButtonProps extends ButtonSizingProps { + id?: string square?: boolean className?: string children?: React.ReactNode @@ -58,50 +59,34 @@ export function ButtonPrimary({ className, children, size, square, disabled, onC ) } -export function ButtonSecondary({ className, children, size, square, disabled, onClick }: ButtonProps) { +export function ButtonSecondary({ children, size, ...props }: ButtonProps) { return ( - + {children} ) } -export function ButtonGhost({ className, children, size, square, disabled, onClick, title }: ButtonProps) { +export function ButtonGhost({ children, size, ...props }: ButtonProps) { return ( - + {children} ) } -export function ButtonBareGhost({ className, children, size, square, disabled, onClick }: ButtonProps) { +export function ButtonBareGhost({ children, size, ...props }: ButtonProps) { return ( - + {children} ) } -export function ButtonLink({ className, children, square, borderless, bold, inline, disabled, onClick }: ButtonProps) { +export function ButtonLink({ children, size, ...props }: ButtonProps) { return ( - - {children} + + {children} ) } diff --git a/packages/ui/src/common/components/buttons/TransactionButton.tsx b/packages/ui/src/common/components/buttons/TransactionButton.tsx index b4f89bd6d1..d331e13719 100644 --- a/packages/ui/src/common/components/buttons/TransactionButton.tsx +++ b/packages/ui/src/common/components/buttons/TransactionButton.tsx @@ -33,10 +33,10 @@ interface TransactionButtonProps extends ButtonProps { isResponsive?: boolean } -export const TransactionButton = ({ isResponsive, disabled, ...props }: TransactionButtonProps) => { +export const TransactionButton = ({ isResponsive, disabled, style, ...props }: TransactionButtonProps) => { const { isTransactionPending } = useTransactionStatus() - const Button = buttonTypes[props.style] + const Button = buttonTypes[style] return ( diff --git a/packages/ui/src/common/components/forms/FieldList.stories.tsx b/packages/ui/src/common/components/forms/FieldList.stories.tsx new file mode 100644 index 0000000000..c8ed8b4e7c --- /dev/null +++ b/packages/ui/src/common/components/forms/FieldList.stories.tsx @@ -0,0 +1,55 @@ +import { Meta, StoryObj } from '@storybook/react' +import React, { useEffect } from 'react' +import { useFormContext } from 'react-hook-form' + +import { FieldList } from './FieldList' +import { InputComponent, InputText } from './InputComponent' + +type Args = { + addLabel?: string + align?: 'start' | 'end' + initial?: string[] + onChange: (values: unknown) => void +} + +export default { + title: 'Common/Forms/FieldList', + component: FieldList, + args: { + addLabel: 'Add field', + align: 'end', + initial: ['foo'], + }, + argTypes: { + align: { control: { type: 'inline-radio' }, options: ['start', 'end'] }, + onChange: { action: 'change' }, + }, +} as Meta + +export const Default: StoryObj = { + name: 'FieldList', + render: ({ onChange, initial, ...props }) => { + const form = useFormContext<{ input: string[] }>() + + useEffect(() => { + form.setValue('input', initial ?? []) + + const subscription = form.watch((data) => onChange(data.input?.filter((v) => typeof v === 'string'))) + return () => subscription.unsubscribe() + }, [form.watch]) + + return ( + ( + + + + )} + unmount={({ name }) => form.unregister(name)} + initialSize={initial?.length} + /> + ) + }, +} diff --git a/packages/ui/src/common/components/forms/FieldList.tsx b/packages/ui/src/common/components/forms/FieldList.tsx new file mode 100644 index 0000000000..6a8df18c74 --- /dev/null +++ b/packages/ui/src/common/components/forms/FieldList.tsx @@ -0,0 +1,108 @@ +import { last } from 'lodash' +import React, { Fragment, ReactNode, useReducer, useRef } from 'react' +import styled from 'styled-components' + +import { ButtonGhost, ButtonPrimary } from '../buttons' +import { CrossIcon, PlusIcon } from '../icons' + +type FieldProps = { name: `${Name}.${number}`; id?: string; index: number } +type Props = { + render: (props: FieldProps) => ReactNode + unmount?: (props: FieldProps) => void + id?: string + name?: Name + initialSize?: number + addLabel?: string + align?: 'start' | 'end' + inputWidth?: 's' | 'xs' | 'full' +} + +type State = { ids: number[]; next: number } +type Action = { type: 'add' } | { type: 'remove'; index: number } +const init = (initialSize: number): State => ({ + ids: Array.from({ length: initialSize }, (_, i) => i), + next: initialSize, +}) +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'add': { + const current = state.next + return { ids: [...state.ids, current], next: current + 1 } + } + case 'remove': + return { ids: state.ids.filter((index) => index !== action.index), next: state.next } + } +} + +export function FieldList({ + render, + unmount, + id, + name = 'field' as Key, + initialSize = 0, + addLabel, + ...styleProps +}: Props) { + const [{ ids }, dispatch] = useReducer(reducer, initialSize, init) + + const container = useRef(null) + const addField = () => { + dispatch({ type: 'add' }) + setTimeout(() => { + const inputs = container.current?.querySelectorAll('input') + last(inputs)?.focus() + }) + } + + return ( + + {ids.map((index) => { + const fieldProps = { name: `${name}.${index}` as const, id: `${id}-${index}`, index } + return ( + + {render(fieldProps)} + { + dispatch({ type: 'remove', index }) + unmount?.(fieldProps) + }} + id={`${fieldProps.id}-remove`} + className="remove-button" + > + + + + ) + })} + + + {addLabel} + + + ) +} + +const Container = styled.div, 'align' | 'inputWidth'>>` + display: grid; + grid-template-columns: 1fr 50px; + width: 100%; + width: ${({ inputWidth }) => { + switch (inputWidth) { + case 's': + return '320px' + case 'xs': + return '200px' + default: + return '100%' + } + }}; + gap: 8px; + align-items: center; + + & > :last-child { + grid-column: 1 / -1; + justify-self: ${({ align = 'start' }) => align}; + } +` diff --git a/packages/ui/src/common/components/storybookParts/Decorators.tsx b/packages/ui/src/common/components/storybookParts/Decorators.tsx new file mode 100644 index 0000000000..e4e0f939ff --- /dev/null +++ b/packages/ui/src/common/components/storybookParts/Decorators.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import styled from 'styled-components' + +import { ProposalConstantsWrapper } from '@/proposals/modals/AddNewProposal/components/ProposalConstantsWrapper' + +import { StepDescriptionColumn, Stepper, StepperBody, StepperModalBody, StepperModalWrapper } from '../StepperModal' + +export const AddProposalModalDecorator = (Story: CallableFunction) => ( + + + + + + + + + + + + + +) + +const ModalContainer = styled.div` + max-width: 1240px; +` diff --git a/packages/ui/src/common/utils.ts b/packages/ui/src/common/utils.ts index 7c7b5e349b..615fa77f08 100644 --- a/packages/ui/src/common/utils.ts +++ b/packages/ui/src/common/utils.ts @@ -143,6 +143,19 @@ export const arrayGroupBy = (items: Item[], key: keyof Item) => {} ) +// Maps: + +export const mapCloneSet = (map: Map, key: K, value: V): Map => { + const clone = new Map(map) + clone.set(key, value) + return clone +} +export const mapCloneDelete = (map: Map, key: K): Map => { + const clone = new Map(map) + clone.delete(key) + return clone +} + // Promises: type MapperP = (value: T, index: number, array: T[] | readonly T[]) => Promise diff --git a/packages/ui/src/council/queries/__generated__/council.generated.tsx b/packages/ui/src/council/queries/__generated__/council.generated.tsx index 6e17e6e3a1..0b7b6e5e54 100644 --- a/packages/ui/src/council/queries/__generated__/council.generated.tsx +++ b/packages/ui/src/council/queries/__generated__/council.generated.tsx @@ -94,6 +94,7 @@ export type PastCouncilProposalsFieldsFragment = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -640,6 +641,7 @@ export type FundingRequestApprovedFragment = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -792,6 +794,7 @@ export type GetPastCouncilQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -893,6 +896,7 @@ export type GetPastCouncilMembersQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -987,6 +991,7 @@ export type GetPastCouncilProposalsQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -1532,6 +1537,7 @@ export type GetPastCouncilStatsQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } diff --git a/packages/ui/src/memberships/components/SelectValidatorAccounts.tsx b/packages/ui/src/memberships/components/SelectValidatorAccounts.tsx index 866a226904..6acbb87cd2 100644 --- a/packages/ui/src/memberships/components/SelectValidatorAccounts.tsx +++ b/packages/ui/src/memberships/components/SelectValidatorAccounts.tsx @@ -3,56 +3,56 @@ import styled from 'styled-components' import { SelectAccount } from '@/accounts/components/SelectAccount' import { Account } from '@/accounts/types' -import { ButtonGhost, ButtonPrimary } from '@/common/components/buttons' import { BaseToggleCheckbox, InputComponent, Label } from '@/common/components/forms' -import { CrossIcon, PlusIcon } from '@/common/components/icons' -import { AlertSymbol } from '@/common/components/icons/symbols' -import { Row, RowInline } from '@/common/components/Modal' +import { FieldList } from '@/common/components/forms/FieldList' +import { RowInline } from '@/common/components/Modal' import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' import { TextMedium, TextSmall } from '@/common/components/typography' -import { toSpliced } from '@/common/model/Polyfill' +import { mapCloneDelete, mapCloneSet } from '@/common/utils' import { useValidators } from '@/validators/hooks/useValidators' type SelectValidatorAccountsState = { isValidator: boolean - accounts: (Account | undefined)[] + accounts: Map } type Action = | { type: 'SetInitialAccounts'; value: Account[] } | { type: 'ToggleIsValidator'; value: boolean } - | { type: 'AddAccount'; value: { index: number; account?: Account } } + | { type: 'AddAccount'; value: { index: number; account: Account } } | { type: 'RemoveAccount'; value: { index: number } } const reducer = (state: SelectValidatorAccountsState, action: Action): SelectValidatorAccountsState => { switch (action.type) { case 'SetInitialAccounts': { - return { isValidator: true, accounts: action.value } + const accounts = new Map(action.value.map((account, index) => [index, account])) + return { isValidator: true, accounts } } case 'ToggleIsValidator': { return { ...state, isValidator: action.value } } case 'AddAccount': { const { index, account } = action.value - return { ...state, accounts: toSpliced(state.accounts, index, 1, account) } + return { ...state, accounts: mapCloneSet(state.accounts, index, account) } } case 'RemoveAccount': { const { index } = action.value - return { ...state, accounts: toSpliced(state.accounts, index, 1) } + return { ...state, accounts: mapCloneDelete(state.accounts, index) } } } } type UseSelectValidatorAccounts = { isValidatorAccount: (account: Account) => boolean - initialValidatorAccounts: Account[] + initialValidatorAccounts?: Account[] state: SelectValidatorAccountsState onChange: (action: Action) => void } export const useSelectValidatorAccounts = (boundAccounts: Account[] = []): UseSelectValidatorAccounts => { - const [state, dispatch] = useReducer(reducer, { isValidator: false, accounts: [] }) + const [state, dispatch] = useReducer(reducer, { isValidator: false, accounts: new Map() }) + const hasNoBoundAccounts = boundAccounts.length === 0 - const validators = useValidators({ skip: !state.isValidator && boundAccounts.length === 0 }) + const validators = useValidators({ skip: !state.isValidator && hasNoBoundAccounts }) const validatorAddresses = useMemo( () => validators?.flatMap(({ stashAccount: stash, controllerAccount: ctrl }) => (ctrl ? [stash, ctrl] : [stash])), [validators] @@ -77,17 +77,14 @@ export const useSelectValidatorAccounts = (boundAccounts: Account[] = []): UseSe return { initialValidatorAccounts, state, isValidatorAccount, onChange: dispatch } } -export const SelectValidatorAccounts = ({ isValidatorAccount, state, onChange }: UseSelectValidatorAccounts) => { +export const SelectValidatorAccounts = ({ + isValidatorAccount, + state, + onChange, + initialValidatorAccounts, +}: UseSelectValidatorAccounts) => { const handleIsValidatorChange = (value: boolean) => onChange({ type: 'ToggleIsValidator', value }) - - const AddAccount = (index: number, account: Account | undefined) => - onChange({ type: 'AddAccount', value: { index, account } }) - const RemoveAccount = (index: number) => onChange({ type: 'RemoveAccount', value: { index } }) - - const validatorAccountSelectorFilter = (index: number, account: Account) => - toSpliced(state.accounts, index, 1).every( - (accountOrUndefined) => accountOrUndefined?.address !== account.address - ) && isValidatorAccount(account) + const selectedAddresses = Array.from(state.accounts.values()).map(({ address }) => address) return ( <> @@ -101,7 +98,7 @@ export const SelectValidatorAccounts = ({ isValidatorAccount, state, onChange }: /> - {state.isValidator && ( + {initialValidatorAccounts && state.isValidator && ( <> @@ -111,53 +108,39 @@ export const SelectValidatorAccounts = ({ isValidatorAccount, state, onChange }: * + If your validator account is not in your signer wallet, paste the account address to the field below: - {state.accounts.map((account, index) => ( - - - + + { + const account = state.accounts.get(index) + const isInvalid = account && !isValidatorAccount(account) + + return ( + AddAccount(index, account)} - filter={(account) => validatorAccountSelectorFilter(index, account)} + onChange={(account) => onChange({ type: 'AddAccount', value: { index: index, account } })} + filter={(account) => !selectedAddresses.includes(account.address) && isValidatorAccount(account)} /> - { - RemoveAccount(index) - }} - className="remove-button" - > - - - - {account && !isValidatorAccount(account) && ( - - - - - - - - This account is neither a validator controller account nor a validator stash account. - - - )} - - ))} - - AddAccount(state.accounts.length, undefined)} - > - Add Validator Account - - + ) + }} + unmount={({ index }) => onChange({ type: 'RemoveAccount', value: { index } })} + addLabel="Add Validator Account" + initialSize={initialValidatorAccounts.length} + align="end" + /> )} @@ -171,18 +154,3 @@ const SelectValidatorAccountWrapper = styled.div` flex-direction: column; gap: 8px; ` - -const InputNotificationIcon = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 12px; - height: 12px; - color: inherit; - padding-right: 2px; - - .blackPart, - .primaryPart { - fill: currentColor; - } -` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 0b1143fdd4..16c3bb3e9e 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -144,7 +144,7 @@ export const BuyMembershipForm = ({ const selectValidatorAccounts = useSelectValidatorAccounts() const { isValidatorAccount, - state: { isValidator, accounts: validatorAccounts }, + state: { isValidator, accounts: accountsMap }, } = selectValidatorAccounts useEffect(() => { @@ -159,17 +159,17 @@ export const BuyMembershipForm = ({ } }, [data?.membershipsConnection.totalCount]) + const validatorAccounts = Array.from(accountsMap.values()) const isFormValid = !isUploading && form.formState.isValid && - (!isValidator || - (validatorAccounts.length > 0 && validatorAccounts.every((account) => account && isValidatorAccount(account)))) + (!isValidator || (validatorAccounts.length > 0 && validatorAccounts.every(isValidatorAccount))) const isDisabled = type === 'onBoarding' && process.env.REACT_APP_CAPTCHA_SITE_KEY ? !captchaToken || !isFormValid : !isFormValid const submit = () => { - const accounts = uniqBy(validatorAccounts as Account[], 'address') + const accounts = uniqBy(validatorAccounts, 'address') form.setValue('validatorAccounts', accounts) const values = form.getValues() uploadAvatarAndSubmit({ ...values, externalResources: { ...definedValues(values.externalResources) } }) diff --git a/packages/ui/src/memberships/modals/InviteMemberModal/InviteMemberFormModal.tsx b/packages/ui/src/memberships/modals/InviteMemberModal/InviteMemberFormModal.tsx index 32390dfe40..3655eeda8d 100644 --- a/packages/ui/src/memberships/modals/InviteMemberModal/InviteMemberFormModal.tsx +++ b/packages/ui/src/memberships/modals/InviteMemberModal/InviteMemberFormModal.tsx @@ -14,7 +14,6 @@ import { Row, } from '@/common/components/Modal' import { TextMedium } from '@/common/components/typography' -import { useKeyring } from '@/common/hooks/useKeyring' import { useYupValidationResolver } from '@/common/utils/validation' import { AvatarInput } from '@/memberships/components/AvatarInput' import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' @@ -51,14 +50,13 @@ const formDefaultValues = { export const InviteMemberFormModal = ({ onClose, onSubmit }: InviteProps) => { const { active } = useMyMemberships() - const keyring = useKeyring() const { uploadAvatarAndSubmit, isUploading } = useUploadAvatarAndSubmit(onSubmit) const [formHandleMap, setFormHandleMap] = useState('') const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: formHandleMap } } }) const form = useForm({ resolver: useYupValidationResolver(InviteMemberSchema), - context: { size: data?.membershipsConnection.totalCount, keyring }, + context: { size: data?.membershipsConnection.totalCount }, mode: 'onChange', defaultValues: formDefaultValues, }) diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx index 2fc3e34a80..45ce1448d1 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx @@ -66,8 +66,8 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) ) const selectValidatorAccounts = useSelectValidatorAccounts(boundAccounts) const { - initialValidatorAccounts, - state: { isValidator, accounts: validatorAccounts }, + initialValidatorAccounts = [], + state: { isValidator, accounts: accountsMap }, isValidatorAccount, } = selectValidatorAccounts @@ -137,12 +137,13 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) const filterRoot = useCallback(filterAccount(controllerAccount), [controllerAccount]) const filterController = useCallback(filterAccount(rootAccount), [rootAccount]) + const validatorAccounts = useMemo(() => Array.from(accountsMap.values()), [accountsMap]) const formData = useMemo( () => ({ ...form.getValues(), isValidator, - validatorAddresses: validatorAccounts.flatMap((account) => account?.address ?? []), + validatorAddresses: validatorAccounts.map(({ address }) => address), } as UpdateMemberForm), [form.getValues(), validatorAccounts] ) @@ -150,8 +151,7 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) const canUpdate = form.formState.isValid && hasAnyEdits(formData, updateMemberFormInitial) && - (!isValidator || - (validatorAccounts.length > 0 && validatorAccounts.every((account) => account && isValidatorAccount(account)))) + (!isValidator || (validatorAccounts.length > 0 && validatorAccounts.every(isValidatorAccount))) const willBecomeUnverifiedValidator = updateMemberFormInitial.isValidator && hasAnyMetadateChanges(formData, updateMemberFormInitial) diff --git a/packages/ui/src/memberships/model/validation.ts b/packages/ui/src/memberships/model/validation.ts index ddeaf76be6..96a02bb9ac 100644 --- a/packages/ui/src/memberships/model/validation.ts +++ b/packages/ui/src/memberships/model/validation.ts @@ -80,7 +80,6 @@ export const NewAddressSchema = (which: string) => name: Yup.string(), address: Yup.string().required('This field is required'), }) - .test(which, 'Address is invalid', (value, testContext) => { - const keyring = testContext?.options?.context?.keyring - return value.address ? isValidAddress(value.address, keyring) : true + .test(which, 'Address is invalid', (value) => { + return value.address ? isValidAddress(value.address) : true }) diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 43dbce3ffc..1217b8f7c6 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -106,6 +106,13 @@ const proposalDetails: Record { diff --git a/packages/ui/src/mocks/helpers/asChainData.ts b/packages/ui/src/mocks/helpers/asChainData.ts index 2473d5470e..32fca265ad 100644 --- a/packages/ui/src/mocks/helpers/asChainData.ts +++ b/packages/ui/src/mocks/helpers/asChainData.ts @@ -11,6 +11,7 @@ const mockApiMethods = (mapFn: (data: any) => any) => (_data: any) => { try { return Object.defineProperties(data, { unwrap: { value: () => data }, + unwrapOr: { value: () => data }, toJSON: { value: () => data }, isSome: { value: Object.keys(data).length > 0 }, get: { diff --git a/packages/ui/src/mocks/helpers/asDerivedBalances.ts b/packages/ui/src/mocks/helpers/asDerivedBalances.ts new file mode 100644 index 0000000000..94f9932845 --- /dev/null +++ b/packages/ui/src/mocks/helpers/asDerivedBalances.ts @@ -0,0 +1,36 @@ +import { DeriveBalancesAll } from '@polkadot/api-derive/types' +import { BN_ZERO } from '@polkadot/util' +import BN from 'bn.js' + +import { LockType } from '@/accounts/types' +import { createType } from '@/common/model/createType' + +import { createBalanceLock } from '../../../test/_mocks/chainTypes' + +type Balances = { available?: number; locked?: number; lockId?: LockType } + +export const asDerivedBalances = ({ available, lockId, locked }: Balances) => { + const availableBalance = new BN(available ?? 0) + const lockedBalance = new BN(locked ?? 0) + + return { + availableBalance: createType('Balance', availableBalance), + lockedBalance: createType('Balance', lockedBalance), + accountId: createType('AccountId', 'j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf'), + accountNonce: createType('Index', 1), + freeBalance: createType('Balance', availableBalance.add(lockedBalance)), + frozenFee: new BN(0), + frozenMisc: new BN(0), + isVesting: false, + lockedBreakdown: lockedBalance.eq(BN_ZERO) ? [] : [createBalanceLock(locked!, lockId ?? 'Bound Staking Account')], + reservedBalance: new BN(0), + vestedBalance: new BN(0), + vestedClaimable: new BN(0), + vestingEndBlock: createType('BlockNumber', 1234), + vestingLocked: new BN(0), + vestingPerBlock: new BN(0), + vestingTotal: new BN(0), + votingBalance: new BN(0), + vesting: [], + } as unknown as DeriveBalancesAll +} diff --git a/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts b/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts index 1181e7ed10..a984536302 100644 --- a/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts +++ b/packages/ui/src/mocks/helpers/proposalDetailsToConstantKey.ts @@ -36,4 +36,5 @@ const proposalDetailsToConstantKeyMap = new Map => + waitFor(() => { + const element = container.querySelector(selector) + if (!element) throw `Element with selector ${selector} not found` + return element as HTMLElement + }, options) diff --git a/packages/ui/src/mocks/providers/api.tsx b/packages/ui/src/mocks/providers/api.tsx index b07e185aa6..fc923656e0 100644 --- a/packages/ui/src/mocks/providers/api.tsx +++ b/packages/ui/src/mocks/providers/api.tsx @@ -12,6 +12,7 @@ import { warning } from '@/common/logger' import { createType } from '@/common/model/createType' import { asChainData } from '../helpers/asChainData' +import { asDerivedBalances } from '../helpers/asDerivedBalances' import { TxMock, fromTxMock } from '../helpers/transactions' export const BLOCK_HEAD = 1337 @@ -42,6 +43,7 @@ export const MockApiProvider: FC = ({ children, chain }) => { // Common mocks: const defaultDerive = { staking: { erasRewards: [], erasPoints: [] }, + balances: { all: asDerivedBalances({}) }, } const defaultQuery = { session: { validators: [] }, diff --git a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx index 33ad5858c8..f481815f23 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/ProposalDetails.tsx @@ -16,6 +16,7 @@ import { useWorkingGroup } from '@/working-groups/hooks/useWorkingGroup' import { Address, + AddressesPreview, Amount, DestinationsPreview, Divider, @@ -57,6 +58,7 @@ const renderTypeMapper: Partial> = { Hash: Hash, DestinationsPreview: DestinationsPreview, BlockTimeDisplay: BlockTimeDisplay, + AddressesPreview: AddressesPreview, } export const ProposalDetails = ({ proposalDetails, gracePeriod, exactExecutionBlock, createdInBlock }: Props) => { diff --git a/packages/ui/src/proposals/components/ProposalDetails/renderers/Address.tsx b/packages/ui/src/proposals/components/ProposalDetails/renderers/Address.tsx index da2536e766..c694b077a8 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/renderers/Address.tsx +++ b/packages/ui/src/proposals/components/ProposalDetails/renderers/Address.tsx @@ -2,18 +2,23 @@ import React from 'react' import { CopyButton } from '@/common/components/buttons' import { NumericValue, StatiscticSpaceRow, StatisticItem } from '@/common/components/statistics' +import { TextBig } from '@/common/components/typography' import { shortenAddress } from '@/common/model/formatters' interface Props { label: string - value: string + value?: string } export const Address = ({ label, value }: Props) => ( - - {shortenAddress(value, 12)} - - + {value ? ( + + {shortenAddress(value, 12)} + + + ) : ( + None + )} ) diff --git a/packages/ui/src/proposals/components/ProposalDetails/renderers/AddressesPreview.tsx b/packages/ui/src/proposals/components/ProposalDetails/renderers/AddressesPreview.tsx new file mode 100644 index 0000000000..7e9e46f2df --- /dev/null +++ b/packages/ui/src/proposals/components/ProposalDetails/renderers/AddressesPreview.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react' +import styled from 'styled-components' + +import { AnonymousAccount } from '@/accounts/components/AnonymousAccount' +import { CloseButton } from '@/common/components/buttons' +import { ArrowRightIcon } from '@/common/components/icons' +import { SidePaneGlass, SidePaneTitle, SidePanelTop } from '@/common/components/SidePane' +import { StatisticButton } from '@/common/components/statistics/StatisticButton' +import { TextInlineBig } from '@/common/components/typography' +import { + PreviewPanel, + PreviewPanelBody, + PreviewPanelHeader, +} from '@/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate' + +import { Address } from './Address' + +type Props = { + label: string + value: string[] +} +export const AddressesPreview = ({ label, value }: Props) => { + const [isOpen, setOpen] = useState(false) + + if (value.length < 2) { + return
+ } + + return ( + <> + setOpen(true)} icon={}> + + {value.length} accounts + + + {isOpen && ( + setOpen(false)}> + + + + {label} + setOpen(false)} /> + + + + + {value.map((address) => ( + + ))} + + + + + )} + + ) +} + +const AccountList = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +` diff --git a/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts b/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts index eb6ff878fa..c47a707000 100644 --- a/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts +++ b/packages/ui/src/proposals/components/ProposalDetails/renderers/index.ts @@ -11,3 +11,4 @@ export * from './ProposalLink' export * from './OpeningLink' export * from './Hash' export * from './DestinationsPreview' +export * from './AddressesPreview' diff --git a/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts b/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts index 4e08b30bbd..8b70b0f150 100644 --- a/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts +++ b/packages/ui/src/proposals/helpers/getDetailsRenderStructure.ts @@ -42,6 +42,7 @@ export type RenderType = | 'Hash' | 'DestinationsPreview' | 'BlockTimeDisplay' + | 'AddressesPreview' export interface RenderNode { label: string @@ -277,6 +278,40 @@ const openingLinkMapper: Mapper = (value) => { }, ] } +const addressMapper = + (label: string) => + (value: string): RenderNode[] => { + return [ + { + label, + value, + renderType: 'Address', + }, + ] + } +const addressesMapper = + (label: string) => + (value: string[]): RenderNode[] => { + return [ + { + label, + value, + renderType: 'AddressesPreview', + }, + ] + } + +const listMapper = + (label: string) => + (value: unknown[]): RenderNode[] => { + return [ + { + label, + value: value.map(String).join(', '), + renderType: 'Text', + }, + ] + } const percentageProposalsAmount: ProposalType[] = ['setReferralCut'] @@ -322,6 +357,13 @@ const mappers: Partial>> = { ammBuyTxFees: percentageMapper('Proposed AMM buy transaction fees'), ammSellTxFees: percentageMapper('Proposed AMM sell transaction fees'), bloatBond: amountMapper('Proposed bloat bond'), + + // UpdateArgoBridgeConstraints + operatorAccount: addressMapper('Operator Account'), + pauserAccounts: addressesMapper('Pauser Accounts'), + bridgingFee: amountMapper('Proposed bridging fee'), + thawnDuration: blocksMapper('Proposed thawn duration'), + remoteChains: listMapper('Remote Chains'), } const mapProposalDetail = (key: ProposalDetailsKeys, proposalDetails: ProposalWithDetails['details']) => { diff --git a/packages/ui/src/proposals/hooks/useProposalConstants.ts b/packages/ui/src/proposals/hooks/useProposalConstants.ts index 1108881ac5..48a1eb25ea 100644 --- a/packages/ui/src/proposals/hooks/useProposalConstants.ts +++ b/packages/ui/src/proposals/hooks/useProposalConstants.ts @@ -61,4 +61,5 @@ const proposalTypeToConstantKey = new Map } + case matches('specificParameters.updateArgoBridgeConstraints'): { + return + } default: return null } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/UpdateArgoBridgeConstraints.stories.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/UpdateArgoBridgeConstraints.stories.tsx new file mode 100644 index 0000000000..6e0b8027f4 --- /dev/null +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/UpdateArgoBridgeConstraints.stories.tsx @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from '@storybook/react' + +import { AddProposalModalDecorator } from '@/common/components/storybookParts/Decorators' +import { member } from '@/mocks/data/members' +import { joy } from '@/mocks/helpers' +import { MocksParameters } from '@/mocks/providers' + +import { UpdateArgoBridgeConstraints } from './UpdateArgoBridgeConstraints' + +const aliceAddress = member('alice').controllerAccount +const bobAddress = member('bob').controllerAccount +const charlieAddress = member('charlie').controllerAccount + +export default { + title: 'Pages/Proposals/ProposalList/Current/Modals/AddNewProposalModal/UpdateArgoBridgeConstraints', + component: UpdateArgoBridgeConstraints, + parameters: { + mocks: { + chain: { + query: { + argoBridge: { + operatorAccount: aliceAddress, + pauserAccounts: [bobAddress, charlieAddress], + bridgingFee: joy(0.1), + thawnDuration: 200, + remoteChains: [37, 42], + }, + }, + }, + } satisfies MocksParameters, + }, +} as Meta + +export const Default: StoryObj = { + name: 'UpdateArgoBridgeConstraints', + decorators: [AddProposalModalDecorator], +} diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/UpdateArgoBridgeConstraints.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/UpdateArgoBridgeConstraints.tsx new file mode 100644 index 0000000000..76d25c3eec --- /dev/null +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/UpdateArgoBridgeConstraints.tsx @@ -0,0 +1,143 @@ +import { uniq } from 'lodash' +import React, { useEffect, useState } from 'react' +import { useFormContext } from 'react-hook-form' + +import { SelectAccount } from '@/accounts/components/SelectAccount' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { useApi } from '@/api/hooks/useApi' +import { CurrencyName } from '@/app/constants/currency' +import { InputComponent, InputNumber, Label, TokenInput } from '@/common/components/forms' +import { FieldList } from '@/common/components/forms/FieldList' +import { Loading } from '@/common/components/Loading' +import { Row } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { TextMedium, TextSmall } from '@/common/components/typography' +import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' +import { formatJoyValue } from '@/common/model/formatters' +import { whenDefined } from '@/common/utils' +import { argoConstraints$ } from '@/proposals/model/argoConstraints' + +import { AddNewProposalForm } from '../../helpers' + +export const UpdateArgoBridgeConstraints = () => { + const { api } = useApi() + const current = useFirstObservableValue(() => argoConstraints$(api), [api?.isConnected]) + const form = useFormContext() + + const [isReady, setIsReady] = useState(false) + const { allAccounts } = useMyAccounts() + useEffect(() => { + if (!current) return + + form.setValue( + 'updateArgoBridgeConstraints.operatorAccount', + whenDefined(current.operatorAccount, (address) => accountOrNamed(allAccounts, address, 'Unsaved account')) + ) + form.setValue( + 'updateArgoBridgeConstraints.pauserAccounts', + current.pauserAccounts.map((address) => accountOrNamed(allAccounts, address, 'Unsaved account')) + ) + form.setValue('updateArgoBridgeConstraints.bridgingFee', current.bridgingFee) + form.setValue('updateArgoBridgeConstraints.thawnDuration', current.thawnDuration) + form.setValue('updateArgoBridgeConstraints.remoteChains', current.remoteChains) + + setIsReady(true) + }, [current]) + + const pauserAccounts = form.watch('updateArgoBridgeConstraints.pauserAccounts') + const pauserAddresses = uniq(pauserAccounts?.flatMap((account) => account?.address ?? []) ?? []) + + return ( + + + +

Specific parameters

+ Update Argo Bridge pallet constraints +
+
+ + {!isReady ? ( + + ) : ( + + + + + + + + Currently {current?.pauserAccounts.join(', ') ?? '-'} + ( + + !pauserAddresses.includes(account.address)} + /> + + )} + unmount={({ name }) => form.unregister(name)} + addLabel="Add account" + initialSize={current?.pauserAccounts.length} + /> + + + + + + + + + + + + + Currently {current?.remoteChains.join(', ') ?? '-'} + ( + + + + )} + unmount={({ name }) => form.unregister(name)} + addLabel="Add chain" + inputWidth="s" + initialSize={current?.remoteChains.length} + /> + + + )} + +
+ ) +} diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx index b5d5ca99b5..e985702438 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/modals/PreviewAndValidate/PreviewAndValidateModal.tsx @@ -32,7 +32,6 @@ import { import { TransactionInfo } from '@/common/components/TransactionInfo' import { TokenValue } from '@/common/components/typography' import { Colors } from '@/common/constants' -import { useKeyring } from '@/common/hooks/useKeyring' import { formatJoyValue } from '@/common/model/formatters' import { joy } from '@/mocks/helpers' @@ -52,7 +51,6 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp const { setValue, getValues } = useFormContext() const maxTotalAmount = api?.consts.proposalsCodex.fundingRequestProposalMaxTotalAmount const maxAllowedAccounts = api?.consts.proposalsCodex.fundingRequestProposalMaxAccounts?.toNumber() - const keyring = useKeyring() const { allAccounts } = useMyAccounts() const accounts = allAccounts as AccountOption[] const [previewAccounts, setPreviewAccounts] = useState([]) @@ -98,7 +96,7 @@ export const PreviewAndValidateModal = ({ onClose }: PreviewAndValidateModalProp csvInput.map((item: string) => { const splitAccountsAndAmounts = item.split(',') const amount = new BN(joy(splitAccountsAndAmounts[1])) - const isValidAccount = isValidAddress(splitAccountsAndAmounts[0], keyring) + const isValidAccount = isValidAddress(splitAccountsAndAmounts[0]) return { account: accountOrNamed(accounts, splitAccountsAndAmounts[0], 'Unknown Member'), amount: amount, diff --git a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts index d4a32fe1b0..5c9ef1864c 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/getSpecificParameters.ts @@ -1,5 +1,7 @@ import { OpeningMetadata } from '@joystream/metadata-protobuf' +import { uniq } from 'lodash' +import { isValidAddress } from '@/accounts/model/isValidAddress' import { Api } from '@/api' import { BN_ZERO } from '@/common/constants' import { createType } from '@/common/model/createType' @@ -219,6 +221,22 @@ export const getSpecificParameters = async ( }, }) } + case 'updateArgoBridgeConstraints': { + const values = specifics.updateArgoBridgeConstraints + return createType('PalletProposalsCodexProposalDetails', { + UpdateArgoBridgeConstraints: { + operatorAccount: values?.operatorAccount?.address, + pauserAccounts: uniq( + values?.pauserAccounts?.flatMap((account) => + account?.address && isValidAddress(account.address) ? account.address : [] + ) + ), + bridgingFee: values?.bridgingFee, + thawnDuration: values?.thawnDuration, + remoteChains: values?.remoteChains?.filter((chain) => typeof chain === 'number'), + }, + }) + } default: return createType('PalletProposalsCodexProposalDetails', { Signal: '' }) } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index 1d6658876f..b55747b007 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -202,6 +202,13 @@ export interface AddNewProposalForm { ammSellTxFees?: number bloatBond?: BN } + updateArgoBridgeConstraints?: { + operatorAccount?: Account + pauserAccounts?: (Account | undefined)[] + bridgingFee?: BN + thawnDuration?: number + remoteChains?: (number | undefined)[] + } } export const schemaFactory = (api?: Api) => { @@ -470,5 +477,17 @@ export const schemaFactory = (api?: Api) => { if (fields && Object.values(fields).some(isDefined)) return true return new Yup.ValidationError('At least one field is required', fields, 'updateTokenPalletTokenConstraints') }), + updateArgoBridgeConstraints: Yup.object() + .shape({ + operatorAccount: AccountSchema, + pauserAccounts: Yup.array(AccountSchema), + bridgingFee: BNSchema.test(minMixed(0, 'Amount must be 0 or greater')), + thawnDuration: NumberSchema.min(0, 'Duration must be 0 or greater'), + remoteChains: Yup.array(Yup.number()), + }) + .test((fields) => { + if (fields && Object.values(fields).some(isDefined)) return true + throw new Yup.ValidationError('At least one field is required', fields, 'updateTokenPalletTokenConstraints') + }), }) } diff --git a/packages/ui/src/proposals/modals/AddNewProposal/machine.ts b/packages/ui/src/proposals/modals/AddNewProposal/machine.ts index 3f5982ded0..6e0ff90716 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/machine.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/machine.ts @@ -52,6 +52,7 @@ export type AddNewProposalState = value: { specificParameters: 'updateTokenPalletTokenConstraints' } context: Required } + | { value: { specificParameters: 'updateArgoBridgeConstraints' }; context: Required } | { value: { specificParameters: 'fundingRequest' }; context: Required } | { value: { specificParameters: 'runtimeUpgrade' }; context: Required } | { value: { specificParameters: 'setReferralCut' }; context: Required } @@ -229,6 +230,7 @@ export const addNewProposalMachine = createMachine< { target: 'setEraPayoutDampingFactor', cond: isType('setEraPayoutDampingFactor') }, { target: 'decreaseCouncilBudget', cond: isType('decreaseCouncilBudget') }, { target: 'updateTokenPalletTokenConstraints', cond: isType('updateTokenPalletTokenConstraints') }, + { target: 'updateArgoBridgeConstraints', cond: isType('updateArgoBridgeConstraints') }, ], }, updateChannelPayouts: {}, @@ -236,6 +238,7 @@ export const addNewProposalMachine = createMachine< setEraPayoutDampingFactor: {}, decreaseCouncilBudget: {}, updateTokenPalletTokenConstraints: {}, + updateArgoBridgeConstraints: {}, signal: {}, setMaxValidatorCount: {}, setReferralCut: {}, diff --git a/packages/ui/src/proposals/model/argoConstraints.ts b/packages/ui/src/proposals/model/argoConstraints.ts new file mode 100644 index 0000000000..4ffd7ed2af --- /dev/null +++ b/packages/ui/src/proposals/model/argoConstraints.ts @@ -0,0 +1,22 @@ +import { Option } from '@polkadot/types' +import { AccountId32 } from '@polkadot/types/interfaces' +import { combineLatest, map } from 'rxjs' + +import { encodeAddress } from '@/accounts/model/encodeAddress' +import { Api } from '@/api' +import { whenDefined } from '@/common/utils' + +export const argoConstraints$ = (api: Api | undefined) => + whenDefined(api?.query.argoBridge, (argoBridge) => + combineLatest({ + operatorAccount: argoBridge + .operatorAccount() + .pipe(map((account: Option) => whenDefined(account.unwrapOr(null)?.toString(), encodeAddress))), + pauserAccounts: argoBridge + .pauserAccounts() + .pipe(map((accounts) => accounts.map((account) => encodeAddress(account.toString())))), + bridgingFee: argoBridge.bridgingFee(), + thawnDuration: argoBridge.thawnDuration().pipe(map((duration) => duration.toNumber())), + remoteChains: argoBridge.remoteChains().pipe(map((chains) => chains.map((chain) => chain.toNumber()))), + } as const) + ) diff --git a/packages/ui/src/proposals/model/proposalDescriptions.ts b/packages/ui/src/proposals/model/proposalDescriptions.ts index 8d2b391941..716c36f560 100644 --- a/packages/ui/src/proposals/model/proposalDescriptions.ts +++ b/packages/ui/src/proposals/model/proposalDescriptions.ts @@ -40,6 +40,7 @@ export const proposalDescriptions: ProposalDescriptions = { setEraPayoutDampingFactor: 'Apply a multiplier to the validator rewards to reduce or increase them.', decreaseCouncilBudget: 'Reduce the council budget by burning part of the tokens', updateTokenPalletTokenConstraints: 'Update CRT pallet constraints.', + updateArgoBridgeConstraints: 'Update Argo Bridge constraints.', createBlogPost: 'Council blog', editBlogPost: 'Unlocked blog post can be edited.', lockBlogPost: 'When a post is locked it can no longer be modified.', diff --git a/packages/ui/src/proposals/model/proposalDetails.ts b/packages/ui/src/proposals/model/proposalDetails.ts index a6a721fb23..476c1bdabd 100644 --- a/packages/ui/src/proposals/model/proposalDetails.ts +++ b/packages/ui/src/proposals/model/proposalDetails.ts @@ -28,6 +28,7 @@ export const proposalDetails: ProposalType[] = [ 'setEraPayoutDampingFactor', 'decreaseCouncilBudget', 'updateTokenPalletTokenConstraints', + 'updateArgoBridgeConstraints', ] export const enabledProposals: ProposalType[] = [ @@ -55,6 +56,7 @@ export const enabledProposals: ProposalType[] = [ 'setEraPayoutDampingFactor', 'decreaseCouncilBudget', 'updateTokenPalletTokenConstraints', + 'updateArgoBridgeConstraints', ] export const typenameToProposalDetails = (typename: string): ProposalType => { diff --git a/packages/ui/src/proposals/queries/__generated__/proposals.generated.tsx b/packages/ui/src/proposals/queries/__generated__/proposals.generated.tsx index 3730a8d3ee..af2ed4c2d6 100644 --- a/packages/ui/src/proposals/queries/__generated__/proposals.generated.tsx +++ b/packages/ui/src/proposals/queries/__generated__/proposals.generated.tsx @@ -86,6 +86,7 @@ export type ProposalFieldsFragment = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -542,6 +543,14 @@ export type ProposalWithDetailsFieldsFragment = { } } | null } + | { + __typename: 'UpdateArgoBridgeConstraintsProposalDetails' + operatorAccount?: string | null + pauserAccounts?: Array | null + bridgingFee?: string | null + thawnDuration?: number | null + remoteChains?: Array | null + } | { __typename: 'UpdateChannelPayoutsProposalDetails' channelCashoutsEnabled?: boolean | null @@ -932,6 +941,7 @@ export type ProposalMentionFieldsFragment = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -1045,6 +1055,7 @@ export type GetProposalsQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -1428,6 +1439,14 @@ export type GetProposalQuery = { } } | null } + | { + __typename: 'UpdateArgoBridgeConstraintsProposalDetails' + operatorAccount?: string | null + pauserAccounts?: Array | null + bridgingFee?: string | null + thawnDuration?: number | null + remoteChains?: Array | null + } | { __typename: 'UpdateChannelPayoutsProposalDetails' channelCashoutsEnabled?: boolean | null @@ -1809,6 +1828,7 @@ export type GetProposalMentionQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -1927,6 +1947,7 @@ export type GetLatestProposalByMemberIdQuery = { | { __typename: 'SignalProposalDetails' } | { __typename: 'SlashWorkingGroupLeadProposalDetails' } | { __typename: 'TerminateWorkingGroupLeadProposalDetails' } + | { __typename: 'UpdateArgoBridgeConstraintsProposalDetails' } | { __typename: 'UpdateChannelPayoutsProposalDetails' } | { __typename: 'UpdateGlobalNftLimitProposalDetails' } | { __typename: 'UpdatePalletFrozenStatusProposalDetails' } @@ -2200,6 +2221,13 @@ export const ProposalWithDetailsFieldsFragmentDoc = gql` ammSellTxFees bloatBond } + ... on UpdateArgoBridgeConstraintsProposalDetails { + operatorAccount + pauserAccounts + bridgingFee + thawnDuration + remoteChains + } } discussionThread { id diff --git a/packages/ui/src/proposals/queries/proposals.graphql b/packages/ui/src/proposals/queries/proposals.graphql index 8ede854488..b8871715c5 100644 --- a/packages/ui/src/proposals/queries/proposals.graphql +++ b/packages/ui/src/proposals/queries/proposals.graphql @@ -211,6 +211,13 @@ fragment ProposalWithDetailsFields on Proposal { ammSellTxFees bloatBond } + ... on UpdateArgoBridgeConstraintsProposalDetails { + operatorAccount + pauserAccounts + bridgingFee + thawnDuration + remoteChains + } } discussionThread { diff --git a/packages/ui/src/proposals/types/ProposalDetails.ts b/packages/ui/src/proposals/types/ProposalDetails.ts index c82e042010..7daec70bce 100644 --- a/packages/ui/src/proposals/types/ProposalDetails.ts +++ b/packages/ui/src/proposals/types/ProposalDetails.ts @@ -100,6 +100,14 @@ type UpdateTokenPalletTokenConstraintsDetail = { bloatBond?: BN } +type UpdateArgoBridgeConstraintsDetail = { + operatorAccount?: string + pauserAccounts?: string[] + bridgingFee?: BN + thawnDuration?: number + remoteChains?: number[] +} + export type FundingRequestDetails = ProposalDetailsNew<'fundingRequest', DestinationsDetail> export type CreateLeadOpeningDetails = ProposalDetailsNew< 'createWorkingGroupLeadOpening', @@ -176,6 +184,11 @@ export type UpdateTokenPalletTokenConstraintsDetails = ProposalDetailsNew< UpdateTokenPalletTokenConstraintsDetail > +export type UpdateArgoBridgeConstraintsDetails = ProposalDetailsNew< + 'updateArgoBridgeConstraints', + UpdateArgoBridgeConstraintsDetail +> + export type ProposalDetails = | BaseProposalDetails | FundingRequestDetails @@ -203,6 +216,7 @@ export type ProposalDetails = | SetEraPayoutDampingFactorProposalDetails | DecreaseCouncilBudgetDetails | UpdateTokenPalletTokenConstraintsDetails + | UpdateArgoBridgeConstraintsDetails export type ProposalDetailsKeys = KeysOfUnion @@ -432,6 +446,17 @@ const asUpdateTokenPalletTokenConstraints: DetailsCast<'UpdateTokenPalletTokenCo bloatBond: whenDefined(fragment.bloatBond, asBN), }) +const asUpdateArgoBridgeConstraints: DetailsCast<'UpdateArgoBridgeConstraintsProposalDetails'> = ( + fragment +): UpdateArgoBridgeConstraintsDetails => ({ + type: 'updateArgoBridgeConstraints', + operatorAccount: fragment.operatorAccount ?? undefined, + pauserAccounts: fragment.pauserAccounts ?? undefined, + bridgingFee: whenDefined(fragment.bridgingFee, asBN), + thawnDuration: fragment.thawnDuration ?? undefined, + remoteChains: fragment.remoteChains ?? undefined, +}) + interface DetailsCast { (fragment: DetailsFragment & { __typename: T }, extra?: ProposalExtraDetails): ProposalDetails } @@ -462,6 +487,7 @@ const detailsCasts: Partial>> = SetEraPayoutDampingFactorProposalDetails: asSetEraPayoutDampingFactor, DecreaseCouncilBudgetProposalDetails: asDecreaseCouncilBudget, UpdateTokenPalletTokenConstraintsProposalDetails: asUpdateTokenPalletTokenConstraints, + UpdateArgoBridgeConstraintsProposalDetails: asUpdateArgoBridgeConstraints, } export const asProposalDetails = (fragment: DetailsFragment, extra?: ProposalExtraDetails): ProposalDetails => { diff --git a/packages/ui/src/proposals/types/proposals.ts b/packages/ui/src/proposals/types/proposals.ts index be97f039da..5c7e4541e5 100644 --- a/packages/ui/src/proposals/types/proposals.ts +++ b/packages/ui/src/proposals/types/proposals.ts @@ -65,6 +65,7 @@ export type ProposalType = | 'setEraPayoutDampingFactor' | 'decreaseCouncilBudget' | 'updateTokenPalletTokenConstraints' + | 'updateArgoBridgeConstraints' ) export type DisabledProposal = 'createBlogPost' | 'editBlogPost' | 'lockBlogPost' | 'unlockBlogPost' diff --git a/packages/ui/test/_mocks/transactions.ts b/packages/ui/test/_mocks/transactions.ts index a7fa39d264..5199e51caf 100644 --- a/packages/ui/test/_mocks/transactions.ts +++ b/packages/ui/test/_mocks/transactions.ts @@ -1,5 +1,4 @@ import { AugmentedEvents } from '@polkadot/api/types' -import { DeriveBalancesAll } from '@polkadot/api-derive/types' import { AnyTuple } from '@polkadot/types/types' import BN from 'bn.js' import { set } from 'lodash' @@ -7,18 +6,19 @@ import { from, Observable, of } from 'rxjs' import { toBalances } from '@/accounts/model/toBalances' import { UseAccounts } from '@/accounts/providers/accounts/provider' -import { Account, LockType } from '@/accounts/types' +import { Account } from '@/accounts/types' import { Api } from '@/api' import { UseApi } from '@/api/providers/provider' import { BN_ZERO } from '@/common/constants' import { createType } from '@/common/model/createType' import { ExtractTuple } from '@/common/model/JoystreamNode' +import { asDerivedBalances } from '@/mocks/helpers/asDerivedBalances' import { createErrorEvents, createSuccessEvents, stubTransactionResult } from '@/mocks/helpers/transactions' import { proposalDetails } from '@/proposals/model/proposalDetails' import { mockedBalances, mockedMyBalances, mockedUseMyAccounts } from '../setup' -import { createBalanceLock, createRuntimeDispatchInfo } from './chainTypes' +import { createRuntimeDispatchInfo } from './chainTypes' export const currentStubErrorMessage = 'Balance too low to send value.' @@ -224,33 +224,8 @@ export const stubCouncilAndReferendum = ( stubQuery(api, 'council.nextRewardPayments', new BN(1000)) } -type Balances = { available?: number; locked?: number; lockId?: LockType } - -export const stubBalances = ({ available, lockId, locked }: Balances) => { - const availableBalance = new BN(available ?? 0) - const lockedBalance = new BN(locked ?? 0) - - const deriveBalances = { - availableBalance: createType('Balance', availableBalance), - lockedBalance: createType('Balance', lockedBalance), - accountId: createType('AccountId', 'j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf'), - accountNonce: createType('Index', 1), - freeBalance: createType('Balance', availableBalance.add(lockedBalance)), - frozenFee: new BN(0), - frozenMisc: new BN(0), - isVesting: false, - lockedBreakdown: lockedBalance.eq(BN_ZERO) ? [] : [createBalanceLock(locked!, lockId ?? 'Bound Staking Account')], - reservedBalance: new BN(0), - vestedBalance: new BN(0), - vestedClaimable: new BN(0), - vestingEndBlock: createType('BlockNumber', 1234), - vestingLocked: new BN(0), - vestingPerBlock: new BN(0), - vestingTotal: new BN(0), - votingBalance: new BN(0), - vesting: [], - } as unknown as DeriveBalancesAll - +export const stubBalances = ({ available, lockId, locked }: Parameters[0]) => { + const deriveBalances = asDerivedBalances({ available, lockId, locked }) const balance = toBalances(deriveBalances) mockedBalances.mockReturnValue(balance) diff --git a/packages/ui/test/accounts/model/isValidAddress.test.ts b/packages/ui/test/accounts/model/isValidAddress.test.ts index c2e26df5b0..0446a789b6 100644 --- a/packages/ui/test/accounts/model/isValidAddress.test.ts +++ b/packages/ui/test/accounts/model/isValidAddress.test.ts @@ -1,28 +1,24 @@ -import { createTestKeyring } from '@polkadot/keyring/testing' - import { isValidAddress } from '@/accounts/model/isValidAddress' import { bobStash } from '../../_mocks/keyring' describe('isValidAddress', () => { - const keyring = createTestKeyring() - it('Valid: Correct address', () => { - expect(isValidAddress(bobStash.address, keyring)).toBeTruthy() + expect(isValidAddress(bobStash.address)).toBeTruthy() }) it('Invalid: Too short', () => { const address = '5tyW46xGFne2UhjJM694Xgs5mUiveU4sbTyGBzmstUspZC9' - expect(isValidAddress(address, keyring)).toBeFalsy() + expect(isValidAddress(address)).toBeFalsy() }) it('Invalid: Too long', () => { const address = '5tyW46x1lGFne2UhjJM694Xgs5mUiveU4sbTyGBzmstUspZC9' - expect(isValidAddress(address, keyring)).toBeFalsy() + expect(isValidAddress(address)).toBeFalsy() }) it('Invalid: Illegal character', () => { const address = '5Hp!9w8EBLe5XCrbczpwq5TSXvedjrBGCwqxK1iQ7qUsSWFc' - expect(isValidAddress(address, keyring)).toBeFalsy() + expect(isValidAddress(address)).toBeFalsy() }) }) diff --git a/yarn.lock b/yarn.lock index dde377a8b9..a413a36caa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4997,9 +4997,9 @@ __metadata: languageName: unknown linkType: soft -"@joystream/types@npm:4.5.0": - version: 4.5.0 - resolution: "@joystream/types@npm:4.5.0" +"@joystream/types@npm:4.6.0": + version: 4.6.0 + resolution: "@joystream/types@npm:4.6.0" dependencies: "@polkadot/api": 10.7.1 "@polkadot/keyring": ^12.6.2 @@ -5008,7 +5008,7 @@ __metadata: "@types/vfile": ^4.0.0 lodash: ^4.17.15 moment: ^2.24.0 - checksum: a77eea2bd5ed6ed59a81e6db2d42f597182541efd0a40620831925dbffe390cefe2cb0b4f509c22d96e3a1bf1dc5294a1cfd6fae5044a37fd325c13ff5664a17 + checksum: 862d7387c3af55fefdb5a33c3669e6696cf4d9c66a5ab3e18004eae0e618e14c3eb7185890c443cfc6504172411adb98355d6f83d19dc4782b41037e02b400a7 languageName: node linkType: hard