diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ed9e33a..247d68f5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -69,7 +69,8 @@ "generate:type-from-defs": "dlx ts-node --skip-project node_modules/.bin/polkadot-types-from-defs --endpoint ./node-metadata.json --package src/interfaces --input ./src/interfaces", "generate:types-from-chain": "dlx ts-node --skip-project node_modules/.bin/polkadot-types-from-chain --endpoint wss://rpc.ibp.network/polkadot --output ./src/interfaces", "test": "cypress open", - "test:ci": "cypress run --browser chrome --headless" + "test:ci": "cypress run --browser chrome --headless", + "postinstall": "yarn papi" }, "browserslist": { "production": [ diff --git a/packages/ui/src/hooks/useCheckBalance.tsx b/packages/ui/src/hooks/useCheckBalance.tsx index d1544e0b..f3ee4156 100644 --- a/packages/ui/src/hooks/useCheckBalance.tsx +++ b/packages/ui/src/hooks/useCheckBalance.tsx @@ -1,9 +1,8 @@ -import BN from 'bn.js' import { useMemo } from 'react' import { useGetBalance } from './useGetBalance' export interface Props { - min?: BN + min?: bigint address?: string } @@ -12,7 +11,7 @@ export const useCheckBalance = ({ min, address }: Props) => { const hasEnoughFreeBalance = useMemo(() => { if (!address || !min || !balance) return false - return balance.gt(min) + return balance > min }, [address, min, balance]) return { hasEnoughFreeBalance } diff --git a/packages/ui/src/hooks/useGetBalance.tsx b/packages/ui/src/hooks/useGetBalance.tsx index e7a831c4..8d565440 100644 --- a/packages/ui/src/hooks/useGetBalance.tsx +++ b/packages/ui/src/hooks/useGetBalance.tsx @@ -1,8 +1,6 @@ import { useEffect, useState } from 'react' import { useApi } from '../contexts/ApiContext' import { formatBnBalance } from '../utils/formatBnBalance' -import BN from 'bn.js' -import { FrameSystemAccountInfo } from '@polkadot/types/lookup' interface useGetBalanceProps { address?: string @@ -11,17 +9,15 @@ interface useGetBalanceProps { export const useGetBalance = ({ address, numberAfterComma = 4 }: useGetBalanceProps) => { const { api, chainInfo } = useApi() - const [balance, setBalance] = useState(null) + const [balance, setBalance] = useState(null) const [balanceFormatted, setFormattedBalance] = useState(null) useEffect(() => { if (!api || !address) return - let unsubscribe: () => void - - api.query.system - .account(address, ({ data: { free, frozen } }: FrameSystemAccountInfo) => { - const transferable = free.sub(frozen) + const unsub = api.query.System.Account.watchValue(address).subscribe( + ({ data: { free, frozen } }) => { + const transferable = free - frozen setBalance(transferable) setFormattedBalance( @@ -30,13 +26,10 @@ export const useGetBalance = ({ address, numberAfterComma = 4 }: useGetBalancePr tokenSymbol: chainInfo?.tokenSymbol }) ) - }) - .then((unsub) => { - unsubscribe = unsub as unknown as () => void - }) - .catch(console.error) + } + ) - return () => unsubscribe && unsubscribe() + return () => unsub && unsub.unsubscribe() }, [address, api, chainInfo?.tokenDecimals, chainInfo?.tokenSymbol, numberAfterComma]) return { balance, balanceFormatted } diff --git a/packages/ui/src/hooks/useMultisigProposalNeededFunds.tsx b/packages/ui/src/hooks/useMultisigProposalNeededFunds.tsx index 610ad006..50bbc623 100644 --- a/packages/ui/src/hooks/useMultisigProposalNeededFunds.tsx +++ b/packages/ui/src/hooks/useMultisigProposalNeededFunds.tsx @@ -1,19 +1,31 @@ import { useEffect, useState } from 'react' import { useApi } from '../contexts/ApiContext' -import BN from 'bn.js' -import { SubmittableExtrinsic } from '@polkadot/api/types' -import { ISubmittableResult } from '@polkadot/types/types' +import { Transaction } from 'polkadot-api' interface Props { threshold?: number | null signatories?: string[] - call?: SubmittableExtrinsic<'promise', ISubmittableResult> + call?: Transaction } export const useMultisigProposalNeededFunds = ({ threshold, signatories, call }: Props) => { const { api, chainInfo } = useApi() - const [min, setMin] = useState(new BN(0)) - const [reserved, setReserved] = useState(new BN(0)) + const [min, setMin] = useState(0n) + const [reserved, setReserved] = useState(0n) + const [multisigDepositFactor, setMultisigDepositFactor] = useState(undefined) + const [multisigDepositBase, setMultisigDepositBase] = useState(undefined) + + useEffect(() => { + if (!api) return + + api.constants.Multisig.DepositBase().then(setMultisigDepositBase).catch(console.error) + }, [api]) + + useEffect(() => { + if (!api) return + + api.constants.Multisig.DepositFactor().then(setMultisigDepositFactor).catch(console.error) + }, [api]) useEffect(() => { if (!api || !signatories || signatories.length < 2) return @@ -24,31 +36,17 @@ export const useMultisigProposalNeededFunds = ({ threshold, signatories, call }: if (!call) return - if (!api.consts.multisig.depositFactor || !api.consts.multisig.depositBase) return - - try { - const genericCall = api.createType('Call', call) - - // get the fees for this call - api - .tx(genericCall) - .paymentInfo('5CXQZrh1MSgnGGCdJu3tqvRfCv7t5iQXGGV9UKotrbfhkavs') - .then((info) => { - // add the funds reserved for a multisig call - const reservedTemp = (api.consts.multisig.depositFactor as unknown as BN) - .muln(threshold) - .add(api.consts.multisig.depositBase as unknown as BN) - - // console.log('reservedTemp', formatBnBalance(reservedTemp, chainInfo.tokenDecimals, { tokenSymbol: chainInfo?.tokenSymbol, numberAfterComma: 3 })) - setMin(reservedTemp.add(info.partialFee)) - setReserved(reservedTemp) - }) - .catch(console.error) - } catch (e) { - console.error('Error in useMultisigProposalNeededFunds') - console.error(e) - } - }, [api, call, chainInfo, signatories, threshold]) + if (!multisigDepositFactor || !multisigDepositBase) return + + call + .getEstimatedFees('5CXQZrh1MSgnGGCdJu3tqvRfCv7t5iQXGGV9UKotrbfhkavs') + .then((info) => { + const reservedTemp = multisigDepositFactor * BigInt(threshold) + multisigDepositBase + setMin(reservedTemp + info) + setReserved(reservedTemp) + }) + .catch(console.error) + }, [api, call, chainInfo, multisigDepositBase, multisigDepositFactor, signatories, threshold]) return { multisigProposalNeededFunds: min, reserved } } diff --git a/packages/ui/src/hooks/usePureProxyCreationNeededFunds.tsx b/packages/ui/src/hooks/usePureProxyCreationNeededFunds.tsx index b55038e6..8bb51c02 100644 --- a/packages/ui/src/hooks/usePureProxyCreationNeededFunds.tsx +++ b/packages/ui/src/hooks/usePureProxyCreationNeededFunds.tsx @@ -1,37 +1,53 @@ import { useEffect, useState } from 'react' import { useApi } from '../contexts/ApiContext' -import BN from 'bn.js' +import { TypedApi } from 'polkadot-api' +import { dot } from '@polkadot-api/descriptors' export const usePureProxyCreationNeededFunds = () => { const { api, chainInfo } = useApi() - const [min, setMin] = useState(new BN(0)) - const [reserved, setReserved] = useState(new BN(0)) + const [min, setMin] = useState(0n) + const [reserved, setReserved] = useState(0n) + const [depositFactor, setDepositFactor] = useState(undefined) + const [depositBase, setDepositBase] = useState(undefined) + const [existentialDeposit, setExistentialDeposit] = useState(undefined) useEffect(() => { - if (!api) return + if (!(api as TypedApi).constants?.Proxy?.ProxyDepositFactor) return + ;(api as TypedApi).constants.Proxy.ProxyDepositFactor() + .then(setDepositFactor) + .catch(console.error) + }, [api]) - if (!chainInfo?.tokenDecimals) return + useEffect(() => { + if (!(api as TypedApi).constants?.Proxy?.ProxyDepositBase) return + ;(api as TypedApi).constants.Proxy.ProxyDepositBase() + .then(setDepositBase) + .catch(console.error) + }, [api]) + + useEffect(() => { + if (!(api as TypedApi).constants?.Balances.ExistentialDeposit) return + ;(api as TypedApi).constants.Balances.ExistentialDeposit() + .then(setExistentialDeposit) + .catch(console.error) + }, [api]) + + useEffect(() => { + if (!api || !existentialDeposit || !depositBase || !depositFactor) return - if ( - !api.consts?.proxy?.proxyDepositFactor || - !api.consts?.proxy?.proxyDepositBase || - !api.consts?.balances?.existentialDeposit - ) - return + // if (!chainInfo?.tokenDecimals) return - const reserved = (api.consts?.proxy?.proxyDepositFactor as unknown as BN) - // we only create one proxy here - .muln(1) - .iadd(api.consts?.proxy?.proxyDepositBase as unknown as BN) + // we only create one proxy here + const reserved = depositFactor * 1n + depositBase - // the signer should survive and have at lease the existential deposit + // the signer should survive and have at least the existential deposit // play safe and add the existential deposit twice which should suffice - const survive = (api.consts.balances.existentialDeposit as unknown as BN).muln(2) + const survive = existentialDeposit * 2n setReserved(reserved) - setMin(reserved.add(survive)) + setMin(reserved + survive) // console.log('reserved Pure Creation', formatBnBalance(reserved.add(survive), chainInfo.tokenDecimals, { tokenSymbol: chainInfo?.tokenSymbol, numberAfterComma: 3 })) - }, [api, chainInfo]) + }, [api, chainInfo, depositBase, depositFactor, existentialDeposit]) return { pureProxyCreationNeededFunds: min, reserved } } diff --git a/packages/ui/src/pages/Creation/Summary.tsx b/packages/ui/src/pages/Creation/Summary.tsx index 1b53439a..a7c1290f 100644 --- a/packages/ui/src/pages/Creation/Summary.tsx +++ b/packages/ui/src/pages/Creation/Summary.tsx @@ -7,7 +7,6 @@ import { MultiProxy } from '../../contexts/MultiProxyContext' import { useAccounts } from '../../contexts/AccountsContext' import { getIntersection } from '../../utils' import { AccountBadge } from '../../types' -import BN from 'bn.js' import { formatBnBalance } from '../../utils/formatBnBalance' import { useApi } from '../../contexts/ApiContext' import { getErrorMessageReservedFunds } from '../../utils/getErrorMessageReservedFunds' @@ -19,8 +18,8 @@ interface Props { name?: string proxyAddress?: string isCreationSummary?: boolean - balanceMin?: BN - reservedBalance: BN + balanceMin?: bigint + reservedBalance: bigint isBalanceError?: boolean selectedMultisig?: MultiProxy['multisigs'][0] // this is only relevant for swaps withProxy?: boolean @@ -52,16 +51,18 @@ const Summary = ({ } const requiredBalanceString = - balanceMin && - formatBnBalance(balanceMin, chainInfo?.tokenDecimals, { - tokenSymbol: chainInfo?.tokenSymbol - }) - - const reservedString = reservedBalance.isZero() - ? '' - : formatBnBalance(reservedBalance, chainInfo?.tokenDecimals, { + (balanceMin !== undefined && + formatBnBalance(balanceMin, chainInfo?.tokenDecimals, { tokenSymbol: chainInfo?.tokenSymbol - }) + })) || + '' + + const reservedString = + reservedBalance === 0n + ? '' + : formatBnBalance(reservedBalance, chainInfo?.tokenDecimals, { + tokenSymbol: chainInfo?.tokenSymbol + }) const errorWithReservedFunds = getErrorMessageReservedFunds( 'selected signer', diff --git a/packages/ui/src/pages/Creation/index.tsx b/packages/ui/src/pages/Creation/index.tsx index b55f7b05..9d651c19 100644 --- a/packages/ui/src/pages/Creation/index.tsx +++ b/packages/ui/src/pages/Creation/index.tsx @@ -10,18 +10,18 @@ import NameSelection from './NameSelection' import Summary from './Summary' import { useSigningCallback } from '../../hooks/useSigningCallback' import { createSearchParams, useNavigate, useSearchParams } from 'react-router-dom' -import { useToasts } from '../../contexts/ToastContext' import { useAccountNames } from '../../contexts/AccountNamesContext' import { useCheckBalance } from '../../hooks/useCheckBalance' import { useMultisigProposalNeededFunds } from '../../hooks/useMultisigProposalNeededFunds' import { usePureProxyCreationNeededFunds } from '../../hooks/usePureProxyCreationNeededFunds' -import { useGetSubscanLinks } from '../../hooks/useSubscanLink' import WithProxySelection from './WithProxySelection' import { useGetSortAddress } from '../../hooks/useGetSortAddress' import { useGetMultisigAddress } from '../../contexts/useGetMultisigAddress' import { isEthereumAddress } from '@polkadot/util-crypto' import { getAsMultiTx } from '../../utils/getAsMultiTx' import { useMultiProxy } from '../../contexts/MultiProxyContext' +import { Binary, Transaction, TypedApi } from 'polkadot-api' +import { dot, MultiAddress, ProxyType } from '@polkadot-api/descriptors' interface Props { className?: string @@ -29,7 +29,6 @@ interface Props { const steps = ['Signatories', 'Threshold & Name', 'Review'] const MultisigCreation = ({ className }: Props) => { - const { getSubscanExtrinsicLink } = useGetSubscanLinks() const [signatories, setSignatories] = useState([]) const [currentStep, setCurrentStep] = useState(0) const isLastStep = useMemo(() => currentStep === steps.length - 1, [currentStep]) @@ -40,6 +39,7 @@ const MultisigCreation = ({ className }: Props) => { const [searchParams] = useSearchParams({ creationInProgress: 'false' }) const signCallBack = useSigningCallback({ onSuccess: () => { + setRefetchMultisigTimeoutMinutes(1) navigate({ pathname: '/', search: createSearchParams({ @@ -47,11 +47,11 @@ const MultisigCreation = ({ className }: Props) => { creationInProgress: 'true' }).toString() }) - } + }, + onError: () => setIsSubmitted(false) }) const { setRefetchMultisigTimeoutMinutes } = useMultiProxy() const { getSortAddress } = useGetSortAddress() - const { addToast } = useToasts() const [name, setName] = useState('') const { addName, getNamesWithExtension } = useAccountNames() const [isSubmitted, setIsSubmitted] = useState(false) @@ -63,13 +63,16 @@ const MultisigCreation = ({ className }: Props) => { const { pureProxyCreationNeededFunds, reserved: proxyReserved } = usePureProxyCreationNeededFunds() const supportsProxy = useMemo(() => { - const hasProxyPallet = !!api && !!api.tx.proxy + const hasProxyPallet = !!api && !!(api as TypedApi).tx.Proxy // Moonbeam and moonriver have the pallet, but it's deactivated return hasProxyPallet && !chainInfo?.isEthereum }, [api, chainInfo]) const multiAddress = useGetMultisigAddress(signatories, threshold) const [withProxy, setWithProxy] = useState(false) - const remarkCall = useMemo(() => { + const [remarkCall, setRemarkCall] = useState | undefined>() + const [batchCall, setBatchCall] = useState | undefined>() + + useEffect(() => { if (withProxy) { // this call is only useful if the user does not want a proxy. return @@ -102,14 +105,19 @@ const MultisigCreation = ({ className }: Props) => { const otherSignatories = getSortAddress( signatories.filter((sig) => sig !== selectedAccount.address) ) - const remarkTx = api.tx.system.remark(`Multix creation ${multiAddress}`) - return getAsMultiTx({ api, threshold, otherSignatories, tx: remarkTx }) + const remarkTx = api.tx.System.remark({ + remark: Binary.fromText(`Multix creation ${multiAddress}`) + }) + getAsMultiTx({ api, threshold, otherSignatories, tx: remarkTx }) + .then(setRemarkCall) + .catch(console.error) }, [api, getSortAddress, multiAddress, selectedAccount, signatories, threshold, withProxy]) const originalName = useMemo( () => multiAddress && getNamesWithExtension(multiAddress), [getNamesWithExtension, multiAddress] ) - const batchCall = useMemo(() => { + + useEffect(() => { if (!withProxy) { // this batchCall is only useful if the user wants a proxy. return @@ -141,21 +149,29 @@ const MultisigCreation = ({ className }: Props) => { const otherSignatories = getSortAddress( signatories.filter((sig) => sig !== selectedAccount.address) ) - const proxyTx = api.tx.proxy.createPure('Any', 0, 0) - const multiSigProxyCall = getAsMultiTx({ api, threshold, otherSignatories, tx: proxyTx }) - - // Some funds are needed on the multisig for the pure proxy creation - const transferTx = api.tx.balances.transferKeepAlive( - multiAddress, - pureProxyCreationNeededFunds.toString() - ) + const proxyTx = (api as TypedApi).tx.Proxy.create_pure({ + proxy_type: ProxyType.Any(), + delay: 0, + index: 0 + }) + getAsMultiTx({ api, threshold, otherSignatories, tx: proxyTx }).then((multiSigProxyCall) => { + // Some funds are needed on the multisig for the pure proxy creation + const transferTx = (api as TypedApi).tx.Balances.transfer_keep_alive({ + dest: MultiAddress.Id(multiAddress), + value: pureProxyCreationNeededFunds + }) - if (!multiSigProxyCall) { - console.error('multiSigProxyCall is undefined in Creation index.tsx') - return - } + if (!multiSigProxyCall) { + console.error('multiSigProxyCall is undefined in Creation index.tsx') + return + } - return api.tx.utility.batchAll([transferTx, multiSigProxyCall]) + setBatchCall( + api.tx.Utility.batch_all({ + calls: [transferTx.decodedCall, multiSigProxyCall.decodedCall] + }) + ) + }) }, [ api, getSortAddress, @@ -180,7 +196,7 @@ const MultisigCreation = ({ className }: Props) => { const neededBalance = useMemo( () => withProxy - ? pureProxyCreationNeededFunds.add(multisigProposalNeededFunds) + ? pureProxyCreationNeededFunds + multisigProposalNeededFunds : multisigProposalNeededFunds, [multisigProposalNeededFunds, pureProxyCreationNeededFunds, withProxy] ) @@ -249,82 +265,28 @@ const MultisigCreation = ({ className }: Props) => { return } - if (!selectedAccount) { - console.error('no selected address') + if (!selectedSigner) { + console.error('no selected signer') return } multiAddress && addName(name, multiAddress) setIsSubmitted(true) - remarkCall - .signAndSend( - selectedAccount.address, - { signer: selectedSigner, withSignedTransaction: true }, - signCallBack - ) - .then(() => { - setRefetchMultisigTimeoutMinutes(1) - }) - .catch((error: Error) => { - setIsSubmitted(false) - - addToast({ - title: error.message, - type: 'error', - link: getSubscanExtrinsicLink(remarkCall.hash.toHex()) - }) - }) - }, [ - addName, - addToast, - getSubscanExtrinsicLink, - multiAddress, - name, - remarkCall, - selectedAccount, - selectedSigner, - setRefetchMultisigTimeoutMinutes, - signCallBack - ]) + remarkCall.signSubmitAndWatch(selectedSigner, { at: 'best' }).subscribe(signCallBack) + }, [addName, multiAddress, name, remarkCall, selectedSigner, signCallBack]) const handleCreateWithPure = useCallback(async () => { - if (!selectedAccount || !batchCall) { - console.error('no selected address') + if (!selectedSigner || !batchCall) { + console.error('no selected signer') return } multiAddress && addName(name, multiAddress) setIsSubmitted(true) - batchCall - .signAndSend( - selectedAccount.address, - { signer: selectedSigner, withSignedTransaction: true }, - signCallBack - ) - .then(() => setRefetchMultisigTimeoutMinutes(1)) - .catch((error: Error) => { - setIsSubmitted(false) - - addToast({ - title: error.message, - type: 'error', - link: getSubscanExtrinsicLink(batchCall.hash.toHex()) - }) - }) - }, [ - addName, - addToast, - batchCall, - getSubscanExtrinsicLink, - multiAddress, - name, - selectedAccount, - selectedSigner, - setRefetchMultisigTimeoutMinutes, - signCallBack - ]) + batchCall.signSubmitAndWatch(selectedSigner, { at: 'best' }).subscribe(signCallBack) + }, [addName, batchCall, multiAddress, name, selectedSigner, signCallBack]) const goNext = useCallback(() => { window.scrollTo(0, 0) @@ -450,7 +412,7 @@ const MultisigCreation = ({ className }: Props) => { threshold={threshold} name={name} isBalanceError={!hasSignerEnoughFunds} - reservedBalance={withProxy ? multisigReserved.add(proxyReserved) : multisigReserved} + reservedBalance={withProxy ? multisigReserved + proxyReserved : multisigReserved} balanceMin={neededBalance} withProxy={withProxy} isSubmittingExtrinsic={isSubmitted} diff --git a/packages/ui/src/utils/formatBnBalance.ts b/packages/ui/src/utils/formatBnBalance.ts index a8227a19..f042b9f6 100644 --- a/packages/ui/src/utils/formatBnBalance.ts +++ b/packages/ui/src/utils/formatBnBalance.ts @@ -1,5 +1,3 @@ -import BN from 'bn.js' - interface Options { numberAfterComma?: number withThousandDelimiter?: boolean @@ -16,7 +14,7 @@ function countLeadingZeros(numberString: string): number { } export const formatBnBalance = ( - value: BN | string, + value: bigint | string, tokenDecimals = 0, { numberAfterComma = 4, withThousandDelimiter = true, tokenSymbol }: Options ): string => { diff --git a/packages/ui/src/utils/getAsMultiTx.ts b/packages/ui/src/utils/getAsMultiTx.ts index ccb5bc08..66cc64bf 100644 --- a/packages/ui/src/utils/getAsMultiTx.ts +++ b/packages/ui/src/utils/getAsMultiTx.ts @@ -6,6 +6,7 @@ interface Params { api: ApiType threshold: number otherSignatories: string[] + tx?: Transaction callData?: HexString weight?: { ref_time: bigint; proof_size: bigint } when?: MultisigStorageInfo['when'] @@ -19,19 +20,27 @@ export const getAsMultiTx = async ({ threshold, otherSignatories, callData, + tx, weight, when }: Params): Promise | undefined> => { - if (!callData) return + // we can pass either the tx, or the callData + if (!callData && !tx) return - const tx = await api.txFromCallData(Binary.fromHex(callData)) + let txToSend: Transaction | undefined = tx + + if (!txToSend && callData) { + txToSend = await api.txFromCallData(Binary.fromHex(callData)) + } + + if (!txToSend) return return api.tx.Multisig.as_multi({ threshold, other_signatories: otherSignatories, maybe_timepoint: when, max_weight: weight || { proof_size: 0n, ref_time: 0n }, - call: tx.decodedCall + call: txToSend.decodedCall }) // return api.tx.multisig.asMulti.meta.args.length === LEGACY_ASMULTI_PARAM_LENGTH // ? api.tx.multisig.asMulti(