Enable Statemine <-> Kusama (w/ fees) (#5495)

* Enable Statemine <-> Kusama (w/ fees)

* Retrievals

* text
This commit is contained in:
Jaco 2021-06-08 12:08:15 +03:00 committed by GitHub
parent e4b216d305
commit 0e72723b7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 192 additions and 65 deletions

View File

@ -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")
//

View File

@ -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}

View File

@ -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;

View File

@ -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';

View 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;
}

View File

@ -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[]][] {

View File

@ -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();

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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 {

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