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]
+ );
+}