From 0e72723b7b09da7b12a9cfbb56f4972ceeb0c10a Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 8 Jun 2021 12:08:15 +0300 Subject: [PATCH] Enable Statemine <-> Kusama (w/ fees) (#5495) * Enable Statemine <-> Kusama (w/ fees) * Retrievals * text --- .../src/endpoints/productionRelayKusama.ts | 4 +- packages/page-parachains/src/Teleport.tsx | 62 ++++++++++++++---- packages/react-components/src/Spinner.tsx | 12 ++-- packages/react-hooks/src/index.ts | 2 + packages/react-hooks/src/useApiUrl.ts | 55 ++++++++++++++++ .../react-hooks/src/useAvailableSlashes.ts | 5 +- packages/react-hooks/src/useBlockTime.ts | 4 +- packages/react-hooks/src/useBlocksPerDays.ts | 2 +- packages/react-hooks/src/useMembers.ts | 4 +- packages/react-hooks/src/useParaApi.ts | 65 +++++++------------ packages/react-hooks/src/useTreasury.ts | 5 +- packages/react-hooks/src/useWeightFee.ts | 37 +++++++++++ 12 files changed, 192 insertions(+), 65 deletions(-) create mode 100644 packages/react-hooks/src/useApiUrl.ts create mode 100644 packages/react-hooks/src/useWeightFee.ts diff --git a/packages/apps-config/src/endpoints/productionRelayKusama.ts b/packages/apps-config/src/endpoints/productionRelayKusama.ts index 8140b5dea8..ea33c6a98c 100644 --- a/packages/apps-config/src/endpoints/productionRelayKusama.ts +++ b/packages/apps-config/src/endpoints/productionRelayKusama.ts @@ -24,7 +24,7 @@ export function createKusama (t: TFunction): EndpointOption { OnFinality: 'wss://kusama.api.onfinality.io/public-ws', 'Patract Elara': 'wss://kusama.elara.patract.io' }, - teleport: [], + teleport: [1000], linked: [ // (1) all system parachains (none available yet) // ... @@ -38,7 +38,7 @@ export function createKusama (t: TFunction): EndpointOption { OnFinality: 'wss://statemine.api.onfinality.io/public-ws', 'Patract Elara': 'wss://statemine.kusama.elara.patract.io' }, - teleport: [] + teleport: [-1] }, /// (3) parachains with id, see Rococo (info here maps to the actual "named icon") // diff --git a/packages/page-parachains/src/Teleport.tsx b/packages/page-parachains/src/Teleport.tsx index 01c917b5cc..121fae47a1 100644 --- a/packages/page-parachains/src/Teleport.tsx +++ b/packages/page-parachains/src/Teleport.tsx @@ -7,8 +7,8 @@ import type { Option } from '@polkadot/apps-config/settings/types'; import React, { useMemo, useState } from 'react'; -import { ChainImg, Dropdown, InputAddress, InputBalance, MarkWarning, Modal, TxButton } from '@polkadot/react-components'; -import { useApi, useTeleport } from '@polkadot/react-hooks'; +import { ChainImg, Dropdown, InputAddress, InputBalance, MarkWarning, Modal, Spinner, TxButton } from '@polkadot/react-components'; +import { useApi, useApiUrl, useTeleport, useWeightFee } from '@polkadot/react-hooks'; import { Available } from '@polkadot/react-query'; import { BN_ZERO } from '@polkadot/util'; @@ -18,10 +18,10 @@ interface Props { onClose: () => void; } -const DEFAULT_WEIGHT = 30_000_000; // 30_000_000 as per the code +const DEST_WEIGHT = 3 * 1_000_000_000; // 3 * BaseXcmWeight on Kusama (on Rococo and Westend this is different) const INVALID_PARAID = Number.MAX_SAFE_INTEGER; -function createOption ({ info, paraId, text }: LinkOption, index: number): Option { +function createOption ({ info, paraId, text }: LinkOption): Option { return { text: (
{text}
), - value: paraId || index + value: paraId || -1 }; } @@ -43,7 +43,6 @@ function Teleport ({ onClose }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const [amount, setAmount] = useState(BN_ZERO); - const [hasAvailable] = useState(true); const [recipientId, setRecipientId] = useState(null); const [senderId, setSenderId] = useState(null); const [recipientParaId, setParaId] = useState(INVALID_PARAID); @@ -54,23 +53,37 @@ function Teleport ({ onClose }: Props): React.ReactElement | null { [destinations] ); + const url = useMemo( + () => destinations.find(({ paraId }, index) => + recipientParaId === -1 + ? index === 0 + : recipientParaId === paraId + )?.value as string, + [destinations, recipientParaId] + ); + + const destinationApi = useApiUrl(url); + const weightFee = useWeightFee(DEST_WEIGHT, destinationApi); + const params = useMemo( () => isParaTeleport ? [ { X1: 'Parent' }, { X1: { AccountId32: { id: recipientId, network: 'Any' } } }, [{ ConcreteFungible: { amount, id: { X1: 'Parent' } } }], - DEFAULT_WEIGHT + DEST_WEIGHT ] : [ { X1: { ParaChain: recipientParaId } }, { X1: { AccountId32: { id: recipientId, network: 'Any' } } }, [{ ConcreteFungible: { amount, id: 'Null' } }], - DEFAULT_WEIGHT + DEST_WEIGHT ], [amount, isParaTeleport, recipientId, recipientParaId] ); + const hasAvailable = !!amount && amount.gte(weightFee); + return ( ('Teleport assets')} @@ -98,7 +111,7 @@ function Teleport ({ onClose }: Props): React.ReactElement | null { onChange={setParaId} options={chainOpts} /> - {oneWay.includes(recipientParaId) && ( + {!isParaTeleport && oneWay.includes(recipientParaId) && ( ('Currently this is a one-way transfer since the on-chain runtime functionality to send the funds from the destination chain back to this account not yet available.')} /> )} @@ -110,7 +123,12 @@ function Teleport ({ onClose }: Props): React.ReactElement | null { type='allPlus' /> - ('If the recipient account is new, the balance needs to be more than the existential deposit on the recipient chain.')}> + +

{t('If the recipient account is new, the balance needs to be more than the existential deposit on the recipient chain.')}

+

{t('The amount deposited to the recipient will be net the calculated cross-chain fee.')}

+ + }> | null { label={t('amount')} onChange={setAmount} /> + {destinationApi + ? ( + <> + ('destination transfer fee')} + /> + ('destination existential deposit')} + /> + + ) + : ( + ('Retrieving destination chain fees')} + variant='appPadded' + /> + ) + }
('Teleport')} onStart={onClose} params={params} diff --git a/packages/react-components/src/Spinner.tsx b/packages/react-components/src/Spinner.tsx index 84a8f2940e..2a4c4302eb 100644 --- a/packages/react-components/src/Spinner.tsx +++ b/packages/react-components/src/Spinner.tsx @@ -11,7 +11,7 @@ interface Props { className?: string; label?: React.ReactNode; noLabel?: boolean; - variant?: 'app' | 'cover' | 'push' | 'mini'; + variant?: 'app' | 'appPadded' | 'cover' | 'push' | 'mini'; } // prefetch @@ -23,12 +23,12 @@ function Spinner ({ className = '', label, noLabel, variant = 'app' }: Props): R const { t } = useTranslation(); return ( -
+
- {!noLabel && variant === 'app' &&
{label || t('Retrieving data')}
} + {!noLabel && variant.startsWith('app') &&
{label || t('Retrieving data')}
}
); } @@ -39,7 +39,11 @@ export default React.memo(styled(Spinner)` margin: 0 auto; text-align: center; - &.isCover { + &.variant-appPadded { + margin-top: 0.5rem; + } + + &.variant-cover { bottom: 0; left: 0; position: absolute; diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 8540731308..5b8d31ce60 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -6,6 +6,7 @@ export { useAccountInfo } from './useAccountInfo'; export { useAccounts } from './useAccounts'; export { useAddresses } from './useAddresses'; export { useApi } from './useApi'; +export { useApiUrl } from './useApiUrl'; export { useAvailableSlashes } from './useAvailableSlashes'; export { useBestNumber } from './useBestNumber'; export { useBlockTime } from './useBlockTime'; @@ -46,3 +47,4 @@ export { useTreasury } from './useTreasury'; export { useTxBatch } from './useTxBatch'; export { useVotingStatus } from './useVotingStatus'; export { useWeight } from './useWeight'; +export { useWeightFee } from './useWeightFee'; diff --git a/packages/react-hooks/src/useApiUrl.ts b/packages/react-hooks/src/useApiUrl.ts new file mode 100644 index 0000000000..acbe3a7e83 --- /dev/null +++ b/packages/react-hooks/src/useApiUrl.ts @@ -0,0 +1,55 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { typesBundle, typesChain } from '@polkadot/apps-config'; +import { isString } from '@polkadot/util'; + +import { useIsMountedRef } from './useIsMountedRef'; + +function disconnect (api: ApiPromise | null): void { + api && api.disconnect().catch(console.error); +} + +export function useApiUrl (url?: string | string[]): ApiPromise | null { + const apiRef = useRef(null); + const mountedRef = useIsMountedRef(); + const [state, setState] = useState(null); + + useEffect((): () => void => { + return (): void => { + disconnect(apiRef.current); + apiRef.current = null; + }; + }, []); + + const _setApi = useCallback( + (api: ApiPromise | null): void => { + disconnect(apiRef.current); + + if (mountedRef.current) { + apiRef.current = api; + setState(api); + } + }, + [mountedRef] + ); + + useEffect((): void => { + _setApi(null); + + url && (isString(url) || url.length) && + ApiPromise + .create({ + provider: new WsProvider(url), + typesBundle, + typesChain + }) + .then(_setApi) + .catch(console.error); + }, [_setApi, url]); + + return state; +} diff --git a/packages/react-hooks/src/useAvailableSlashes.ts b/packages/react-hooks/src/useAvailableSlashes.ts index 134ea4e8ae..5f9d2391fb 100644 --- a/packages/react-hooks/src/useAvailableSlashes.ts +++ b/packages/react-hooks/src/useAvailableSlashes.ts @@ -8,9 +8,12 @@ import type { EraIndex, UnappliedSlash } from '@polkadot/types/interfaces'; import BN from 'bn.js'; import { useEffect, useState } from 'react'; -import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks'; import { BN_ONE } from '@polkadot/util'; +import { useApi } from './useApi'; +import { useCall } from './useCall'; +import { useIsMountedRef } from './useIsMountedRef'; + type Unsub = () => void; export function useAvailableSlashes (): [BN, UnappliedSlash[]][] { diff --git a/packages/react-hooks/src/useBlockTime.ts b/packages/react-hooks/src/useBlockTime.ts index a1cd302c7d..ea17060780 100644 --- a/packages/react-hooks/src/useBlockTime.ts +++ b/packages/react-hooks/src/useBlockTime.ts @@ -7,16 +7,16 @@ import type { Time } from '@polkadot/util/types'; import BN from 'bn.js'; import { useMemo } from 'react'; -import { useApi } from '@polkadot/react-hooks'; import { BN_ONE, extractTime } from '@polkadot/util'; import { useTranslation } from './translate'; +import { useApi } from './useApi'; type Result = [number, string, Time]; const DEFAULT_TIME = new BN(6000); -export function useBlockTime (blocks = BN_ONE, apiOverride?: ApiPromise): Result { +export function useBlockTime (blocks = BN_ONE, apiOverride?: ApiPromise | null): Result { const { t } = useTranslation(); const { api } = useApi(); diff --git a/packages/react-hooks/src/useBlocksPerDays.ts b/packages/react-hooks/src/useBlocksPerDays.ts index 6197ebcdab..b43cfd9271 100644 --- a/packages/react-hooks/src/useBlocksPerDays.ts +++ b/packages/react-hooks/src/useBlocksPerDays.ts @@ -4,7 +4,7 @@ import BN from 'bn.js'; import { useMemo } from 'react'; -import { useApi } from '@polkadot/react-hooks'; +import { useApi } from './useApi'; const DEFAULT_TIME = new BN(6000); const A_DAY = new BN(24 * 60 * 60 * 1000); diff --git a/packages/react-hooks/src/useMembers.ts b/packages/react-hooks/src/useMembers.ts index 9709564203..df8f59b146 100644 --- a/packages/react-hooks/src/useMembers.ts +++ b/packages/react-hooks/src/useMembers.ts @@ -5,7 +5,9 @@ import type { AccountId } from '@polkadot/types/interfaces'; import { useMemo } from 'react'; -import { useAccounts, useApi, useCall } from '@polkadot/react-hooks'; +import { useAccounts } from './useAccounts'; +import { useApi } from './useApi'; +import { useCall } from './useCall'; interface Result { isMember: boolean; diff --git a/packages/react-hooks/src/useParaApi.ts b/packages/react-hooks/src/useParaApi.ts index 31be4414d6..9c7d7dce76 100644 --- a/packages/react-hooks/src/useParaApi.ts +++ b/packages/react-hooks/src/useParaApi.ts @@ -4,62 +4,45 @@ import type BN from 'bn.js'; import type { LinkOption } from '@polkadot/apps-config/endpoints/types'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; -import { ApiPromise, WsProvider } from '@polkadot/api'; -import { typesBundle, typesChain } from '@polkadot/apps-config'; +import { ApiPromise } from '@polkadot/api'; +import { useApiUrl } from './useApiUrl'; import { useIsMountedRef } from './useIsMountedRef'; import { useParaEndpoints } from './useParaEndpoints'; interface Result { api?: ApiPromise | null; endpoints: LinkOption[]; -} - -function disconnect (api: ApiPromise | null): void { - api && api.disconnect().catch(console.error); + urls: string[]; } export function useParaApi (paraId: BN | number): Result { - const apiRef = useRef(null); const mountedRef = useIsMountedRef(); const endpoints = useParaEndpoints(paraId); - const [state, setState] = useState({ api: null, endpoints }); - - useEffect((): () => void => { - return (): void => { - disconnect(apiRef.current); - apiRef.current = null; - }; - }, []); - - const _setApi = useCallback( - (api: ApiPromise | null): void => { - disconnect(apiRef.current); - - if (mountedRef.current) { - apiRef.current = api; - setState(({ endpoints }) => ({ api, endpoints })); - } - }, - [mountedRef] - ); + const [state, setState] = useState(() => ({ + api: null, + endpoints, + urls: [] + })); + const api = useApiUrl(state.urls); useEffect((): void => { - if (endpoints.length) { - ApiPromise - .create({ - provider: new WsProvider(endpoints[endpoints.length - 1].value as string), - typesBundle, - typesChain - }) - .then(_setApi) - .catch(console.error); - } else { - _setApi(null); - } - }, [_setApi, endpoints]); + mountedRef.current && setState({ + api: null, + endpoints, + urls: endpoints.map(({ value }) => value as string).reverse() + }); + }, [endpoints, mountedRef]); + + useEffect((): void => { + mountedRef.current && setState(({ endpoints, urls }) => ({ + api, + endpoints, + urls + })); + }, [api, mountedRef]); return state; } diff --git a/packages/react-hooks/src/useTreasury.ts b/packages/react-hooks/src/useTreasury.ts index 2b77b19467..62e0473b54 100644 --- a/packages/react-hooks/src/useTreasury.ts +++ b/packages/react-hooks/src/useTreasury.ts @@ -7,10 +7,11 @@ import type { Balance } from '@polkadot/types/interfaces'; import { useEffect, useState } from 'react'; import { DeriveBalancesAccount } from '@polkadot/api-derive/types'; -import { useApi } from '@polkadot/react-hooks/useApi'; -import { useCall } from '@polkadot/react-hooks/useCall'; import { BN_MILLION, BN_ZERO, stringToU8a } from '@polkadot/util'; +import { useApi } from './useApi'; +import { useCall } from './useCall'; + const TREASURY_ACCOUNT = stringToU8a('modlpy/trsry'.padEnd(32, '\0')); interface Result { diff --git a/packages/react-hooks/src/useWeightFee.ts b/packages/react-hooks/src/useWeightFee.ts new file mode 100644 index 0000000000..06cc200419 --- /dev/null +++ b/packages/react-hooks/src/useWeightFee.ts @@ -0,0 +1,37 @@ +// Copyright 2017-2021 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { WeightToFeeCoefficient } from '@polkadot/types/interfaces'; + +import BN from 'bn.js'; +import { useMemo } from 'react'; + +import { BN_BILLION, BN_ZERO, bnToBn, isUndefined } from '@polkadot/util'; + +import { useApi } from './useApi'; + +export function useWeightFee (weight: BN | number, apiOverride?: ApiPromise | null): BN { + const { api } = useApi(); + + return useMemo( + () => isUndefined(apiOverride) || apiOverride + ? ((apiOverride || api).consts.transactionPayment?.weightToFee || []).reduce((acc, { coeffFrac, coeffInteger, degree, negative }: WeightToFeeCoefficient): BN => { + const w = bnToBn(weight).pow(degree); + const frac = coeffFrac.mul(w).div(BN_BILLION); + const integer = coeffInteger.mul(w); + + if (negative.isTrue) { + acc.isub(frac); + acc.isub(integer); + } else { + acc.iadd(frac); + acc.iadd(integer); + } + + return acc; + }, new BN(0)) + : BN_ZERO, + [api, apiOverride, weight] + ); +}