Allow xcm asset teleport (#5413)

* Allow xcm asset teleport

* Should be operational...

* lock cron

* Teleport from parachain to relay

* Only enable on relay

* Disable para teleport
This commit is contained in:
Jaco 2021-06-02 16:11:11 +03:00 committed by GitHub
parent f35c0de405
commit 7148e7e6b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 314 additions and 39 deletions

View File

@ -2,7 +2,7 @@ name: 'Lock Threads'
on:
schedule:
- cron: '4 4 * * *'
- cron: '9 9 * * *'
jobs:
lock:

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { LinkOption } from './types';
export const CUSTOM_ENDPOINT_KEY = 'polkadot-app-custom-endpoints';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { LinkOption } from './types';
import { createCustom, createDev, createOwn } from './development';
import { createProduction } from './production';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { LinkOption } from './types';
import { expandEndpoints } from './util';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { LinkOption } from './types';
import { createKusama } from './productionRelayKusama';
import { createPolkadot } from './productionRelayPolkadot';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { LinkOption } from './types';
import { expandEndpoints } from './util';

View File

@ -33,6 +33,7 @@ export function createWestend (t: TFunction): EndpointOption {
// (2) common good, leave as second group
{
info: 'westmint',
allowTeleport: true,
paraId: 1000,
text: t('rpc.westend.shell', 'Westmint', { ns: 'apps-config' }),
providers: {

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { LinkOption } from './types';
import { createRococo } from './testingRelayRococo';
import { createWestend } from './testingRelayWestend';

View File

@ -1,7 +1,10 @@
// Copyright 2017-2021 @polkadot/apps-config authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '../settings/types';
export interface EndpointOption {
allowTeleport?: boolean;
dnslink?: string;
genesisHash?: string;
isChild?: boolean;
@ -13,3 +16,17 @@ export interface EndpointOption {
providers: Record<string, string>;
text: React.ReactNode;
}
export interface LinkOption extends Option {
allowTeleport?: boolean;
dnslink?: string;
genesisHash?: string;
genesisHashRelay?: string;
isChild?: boolean;
isDevelopment?: boolean;
isRelay?: boolean;
isSpaced?: boolean;
linked?: LinkOption[];
paraId?: number;
textBy: string;
}

View File

@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { LinkOption } from '../settings/types';
import type { EndpointOption } from './types';
import type { EndpointOption, LinkOption } from './types';
export function expandLinked (input: LinkOption[]): LinkOption[] {
return input.reduce((result: LinkOption[], entry): LinkOption[] => {
@ -22,9 +21,9 @@ export function expandLinked (input: LinkOption[]): LinkOption[] {
}, []);
}
export function expandEndpoint (t: TFunction, input: EndpointOption): LinkOption[] {
const { dnslink, genesisHash, info, isChild, isDisabled, linked, paraId, providers, text } = input;
export function expandEndpoint (t: TFunction, { allowTeleport, dnslink, genesisHash, info, isChild, isDisabled, linked, paraId, providers, text }: EndpointOption): LinkOption[] {
const base = {
allowTeleport,
genesisHash,
info,
isChild,
@ -36,6 +35,7 @@ export function expandEndpoint (t: TFunction, input: EndpointOption): LinkOption
const result = Object.entries(providers).map(([host, value], index): LinkOption => ({
...base,
dnslink: index === 0 ? dnslink : undefined,
isRelay: false,
textBy: t('rpc.hosted.by', 'hosted by {{host}}', { ns: 'apps-config', replace: { host } }),
value
}));
@ -45,7 +45,15 @@ export function expandEndpoint (t: TFunction, input: EndpointOption): LinkOption
const options: LinkOption[] = [];
linked.forEach((o) => options.push(...expandEndpoint(t, o)));
last.isRelay = true;
last.linked = options;
// if one of the children allows teleport, add it to the root as well
const allowTeleport = options.some(({ allowTeleport }) => allowTeleport);
result.forEach((r): void => {
r.allowTeleport = allowTeleport;
});
}
return expandLinked(result);

View File

@ -8,15 +8,3 @@ export interface Option {
text: React.ReactNode;
value: string | number;
}
export interface LinkOption extends Option {
dnslink?: string;
genesisHash?: string;
genesisHashRelay?: string;
isChild?: boolean;
isDevelopment?: boolean;
isSpaced?: boolean;
linked?: LinkOption[];
paraId?: number;
textBy: string;
}

View File

@ -28,6 +28,7 @@ import staking from './staking';
import storage from './storage';
import sudo from './sudo';
import techcomm from './techcomm';
import teleport from './teleport';
import transfer from './transfer';
import treasury from './treasury';
@ -39,6 +40,7 @@ export default function create (t: TFunction): Routes {
claims(t),
poll(t),
transfer(t),
teleport(t),
staking(t),
democracy(t),
council(t),

View File

@ -0,0 +1,30 @@
// Copyright 2017-2021 @polkadot/apps-routing authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { Route } from './types';
import Modal from '@polkadot/app-parachains/Teleport';
export default function create (t: TFunction): Route {
return {
Component: Modal,
Modal,
display: {
isHidden: false,
needsAccounts: true,
needsApi: [
[
'tx.xcm.teleportAssets',
'tx.xcmPallet.teleportAssets',
'tx.polkadotXcm.teleportAssets'
]
],
needsTeleport: true
},
group: 'accounts',
icon: 'share-square',
name: 'teleport',
text: t('nav.teleport', 'Teleport', { ns: 'apps-routing' })
};
}

View File

@ -11,14 +11,16 @@ export interface RouteProps extends AppProps, BareProps {
}
export interface Route {
Component: React.ComponentType<RouteProps>;
Modal?: React.ComponentType<any>;
// FIXME This is weird, we really expect the memo to be there...
Component: React.ComponentType<RouteProps> | React.MemoExoticComponent<any>;
Modal?: React.ComponentType<any> | React.MemoExoticComponent<any>;
display: {
isHidden?: boolean;
isModal?: boolean;
needsAccounts?: boolean;
needsApi?: (string | string[])[];
needsSudo?: boolean;
needsTeleport?: boolean;
};
group: RouteGroup;
icon: IconName;

View File

@ -1,7 +1,7 @@
// Copyright 2017-2021 @polkadot/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@polkadot/apps-config/settings/types';
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { Group } from './types';
// ok, this seems to be an eslint bug, this _is_ a package import

View File

@ -12,7 +12,7 @@ import { useLocation } from 'react-router-dom';
import styled from 'styled-components';
import createRoutes from '@polkadot/apps-routing';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
import { useAccounts, useApi, useCall, useTeleport } from '@polkadot/react-hooks';
import { findMissingApis } from '../endpoint';
import { useTranslation } from '../translate';
@ -42,7 +42,7 @@ function createExternals (t: TFunction): ItemRoute[] {
];
}
function checkVisible ({ api, isApiConnected, isApiReady }: ApiProps, hasAccounts: boolean, hasSudo: boolean, { isHidden, needsAccounts, needsApi, needsSudo }: Route['display']): boolean {
function checkVisible ({ api, isApiConnected, isApiReady }: ApiProps, allowTeleport: boolean, hasAccounts: boolean, hasSudo: boolean, { isHidden, needsAccounts, needsApi, needsSudo, needsTeleport }: Route['display']): boolean {
if (isHidden) {
return false;
} else if (needsAccounts && !hasAccounts) {
@ -53,12 +53,14 @@ function checkVisible ({ api, isApiConnected, isApiReady }: ApiProps, hasAccount
return false;
} else if (needsSudo && !hasSudo) {
return false;
} else if (needsTeleport && !allowTeleport) {
return false;
}
return findMissingApis(api, needsApi).length === 0;
}
function extractGroups (routing: Routes, groupNames: Record<string, string>, apiProps: ApiProps, hasAccounts: boolean, hasSudo: boolean): Group[] {
function extractGroups (routing: Routes, groupNames: Record<string, string>, apiProps: ApiProps, allowTeleport: boolean, hasAccounts: boolean, hasSudo: boolean): Group[] {
return Object
.values(
routing.reduce((all: Groups, route): Groups => {
@ -77,7 +79,7 @@ function extractGroups (routing: Routes, groupNames: Record<string, string>, api
.map(({ name, routes }): Group => ({
name,
routes: routes.filter(({ display }) =>
checkVisible(apiProps, hasAccounts, hasSudo, display)
checkVisible(apiProps, allowTeleport, hasAccounts, hasSudo, display)
)
}))
.filter(({ routes }) => routes.length);
@ -87,6 +89,7 @@ function Menu ({ className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts, hasAccounts } = useAccounts();
const apiProps = useApi();
const { allowTeleport } = useTeleport();
const sudoKey = useCall<AccountId>(apiProps.isApiReady && apiProps.api.query.sudo?.key);
const location = useLocation();
@ -107,8 +110,8 @@ function Menu ({ className = '' }: Props): React.ReactElement<Props> {
);
const visibleGroups = useMemo(
() => extractGroups(routeRef.current, groupRef.current, apiProps, hasAccounts, hasSudo),
[apiProps, hasAccounts, hasSudo]
() => extractGroups(routeRef.current, groupRef.current, apiProps, allowTeleport, hasAccounts, hasSudo),
[allowTeleport, apiProps, hasAccounts, hasSudo]
);
const activeRoute = useMemo(

View File

@ -0,0 +1,124 @@
// Copyright 2017-2021 @polkadot/app-parachains authors & contributors
// SPDX-License-Identifier: Apache-2.0
import BN from 'bn.js';
import React, { useMemo, useState } from 'react';
import { Dropdown, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
import { useApi, useTeleport } from '@polkadot/react-hooks';
import { Available } from '@polkadot/react-query';
import { BN_ZERO } from '@polkadot/util';
import { useTranslation } from './translate';
interface Props {
onClose: () => void;
}
const INVALID_PARAID = Number.MAX_SAFE_INTEGER;
const DEFAULT_WEIGHT = 1_000_000_000;
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);
// FIXME We need to actually calculate the destination weight
const [weight] = useState(DEFAULT_WEIGHT);
const { allowTeleport, destinations, isParachain } = useTeleport();
const chainOpts = useMemo(
() => destinations.map(({ paraId, text }, index) => ({ text, value: paraId || index })),
[destinations]
);
const params = useMemo(
() => isParachain
? [
{ X1: 'Parent' },
{ X1: { AccountId32: { id: recipientId, network: 'Any' } } },
[{ ConcreteFungible: { amount, id: { X1: 'Parent' } } }],
weight
]
: [
{ X1: { ParaChain: recipientParaId } },
{ X1: { AccountId32: { id: recipientId, network: 'Any' } } },
[{ ConcreteFungible: { amount, id: 'Null' } }],
weight
],
[amount, isParachain, recipientId, recipientParaId, weight]
);
console.log('Teleport', allowTeleport, chainOpts);
if (!allowTeleport || !chainOpts.length) {
return null;
}
return (
<Modal
header={t<string>('Teleport assets')}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t<string>('The transferred balance will be subtracted (along with fees) from the sender account.')}>
<InputAddress
label={t<string>('send from account')}
labelExtra={
<Available
label={t<string>('transferrable')}
params={senderId}
/>
}
onChange={setSenderId}
type='account'
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The destination chain for this asset teleport. The transferred value will appear on this chain.')}>
<Dropdown
defaultValue={chainOpts[0]?.value}
label={t<string>('destination chain')}
onChange={setParaId}
options={chainOpts}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The beneficiary will have access to the transferred amount when the transaction is included in a block.')}>
<InputAddress
label={t<string>('send to address')}
onChange={setRecipientId}
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.')}>
<InputBalance
autoFocus
isError={!hasAvailable}
isZeroable
label={t<string>('amount')}
onChange={setAmount}
/>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<TxButton
accountId={senderId}
icon='share-square'
isDisabled={!hasAvailable || !recipientId || !amount || (!isParachain && recipientParaId === INVALID_PARAID)}
label={t<string>('Teleport')}
onStart={onClose}
params={params}
tx={
(api.tx.xcm && api.tx.xcm.teleportAssets) ||
(api.tx.xcmPallet && api.tx.xcmPallet.teleportAssets) ||
(api.tx.polkadotXcm && api.tx.polkadotXcm.teleportAssets)
}
/>
</Modal.Actions>
</Modal>
);
}
export default React.memo(Teleport);

View File

@ -186,8 +186,8 @@ function Api ({ children, store, url }: Props): React.ReactElement<Props> | null
const [extensions, setExtensions] = useState<InjectedExtension[] | undefined>();
const value = useMemo<ApiProps>(
() => ({ ...state, api, apiError, extensions, isApiConnected, isApiInitialized, isWaitingInjected: !extensions }),
[apiError, extensions, isApiConnected, isApiInitialized, state]
() => ({ ...state, api, apiError, apiUrl: url, extensions, isApiConnected, isApiInitialized, isWaitingInjected: !extensions }),
[apiError, extensions, isApiConnected, isApiInitialized, state, url]
);
// initial initialization

View File

@ -31,6 +31,7 @@ export interface ApiState {
export interface ApiProps extends ApiState {
api: ApiPromise;
apiError: string | null;
apiUrl?: string;
extensions?: InjectedExtension[];
isApiConnected: boolean;
isApiInitialized: boolean;

View File

@ -40,6 +40,7 @@ export { useRegistrars } from './useRegistrars';
export { useSavedFlags } from './useSavedFlags';
export { useStepper } from './useStepper';
export { useSudo } from './useSudo';
export { useTeleport } from './useTeleport';
export { useToggle } from './useToggle';
export { useTreasury } from './useTreasury';
export { useTxBatch } from './useTxBatch';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { LinkOption } from '@polkadot/apps-config/settings/types';
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import { useCallback, useEffect, useRef, useState } from 'react';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { LinkOption } from '@polkadot/apps-config/settings/types';
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import { useMemo } from 'react';
@ -11,8 +11,10 @@ import { bnToBn } from '@polkadot/util';
import { useApi } from './useApi';
const endpoints = createWsEndpoints((key: string, value: string | undefined) => value || key);
function extractRelayEndpoints (genesisHash: string): LinkOption[] {
return createWsEndpoints((key: string, value: string | undefined) => value || key).filter(({ genesisHashRelay }) =>
return endpoints.filter(({ genesisHashRelay }) =>
genesisHash === genesisHashRelay
);
}
@ -23,7 +25,7 @@ function extractParaEndpoints (allEndpoints: LinkOption[], paraId: BN | number):
return allEndpoints.filter(({ paraId }) => paraId === numId);
}
function useRelayEndpoints (): LinkOption[] {
export function useRelayEndpoints (): LinkOption[] {
const { api } = useApi();
return useMemo(
@ -46,7 +48,10 @@ export function useIsParasLinked (ids?: (BN | number)[] | null): Record<string,
return useMemo(
() => ids
? ids.reduce((all: Record<string, boolean>, id) => ({ ...all, [id.toString()]: extractParaEndpoints(endpoints, id).length !== 0 }), {})
? ids.reduce((all: Record<string, boolean>, id) => ({
...all,
[id.toString()]: extractParaEndpoints(endpoints, id).length !== 0
}), {})
: {},
[endpoints, ids]
);

View File

@ -0,0 +1,93 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { ParaId } from '@polkadot/types/interfaces';
import { useEffect, useState } from 'react';
import { createWsEndpoints } from '@polkadot/apps-config';
import { useApi } from './useApi';
import { useCall } from './useCall';
interface Teleport {
allowTeleport: boolean;
destinations: LinkOption[];
isParachain: boolean;
paraId?: ParaId;
}
const endpoints = createWsEndpoints((k: string, v: string | undefined) => v || k).filter(({ allowTeleport }) => allowTeleport);
function extractRelayDestinations (relayGenesis: string, filter: (l: LinkOption) => boolean): LinkOption[] {
return endpoints
.filter((l) =>
(
l.genesisHashRelay === relayGenesis ||
l.genesisHash === relayGenesis
) && filter(l)
)
.reduce((result: LinkOption[], curr): LinkOption[] => {
if (!result.some(({ genesisHash, paraId }) => paraId === curr.paraId || genesisHash === curr.genesisHash)) {
result.push(curr);
}
return result;
}, [])
.sort((a, b) =>
a.isRelay === b.isRelay
? 0
: a.isRelay
? -1
: 1
);
}
export function useTeleport (): Teleport {
const { api, apiUrl, isApiReady } = useApi();
const chainParaId = useCall<ParaId>(isApiReady && api.query.parachainInfo?.parachainId);
const [state, setState] = useState<Teleport>(() => ({
allowTeleport: false,
destinations: [],
isParachain: false
}));
useEffect((): void => {
if (isApiReady) {
const relayGenesis = api.genesisHash.toHex();
const destinations = extractRelayDestinations(relayGenesis, ({ genesisHash }) => relayGenesis !== genesisHash);
setState((prev) => ({
...prev,
allowTeleport: destinations.length !== 0,
destinations
}));
}
}, [api, isApiReady]);
useEffect((): void => {
if (chainParaId) {
const endpoint = endpoints.find(({ paraId, value }) =>
chainParaId.eq(paraId) &&
value === apiUrl
);
setState((prev) => ({
...prev,
// FIXME Cannot quite get this working...
allowTeleport: false, // !!endpoint,
destinations: endpoint && endpoint.genesisHashRelay
// FIXME we probably just want to check for !chainParaId.eq(paraId) || !!genesisHash (would affect Teleport modal)
? extractRelayDestinations(endpoint.genesisHashRelay, ({ genesisHash }) => !!genesisHash)
: [],
isParachain: true,
paraId: chainParaId
}));
}
}, [apiUrl, chainParaId]);
console.log('useTeleport', JSON.stringify(state));
return state;
}