Enable Statemine <-> Kusama (w/ fees) (#5495)
* Enable Statemine <-> Kusama (w/ fees) * Retrievals * text
This commit is contained in:
parent
e4b216d305
commit
0e72723b7b
@ -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")
|
||||
//
|
||||
|
||||
@ -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: (
|
||||
<div
|
||||
@ -35,7 +35,7 @@ function createOption ({ info, paraId, text }: LinkOption, index: number): Optio
|
||||
<div className='ui--Dropdown-name'>{text}</div>
|
||||
</div>
|
||||
),
|
||||
value: paraId || index
|
||||
value: paraId || -1
|
||||
};
|
||||
}
|
||||
|
||||
@ -43,7 +43,6 @@ function Teleport ({ onClose }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [amount, setAmount] = useState<BN | undefined>(BN_ZERO);
|
||||
const [hasAvailable] = useState(true);
|
||||
const [recipientId, setRecipientId] = useState<string | null>(null);
|
||||
const [senderId, setSenderId] = useState<string | null>(null);
|
||||
const [recipientParaId, setParaId] = useState(INVALID_PARAID);
|
||||
@ -54,23 +53,37 @@ function Teleport ({ onClose }: Props): React.ReactElement<Props> | 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 (
|
||||
<Modal
|
||||
header={t<string>('Teleport assets')}
|
||||
@ -98,7 +111,7 @@ function Teleport ({ onClose }: Props): React.ReactElement<Props> | null {
|
||||
onChange={setParaId}
|
||||
options={chainOpts}
|
||||
/>
|
||||
{oneWay.includes(recipientParaId) && (
|
||||
{!isParaTeleport && oneWay.includes(recipientParaId) && (
|
||||
<MarkWarning content={t<string>('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.')} />
|
||||
)}
|
||||
</Modal.Columns>
|
||||
@ -110,7 +123,12 @@ function Teleport ({ onClose }: Props): React.ReactElement<Props> | null {
|
||||
type='allPlus'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t<string>('If the recipient account is new, the balance needs to be more than the existential deposit on the recipient chain.')}>
|
||||
<Modal.Columns hint={
|
||||
<>
|
||||
<p>{t<string>('If the recipient account is new, the balance needs to be more than the existential deposit on the recipient chain.')}</p>
|
||||
<p>{t<string>('The amount deposited to the recipient will be net the calculated cross-chain fee.')}</p>
|
||||
</>
|
||||
}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
isError={!hasAvailable}
|
||||
@ -118,13 +136,35 @@ function Teleport ({ onClose }: Props): React.ReactElement<Props> | null {
|
||||
label={t<string>('amount')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
{destinationApi
|
||||
? (
|
||||
<>
|
||||
<InputBalance
|
||||
defaultValue={weightFee}
|
||||
isDisabled
|
||||
label={t<string>('destination transfer fee')}
|
||||
/>
|
||||
<InputBalance
|
||||
defaultValue={destinationApi.consts.balances.existentialDeposit}
|
||||
isDisabled
|
||||
label={t<string>('destination existential deposit')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<Spinner
|
||||
label={t<string>('Retrieving destination chain fees')}
|
||||
variant='appPadded'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
accountId={senderId}
|
||||
icon='share-square'
|
||||
isDisabled={!allowTeleport || !hasAvailable || !recipientId || !amount || (!isParaTeleport && recipientParaId === INVALID_PARAID)}
|
||||
isDisabled={!allowTeleport || !hasAvailable || !recipientId || !amount || !destinationApi || (!isParaTeleport && recipientParaId === INVALID_PARAID)}
|
||||
label={t<string>('Teleport')}
|
||||
onStart={onClose}
|
||||
params={params}
|
||||
|
||||
@ -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 (
|
||||
<div className={`${className} ui--Spinner${variant === 'cover' ? ' isCover' : ''}`}>
|
||||
<div className={`${className} ui--Spinner variant-${variant}`}>
|
||||
<img
|
||||
className={variant === 'push' ? '' : 'highlight--bg highlight--border'}
|
||||
src={spinnerSrc as string}
|
||||
/>
|
||||
{!noLabel && variant === 'app' && <div className='text'>{label || t('Retrieving data')}</div>}
|
||||
{!noLabel && variant.startsWith('app') && <div className='text'>{label || t('Retrieving data')}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
55
packages/react-hooks/src/useApiUrl.ts
Normal file
55
packages/react-hooks/src/useApiUrl.ts
Normal file
@ -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<ApiPromise | null>(null);
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [state, setState] = useState<ApiPromise | null>(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;
|
||||
}
|
||||
@ -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[]][] {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<ApiPromise | null>(null);
|
||||
const mountedRef = useIsMountedRef();
|
||||
const endpoints = useParaEndpoints(paraId);
|
||||
const [state, setState] = useState<Result>({ 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<Result>(() => ({
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
37
packages/react-hooks/src/useWeightFee.ts
Normal file
37
packages/react-hooks/src/useWeightFee.ts
Normal file
@ -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]
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user