From cf11225d08d28b31af3f9a377c9d1f209469bec6 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Sun, 20 Aug 2023 08:41:55 -0700 Subject: [PATCH 01/30] wip --- src/pages/identity.tsx | 10 ++------ src/pages/transfer.tsx | 38 ++++++++++++++++++++++++++++++- styles/pages/identity.module.scss | 6 +++++ styles/pages/transfer.module.scss | 0 tsconfig.json | 1 + 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 styles/pages/identity.module.scss create mode 100644 styles/pages/transfer.module.scss diff --git a/src/pages/identity.tsx b/src/pages/identity.tsx index 0168520..fe7aa4d 100644 --- a/src/pages/identity.tsx +++ b/src/pages/identity.tsx @@ -10,6 +10,7 @@ import { Typography, } from '@mui/material'; import { useState } from 'react'; +import '@styles/pages/identity.module.scss'; import { CreateIdentity, RemoveIdentity } from '@/components/Buttons'; import { AddressCard } from '@/components/Cards'; @@ -34,14 +35,7 @@ const IdentityPage = () => { return ( <> - + My Identity diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index d9b8cd2..c23eded 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -1,5 +1,41 @@ +import { Box, FormLabel, MenuItem, TextField } from '@mui/material'; +import '@styles/pages/transfer.module.scss'; + +import { useIdentity } from '@/contracts'; + const TransferPage = () => { - return
Transfer
; + const { networks } = useIdentity(); + + return ( + + List of networks + setNetworkId(Number(e.target.value))} + > + {Object.entries(networks).map(([id, network], index) => ( + + {network.name} + + ))} + + + ); }; export default TransferPage; diff --git a/styles/pages/identity.module.scss b/styles/pages/identity.module.scss new file mode 100644 index 0000000..87027bc --- /dev/null +++ b/styles/pages/identity.module.scss @@ -0,0 +1,6 @@ +.identity-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32px; +} diff --git a/styles/pages/transfer.module.scss b/styles/pages/transfer.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index e64da08..31fb0ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], + "@styles/*": ["./styles/*"], "~/*": ["./public/*"] }, "incremental": true From 09c594d2a28d42286d7196b03a8d285900e9b273 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Sun, 20 Aug 2023 13:08:58 -0700 Subject: [PATCH 02/30] wip: transfer page UI --- .env.example | 3 +- next.config.js | 1 + src/consts/index.ts | 7 ++ src/contexts/RelayApi/index.tsx | 105 +++++++++++++++++++++++++++ src/pages/_app.tsx | 29 ++++---- src/pages/identity.tsx | 4 +- src/pages/transfer.tsx | 114 ++++++++++++++++++++++-------- styles/global.scss | 4 +- styles/pages/identity.module.scss | 2 +- styles/pages/transfer.module.scss | 9 +++ 10 files changed, 231 insertions(+), 47 deletions(-) create mode 100644 src/consts/index.ts create mode 100644 src/contexts/RelayApi/index.tsx diff --git a/.env.example b/.env.example index b6eeb41..fd90eb7 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ CONTRACT_IDENTITY="AddressOfIdentityContract" -CONTRACT_ADDRESS_BOOK="AddressOfAddressBookContract" \ No newline at end of file +CONTRACT_ADDRESS_BOOK="AddressOfAddressBookContract" +RELAY_CHAIN="kusama_or_polkadot" \ No newline at end of file diff --git a/next.config.js b/next.config.js index 41d7c89..e0948aa 100644 --- a/next.config.js +++ b/next.config.js @@ -7,6 +7,7 @@ const nextConfig = { env: { CONTRACT_IDENTITY: process.env.CONTRACT_IDENTITY, CONTRACT_ADDRESS_BOOK: process.env.CONTRACT_ADDRESS_BOOK, + RELAY_CHAIN: process.env.RELAY_CHAIN, }, async redirects() { return [ diff --git a/src/consts/index.ts b/src/consts/index.ts new file mode 100644 index 0000000..2b58f71 --- /dev/null +++ b/src/consts/index.ts @@ -0,0 +1,7 @@ +type RELAY_CHAIN_OPTION = 'polkadot' | 'kusama'; +const RELAY_CHAIN_ENDPOINTS = { + polkadot: "wss://polkadot.api.onfinality.io/public-ws", + kusama: "wss://kusama.api.onfinality.io/public-ws" +}; +export const RELAY_CHAIN = (process.env.RELAY_CHAIN || 'polkadot') as RELAY_CHAIN_OPTION; +export const RELAY_CHAIN_ENDPOINT = RELAY_CHAIN_ENDPOINTS[RELAY_CHAIN]; \ No newline at end of file diff --git a/src/contexts/RelayApi/index.tsx b/src/contexts/RelayApi/index.tsx new file mode 100644 index 0000000..366765b --- /dev/null +++ b/src/contexts/RelayApi/index.tsx @@ -0,0 +1,105 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; +import jsonrpc from '@polkadot/types/interfaces/jsonrpc'; +import { DefinitionRpcExt } from '@polkadot/types/types'; +import React, { useContext, useEffect, useReducer } from 'react'; + +import { RELAY_CHAIN_ENDPOINT } from '@/consts'; + +import { useToast } from '../Toast'; + +/// +// Initial state for `useReducer` + +type State = { + socket: string; + jsonrpc: Record>; + api: any; + apiError: any; + apiState: any; +}; + +const initialState: State = { + // These are the states + socket: RELAY_CHAIN_ENDPOINT, + jsonrpc: { ...jsonrpc }, + api: null, + apiError: null, + apiState: null, +}; + +/// +// Reducer function for `useReducer` + +const reducer = (state: any, action: any) => { + switch (action.type) { + case 'CONNECT_INIT': + return { ...state, apiState: 'CONNECT_INIT' }; + case 'CONNECT': + return { ...state, api: action.payload, apiState: 'CONNECTING' }; + case 'CONNECT_SUCCESS': + return { ...state, apiState: 'READY' }; + case 'CONNECT_ERROR': + return { ...state, apiState: 'ERROR', apiError: action.payload }; + default: + throw new Error(`Unknown type: ${action.type}`); + } +}; + +/// +// Connecting to the Substrate node + +const connect = (state: any, dispatch: any) => { + const { apiState, socket, jsonrpc } = state; + // We only want this function to be performed once + if (apiState) return; + + dispatch({ type: 'CONNECT_INIT' }); + + const provider = new WsProvider(socket); + const _api = new ApiPromise({ provider, rpc: jsonrpc }); + + // Set listeners for disconnection and reconnection event. + _api.on('connected', () => { + dispatch({ type: 'CONNECT', payload: _api }); + // `ready` event is not emitted upon reconnection and is checked explicitly here. + _api.isReady.then(() => dispatch({ type: 'CONNECT_SUCCESS' })); + }); + _api.on('ready', () => dispatch({ type: 'CONNECT_SUCCESS' })); + _api.on('error', (err) => dispatch({ type: 'CONNECT_ERROR', payload: err })); +}; + +const defaultValue = { + state: initialState, +}; + +const RelayApiContext = React.createContext(defaultValue); + +const RelayApiContextProvider = (props: any) => { + const [state, dispatch] = useReducer(reducer, initialState); + const { toastError, toastSuccess } = useToast(); + + useEffect(() => { + state.apiError && + toastError( + `Failed to connect to relay chain: error = ${state.apiError.toString()}` + ); + }, [state.apiError]); + + useEffect(() => { + state.apiState === 'READY' && + toastSuccess('Successfully connected to relay chain'); + }, [state.apiState]); + + useEffect(() => { + connect(state, dispatch); + }, [process.env.RELAY_CHAIN_ENDPOINT]); + + return ( + + {props.children} + + ); +}; +const useRelayApi = () => useContext(RelayApiContext); + +export { RelayApiContextProvider, useRelayApi }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index afe611b..35d40a1 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,6 +14,7 @@ import theme from '@/utils/muiTheme'; import { Layout } from '@/components/Layout'; +import { RelayApiContextProvider } from '@/contexts/RelayApi'; import { ToastProvider } from '@/contexts/Toast'; import { IdentityContractProvider } from '@/contracts'; import { AddressBookContractProvider } from '@/contracts/addressbook/context'; @@ -42,19 +43,21 @@ export default function MyApp(props: MyAppProps) { {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} - - - - - {getLayout()} - - - - + + + + + + {getLayout()} + + + + + diff --git a/src/pages/identity.tsx b/src/pages/identity.tsx index fe7aa4d..660266c 100644 --- a/src/pages/identity.tsx +++ b/src/pages/identity.tsx @@ -9,8 +9,8 @@ import { Grid, Typography, } from '@mui/material'; +import styles from '@styles/pages/identity.module.scss'; import { useState } from 'react'; -import '@styles/pages/identity.module.scss'; import { CreateIdentity, RemoveIdentity } from '@/components/Buttons'; import { AddressCard } from '@/components/Cards'; @@ -35,7 +35,7 @@ const IdentityPage = () => { return ( <> - + My Identity diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index c23eded..e980b65 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -1,39 +1,97 @@ -import { Box, FormLabel, MenuItem, TextField } from '@mui/material'; -import '@styles/pages/transfer.module.scss'; +import { + Backdrop, + Box, + CircularProgress, + FormControl, + FormLabel, + MenuItem, + TextField, +} from '@mui/material'; +import styles from '@styles/pages/transfer.module.scss'; +import { useCallback, useEffect, useState } from 'react'; +import { useRelayApi } from '@/contexts/RelayApi'; +import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; const TransferPage = () => { const { networks } = useIdentity(); + const [sourceChainId, setSourceChainId] = useState(); + const [destChainId, setDestChainId] = useState(); + const { + state: { api: relayApi }, + } = useRelayApi(); + const { toastSuccess, toastError } = useToast(); + const [loadingAssets, setLoadingAssets] = useState(false); + + const loadAssets = useCallback(async () => { + if (sourceChainId === undefined || destChainId === undefined) return; + setLoadingAssets(true); + + if (sourceChainId !== destChainId) { + const hrmp = await relayApi.query.hrmp.hrmpChannels({ + sender: sourceChainId, + recipient: destChainId, + }); + + if (hrmp.isEmpty) { + toastError( + "There's no HRMP channel open between the source and destination chain" + ); + } + } + + setLoadingAssets(false); + }, [sourceChainId, destChainId]); + + useEffect(() => { + loadAssets(); + }, [sourceChainId, destChainId]); return ( - - List of networks - setNetworkId(Number(e.target.value))} + + + + Source chain + setSourceChainId(Number(e.target.value))} + > + {Object.values(networks).map((network, index) => ( + + {network.name} + + ))} + + + + Destination chain + setDestChainId(Number(e.target.value))} + > + {Object.values(networks).map((network, index) => ( + + {network.name} + + ))} + + + + theme.zIndex.drawer + 1 }} + open={loadingAssets} > - {Object.entries(networks).map(([id, network], index) => ( - - {network.name} - - ))} - + + ); }; diff --git a/styles/global.scss b/styles/global.scss index da77b61..9c8947b 100644 --- a/styles/global.scss +++ b/styles/global.scss @@ -54,7 +54,7 @@ .MuiFormLabel-root { font-size: 16px; - font-weight: 600; + // font-weight: 600; line-height: 1.6; color: #181336; } @@ -70,7 +70,7 @@ .form-item { display: flex; flex-direction: column; - gap: 4px; + gap: 8px; } .modal-buttons { diff --git a/styles/pages/identity.module.scss b/styles/pages/identity.module.scss index 87027bc..423eae5 100644 --- a/styles/pages/identity.module.scss +++ b/styles/pages/identity.module.scss @@ -1,4 +1,4 @@ -.identity-container { +.identityContainer { display: flex; align-items: center; justify-content: space-between; diff --git a/styles/pages/transfer.module.scss b/styles/pages/transfer.module.scss index e69de29..5f99598 100644 --- a/styles/pages/transfer.module.scss +++ b/styles/pages/transfer.module.scss @@ -0,0 +1,9 @@ +.transferContainer { + display: flex; + flex-direction: column; + background: white; + margin-left: auto; + margin-right: auto; + min-width: 500px; + padding: 24px 16px; +} From a4122aefdcf5897f3035b8d1b9d97d024feaef37 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Sun, 20 Aug 2023 20:50:23 -0700 Subject: [PATCH 03/30] get shared assets --- src/consts/index.ts | 2 +- src/pages/transfer.tsx | 29 ++++++++++++++++++++++++++++- src/utils/assetRegistry.ts | 35 +++++++++++++++++++++++++---------- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/consts/index.ts b/src/consts/index.ts index 2b58f71..4a640de 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -1,4 +1,4 @@ -type RELAY_CHAIN_OPTION = 'polkadot' | 'kusama'; +export type RELAY_CHAIN_OPTION = 'polkadot' | 'kusama'; const RELAY_CHAIN_ENDPOINTS = { polkadot: "wss://polkadot.api.onfinality.io/public-ws", kusama: "wss://kusama.api.onfinality.io/public-ws" diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index e980b65..a2105e7 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -4,12 +4,17 @@ import { CircularProgress, FormControl, FormLabel, + List, + ListItem, MenuItem, TextField, } from '@mui/material'; import styles from '@styles/pages/transfer.module.scss'; import { useCallback, useEffect, useState } from 'react'; +import AssetRegistry, { Asset } from '@/utils/assetRegistry'; + +import { RELAY_CHAIN } from '@/consts'; import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; @@ -21,8 +26,9 @@ const TransferPage = () => { const { state: { api: relayApi }, } = useRelayApi(); - const { toastSuccess, toastError } = useToast(); + const { toastError } = useToast(); const [loadingAssets, setLoadingAssets] = useState(false); + const [assets, setAssets] = useState([]); const loadAssets = useCallback(async () => { if (sourceChainId === undefined || destChainId === undefined) return; @@ -38,7 +44,21 @@ const TransferPage = () => { toastError( "There's no HRMP channel open between the source and destination chain" ); + setAssets([]); + } else { + const _assets = await AssetRegistry.getSharedAssets( + RELAY_CHAIN, + sourceChainId, + destChainId + ); + setAssets(_assets); } + } else { + const _assets = await AssetRegistry.getAssetsOnBlockchain( + RELAY_CHAIN, + sourceChainId + ); + setAssets(_assets); } setLoadingAssets(false); @@ -85,6 +105,13 @@ const TransferPage = () => { ))} + {!loadingAssets && ( + + {assets.map((asset, index) => ( + {asset.name} + ))} + + )} theme.zIndex.drawer + 1 }} diff --git a/src/utils/assetRegistry.ts b/src/utils/assetRegistry.ts index 10bf2a6..247871e 100644 --- a/src/utils/assetRegistry.ts +++ b/src/utils/assetRegistry.ts @@ -1,10 +1,9 @@ import axios from 'axios'; +import { RELAY_CHAIN_OPTION } from '@/consts'; type ChainId = number | string; -type RelayChain = 'polkadot' | 'kusama'; - -type Asset = { +export type Asset = { asset: any; name: string; symbol: string; @@ -14,7 +13,7 @@ type Asset = { confidence: number; }; -type MultiAsset = { +export type MultiAsset = { parents: number; interior: | 'Here' @@ -33,13 +32,13 @@ const xcmGAR = class AssetRegistry { public static async getAssetsOnBlockchain( - relay: RelayChain, + relay: RELAY_CHAIN_OPTION, chain: ChainId ): Promise { const blockchains = (await axios.get(xcmGAR)).data; const blockchain = blockchains.assets[relay].find( - (b: any) => (typeof chain === 'string') ? b.id.toLowerCase() == chain.toLowerCase() : b.paraID === chain + (b: any) => (typeof chain === 'string') ? b.id.toLowerCase() === chain.toLowerCase() : b.paraID === chain ); if (!blockchain) { @@ -142,7 +141,7 @@ class AssetRegistry { } public static async isSupportedOnBothChains( - relay: RelayChain, + relay: RELAY_CHAIN_OPTION, chainA: ChainId, chainB: ChainId, asset: any @@ -154,11 +153,11 @@ class AssetRegistry { } public static async isSupportedOnChain( - relay: RelayChain, - chain: ChainId, + relay: RELAY_CHAIN_OPTION, + chainId: ChainId, asset: any ): Promise { - const assets = await this.getAssetsOnBlockchain(relay, chain); + const assets = await this.getAssetsOnBlockchain(relay, chainId); const found = assets.find( (el: Asset) => @@ -170,6 +169,22 @@ class AssetRegistry { return false; } + + public static async getSharedAssets(network: RELAY_CHAIN_OPTION, chainA: ChainId, chainB: ChainId): Promise { + const assetsA = await this.getAssetsOnBlockchain(network, chainA); + const assetsB = await this.getAssetsOnBlockchain(network, chainB); + const assets: Asset[] = []; + + assetsA.forEach((asset) => { + const found = assetsB.find( + (el: Asset) => + el.xcmInteriorKey && + JSON.stringify(el.xcmInteriorKey) === JSON.stringify(asset) + ); + if (found) assets.push(asset); + }); + return assets; + } } export default AssetRegistry; From 6b92ab649570b3ce9ab69753918186dbd4362fbe Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Wed, 23 Aug 2023 09:02:34 +0200 Subject: [PATCH 04/30] merge fix --- src/utils/assetRegistry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/assetRegistry.ts b/src/utils/assetRegistry.ts index 912f9a3..891fbb2 100644 --- a/src/utils/assetRegistry.ts +++ b/src/utils/assetRegistry.ts @@ -141,7 +141,7 @@ class AssetRegistry { } public static async assetsSupportedOnBothChains( - relay: RelayChain, + relay: RELAY_CHAIN_OPTION, chainA: ChainId, chainB: ChainId, ): Promise { @@ -179,7 +179,7 @@ class AssetRegistry { chain: ChainId, xcmAsset: any ): Promise { - const assets = await this.getAssetsOnBlockchain(relay, chainId); + const assets = await this.getAssetsOnBlockchain(relay, chain); return this.isSupported(xcmAsset, assets); } From 64eabdb44005e607a737f6c6c5cd7dda0499b7f0 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Wed, 23 Aug 2023 13:46:47 -0700 Subject: [PATCH 05/30] fetch token balance --- src/consts/index.ts | 3 +- src/pages/transfer.tsx | 103 ++++++++++++++++++++++++------ src/utils/assetRegistry.ts | 4 +- styles/pages/transfer.module.scss | 6 ++ 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/consts/index.ts b/src/consts/index.ts index 4a640de..b49252c 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -4,4 +4,5 @@ const RELAY_CHAIN_ENDPOINTS = { kusama: "wss://kusama.api.onfinality.io/public-ws" }; export const RELAY_CHAIN = (process.env.RELAY_CHAIN || 'polkadot') as RELAY_CHAIN_OPTION; -export const RELAY_CHAIN_ENDPOINT = RELAY_CHAIN_ENDPOINTS[RELAY_CHAIN]; \ No newline at end of file +export const RELAY_CHAIN_ENDPOINT = RELAY_CHAIN_ENDPOINTS[RELAY_CHAIN]; +export const ZERO = BigInt(0); diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index a2105e7..2fddc73 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -4,23 +4,25 @@ import { CircularProgress, FormControl, FormLabel, - List, - ListItem, MenuItem, + Select, TextField, } from '@mui/material'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { useInkathon } from '@scio-labs/use-inkathon'; import styles from '@styles/pages/transfer.module.scss'; import { useCallback, useEffect, useState } from 'react'; import AssetRegistry, { Asset } from '@/utils/assetRegistry'; -import { RELAY_CHAIN } from '@/consts'; +import { RELAY_CHAIN, ZERO } from '@/consts'; import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; const TransferPage = () => { const { networks } = useIdentity(); + const { activeAccount } = useInkathon(); const [sourceChainId, setSourceChainId] = useState(); const [destChainId, setDestChainId] = useState(); const { @@ -29,6 +31,11 @@ const TransferPage = () => { const { toastError } = useToast(); const [loadingAssets, setLoadingAssets] = useState(false); const [assets, setAssets] = useState([]); + const [tokenId, setTokenId] = useState(''); + const canWork = + !loadingAssets && sourceChainId !== undefined && destChainId !== undefined; + const [loadingBalance, setLoadingBalance] = useState(false); + const [sourceBalance, setSourceBalance] = useState(ZERO); const loadAssets = useCallback(async () => { if (sourceChainId === undefined || destChainId === undefined) return; @@ -36,8 +43,8 @@ const TransferPage = () => { if (sourceChainId !== destChainId) { const hrmp = await relayApi.query.hrmp.hrmpChannels({ - sender: sourceChainId, - recipient: destChainId, + sender: networks[sourceChainId].paraId, + recipient: networks[destChainId].paraId, }); if (hrmp.isEmpty) { @@ -46,18 +53,19 @@ const TransferPage = () => { ); setAssets([]); } else { - const _assets = await AssetRegistry.getSharedAssets( + const _assets = await AssetRegistry.assetsSupportedOnBothChains( RELAY_CHAIN, - sourceChainId, - destChainId + networks[sourceChainId].paraId, + networks[destChainId].paraId ); setAssets(_assets); } } else { const _assets = await AssetRegistry.getAssetsOnBlockchain( RELAY_CHAIN, - sourceChainId + networks[sourceChainId].paraId ); + setTokenId(''); setAssets(_assets); } @@ -68,6 +76,44 @@ const TransferPage = () => { loadAssets(); }, [sourceChainId, destChainId]); + useEffect(() => { + const fetchAssetBalance = async ( + chainId: number, + tokenId: string, + account: string, + callback: (_value: bigint) => void + ): Promise => { + try { + const provider = new WsProvider(networks[chainId].rpcUrls[0]); + const api = new ApiPromise({ provider }); + + await api.isReady; + + const res = await api.query.assets?.account(tokenId, account); + if (res.isEmpty) callback(ZERO); + else callback(BigInt(res.toString())); + } catch { + callback(ZERO); + } + }; + const fetchBalances = async () => { + if (sourceChainId === undefined || !activeAccount) return; + + setLoadingBalance(true); + + await fetchAssetBalance( + sourceChainId, + tokenId, + activeAccount.address, + (value) => setSourceBalance(value) + ); + + setLoadingBalance(false); + }; + + fetchBalances(); + }, [tokenId]); + return ( @@ -81,8 +127,8 @@ const TransferPage = () => { value={sourceChainId || ''} onChange={(e) => setSourceChainId(Number(e.target.value))} > - {Object.values(networks).map((network, index) => ( - + {Object.entries(networks).map(([chainId, network], index) => ( + {network.name} ))} @@ -98,24 +144,41 @@ const TransferPage = () => { value={destChainId || ''} onChange={(e) => setDestChainId(Number(e.target.value))} > - {Object.values(networks).map((network, index) => ( - + {Object.entries(networks).map(([chainId, network], index) => ( + {network.name} ))} - {!loadingAssets && ( - - {assets.map((asset, index) => ( - {asset.name} - ))} - + {canWork && + (assets.length > 0 ? ( + + Select asset to transfer + + + ) : ( +
There are no assets supported on both networks.
+ ))} + {canWork && !loadingBalance && tokenId && ( +
+
Balance:
+
{sourceBalance.toString()}
+
)}
theme.zIndex.drawer + 1 }} - open={loadingAssets} + open={loadingAssets || loadingBalance} > diff --git a/src/utils/assetRegistry.ts b/src/utils/assetRegistry.ts index 0d3a436..bcab4c3 100644 --- a/src/utils/assetRegistry.ts +++ b/src/utils/assetRegistry.ts @@ -4,7 +4,7 @@ type ChainId = number | string; type RelayChain = 'polkadot' | 'kusama'; -type Asset = { +export type Asset = { asset: any; name: string; symbol: string; @@ -145,7 +145,7 @@ class AssetRegistry { relay: RelayChain, chainA: ChainId, chainB: ChainId, - ): Promise { + ): Promise { const assetsOnChainA = await this.getAssetsOnBlockchain(relay, chainA); const assetsOnChainB = await this.getAssetsOnBlockchain(relay, chainB); diff --git a/styles/pages/transfer.module.scss b/styles/pages/transfer.module.scss index 5f99598..8e97266 100644 --- a/styles/pages/transfer.module.scss +++ b/styles/pages/transfer.module.scss @@ -7,3 +7,9 @@ min-width: 500px; padding: 24px 16px; } + +.balanceContainer { + display: flex; + justify-content: space-between; + align-items: center; +} \ No newline at end of file From 6e25faec4f59abf63b0f35297a1b4e443a6e0dec Mon Sep 17 00:00:00 2001 From: cute0laf Date: Wed, 23 Aug 2023 13:58:46 -0700 Subject: [PATCH 06/30] get addresses --- src/contracts/identity/context.tsx | 24 +++++++++++++++++------- src/pages/transfer.tsx | 4 ++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/contracts/identity/context.tsx b/src/contracts/identity/context.tsx index e8f72eb..cf55b03 100644 --- a/src/contracts/identity/context.tsx +++ b/src/contracts/identity/context.tsx @@ -164,14 +164,11 @@ const IdentityContractProvider = ({ children }: Props) => { setLoadingNetworks(false); }, [api, contract, toastError]); - const fetchAddresses = useCallback(async () => { - if (!api || !contract || identityNo === null) { - setAddresses([]); - return; - } + const getAddresses = async (no: number) => { + if (!api || !contract) return []; try { const result = await contractQuery(api, '', contract, 'identity', {}, [ - identityNo, + no, ]); const { output, isError, decodedOutput } = decodeOutput( result, @@ -190,8 +187,21 @@ const IdentityContractProvider = ({ children }: Props) => { address, }); } - setAddresses(_addresses); + return _addresses; } catch (e) { + return []; + } + }; + + const fetchAddresses = useCallback(async () => { + if (!api || !contract || identityNo === null) { + setAddresses([]); + return; + } + try { + const _addresses = await getAddresses(identityNo); + setAddresses(_addresses); + } catch { setAddresses([]); } }, [api, contract, identityNo]); diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index 2fddc73..ab49149 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -124,7 +124,7 @@ const TransferPage = () => { select sx={{ mt: '8px' }} required - value={sourceChainId || ''} + value={sourceChainId === undefined ? '' : sourceChainId} onChange={(e) => setSourceChainId(Number(e.target.value))} > {Object.entries(networks).map(([chainId, network], index) => ( @@ -141,7 +141,7 @@ const TransferPage = () => { select sx={{ mt: '8px' }} required - value={destChainId || ''} + value={destChainId === undefined ? '' : destChainId} onChange={(e) => setDestChainId(Number(e.target.value))} > {Object.entries(networks).map(([chainId, network], index) => ( From 538b50367c4b9a06fa0de6bae6b621a520465a50 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Thu, 24 Aug 2023 12:02:43 -0700 Subject: [PATCH 07/30] select identity, transfer --- src/contracts/identity/context.tsx | 7 +- src/pages/transfer.tsx | 125 +++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/src/contracts/identity/context.tsx b/src/contracts/identity/context.tsx index 1df4b22..4e38677 100644 --- a/src/contracts/identity/context.tsx +++ b/src/contracts/identity/context.tsx @@ -34,6 +34,7 @@ interface IdentityContract { contract: ContractPromise | undefined; fetchIdentityNo: () => Promise; fetchAddresses: () => Promise; + getAddresses: (_id: number) => Promise; loading: boolean; } @@ -49,6 +50,9 @@ const defaultIdentity: IdentityContract = { fetchAddresses: async () => { /* */ }, + getAddresses: async (): Promise => { + return []; + }, loading: true, }; @@ -166,7 +170,7 @@ const IdentityContractProvider = ({ children }: Props) => { setLoadingNetworks(false); }, [api, contract, toastError]); - const getAddresses = async (no: number) => { + const getAddresses = async (no: number): Promise => { if (!api || !contract) return []; try { const result = await contractQuery(api, '', contract, 'identity', {}, [ @@ -230,6 +234,7 @@ const IdentityContractProvider = ({ children }: Props) => { fetchAddresses, fetchIdentityNo, loading: loadingIdentityNo || loadingNetworks, + getAddresses, }} > {children} diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index ab49149..df9f5ae 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -1,6 +1,7 @@ import { Backdrop, Box, + Button, CircularProgress, FormControl, FormLabel, @@ -19,23 +20,43 @@ import { RELAY_CHAIN, ZERO } from '@/consts'; import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; +import { useAddressBook } from '@/contracts/addressbook/context'; const TransferPage = () => { - const { networks } = useIdentity(); + const { + networks, + getAddresses, + addresses, + contract: identityContract, + } = useIdentity(); const { activeAccount } = useInkathon(); + const { toastError } = useToast(); + const { identities } = useAddressBook(); + const [sourceChainId, setSourceChainId] = useState(); const [destChainId, setDestChainId] = useState(); const { state: { api: relayApi }, } = useRelayApi(); - const { toastError } = useToast(); + const [loadingAssets, setLoadingAssets] = useState(false); const [assets, setAssets] = useState([]); - const [tokenId, setTokenId] = useState(''); - const canWork = - !loadingAssets && sourceChainId !== undefined && destChainId !== undefined; + const [tokenId, setTokenId] = useState(); const [loadingBalance, setLoadingBalance] = useState(false); const [sourceBalance, setSourceBalance] = useState(ZERO); + const [recipientId, setRecipientId] = useState(); + const [recipientOk, setRecipientOk] = useState(false); + const [recipientAddress, setRecipientAddress] = useState(); + + const chainsSelected = + !loadingAssets && sourceChainId !== undefined && destChainId !== undefined; + const assetSelected = chainsSelected && Boolean(tokenId); + + const canTransfer = assetSelected && recipientId !== undefined; + + useEffect(() => setTokenId(undefined), [chainsSelected]); + + useEffect(() => setRecipientId(undefined), [assetSelected]); const loadAssets = useCallback(async () => { if (sourceChainId === undefined || destChainId === undefined) return; @@ -92,12 +113,19 @@ const TransferPage = () => { const res = await api.query.assets?.account(tokenId, account); if (res.isEmpty) callback(ZERO); else callback(BigInt(res.toString())); + + await api.disconnect(); } catch { callback(ZERO); } }; const fetchBalances = async () => { - if (sourceChainId === undefined || !activeAccount) return; + if ( + sourceChainId === undefined || + !activeAccount || + tokenId === undefined + ) + return; setLoadingBalance(true); @@ -114,6 +142,62 @@ const TransferPage = () => { fetchBalances(); }, [tokenId]); + useEffect(() => { + if (sourceChainId === undefined) return; + const index = addresses.findIndex( + (address) => address.networkId === sourceChainId + ); + index === -1 && + toastError(`You don't have ${networks[sourceChainId].name} address`); + }, [sourceChainId]); + + useEffect(() => { + const checkIdentity = async () => { + if (recipientId === undefined || destChainId === undefined) return; + const addresses = await getAddresses(recipientId); + const index = addresses.findIndex( + (address) => address.networkId === destChainId + ); + setRecipientOk(index !== -1); + setRecipientAddress(addresses[index].address); + + index === -1 && + toastError( + `${identities[recipientId].nickName} does not have ${networks[destChainId].name} address.` + ); + }; + setRecipientOk(false); + checkIdentity(); + }, [recipientId]); + + const transferAsset = () => { + if ( + recipientAddress === undefined || + destChainId === undefined || + identityContract === undefined + ) + return; + // TODO: + // TransactionRouter.sendTokens( + // identityContract, + // { + // keypair: activeAccount, + // network: sourceChainId, + // } as Sender, + // { + // addressRaw: decodeAddress(recipientAddress), + // type: networks[destChainId].accountType, + // network: destChainId, + // } as Receiver, + // 0, // FIXME: + // { + // // FIXME: + // multiAsset: 0, + // amount: 1, + // } + // ); + }; + return ( @@ -151,7 +235,7 @@ const TransferPage = () => { ))} - {canWork && + {chainsSelected && (assets.length > 0 ? ( Select asset to transfer @@ -169,12 +253,37 @@ const TransferPage = () => { ) : (
There are no assets supported on both networks.
))} - {canWork && !loadingBalance && tokenId && ( + {assetSelected && !loadingBalance && (
Balance:
{sourceBalance.toString()}
)} + {assetSelected && ( + + Select recipient + + + )} + {canTransfer && ( + + )}
theme.zIndex.drawer + 1 }} From 9c7b406920a44f71f2321d5e6fe0ebde155425b9 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Fri, 25 Aug 2023 16:11:04 +0200 Subject: [PATCH 08/30] isTransferSupported --- src/consts/index.ts | 29 ++++++++++++++++++ src/pages/transfer.tsx | 69 +++++++++++++++++++++++++++++------------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/consts/index.ts b/src/consts/index.ts index b49252c..50f1cbe 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -6,3 +6,32 @@ const RELAY_CHAIN_ENDPOINTS = { export const RELAY_CHAIN = (process.env.RELAY_CHAIN || 'polkadot') as RELAY_CHAIN_OPTION; export const RELAY_CHAIN_ENDPOINT = RELAY_CHAIN_ENDPOINTS[RELAY_CHAIN]; export const ZERO = BigInt(0); + +// NOTE: we do not need to store the name of these chains, but they are convenient +// for us while reading to code to see which chains support local XCM execution. +export const chainsSupportingXcmExecute = [ + { + relayChain: "kusama", + paraId: 0 + }, + { + relayChain: "kusama", + name: "crab", + paraId: 2105 + }, + { + relayChain: "kusama", + name: "quartz by unique", + paraId: 2095 + }, + { + relayChain: "polkadot", + name: "darwinia", + paraId: 2046 + }, + { + relayChain: "polkadot", + name: "unique network", + paraId: 2037 + } +]; diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index df9f5ae..534fa48 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -16,7 +16,7 @@ import { useCallback, useEffect, useState } from 'react'; import AssetRegistry, { Asset } from '@/utils/assetRegistry'; -import { RELAY_CHAIN, ZERO } from '@/consts'; +import { RELAY_CHAIN, ZERO, chainsSupportingXcmExecute } from '@/consts'; import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; @@ -170,32 +170,59 @@ const TransferPage = () => { checkIdentity(); }, [recipientId]); + const isTransferSupported = ( + originParaId: number, + reserveParaId: number, + ): boolean => { + // If the origin is the reserve chain that means that we can use the existing + // `limitedReserveTransferAssets` or `limitedTeleportAssets` extrinsics which are + // supported on all chains that have the xcm pallet. + if (originParaId == reserveParaId) { + return true; + } + + const isOriginSupportingLocalXCM = chainsSupportingXcmExecute.findIndex( + (chain) => chain.paraId == originParaId && chain.relayChain == RELAY_CHAIN, + ); + + // We only need the origin chain to support XCM for any other type of transfer to + // work. + if (isOriginSupportingLocalXCM) { + return true; + } + + return false; + }; + const transferAsset = () => { if ( recipientAddress === undefined || destChainId === undefined || identityContract === undefined - ) + ) { return; - // TODO: - // TransactionRouter.sendTokens( - // identityContract, - // { - // keypair: activeAccount, - // network: sourceChainId, - // } as Sender, - // { - // addressRaw: decodeAddress(recipientAddress), - // type: networks[destChainId].accountType, - // network: destChainId, - // } as Receiver, - // 0, // FIXME: - // { - // // FIXME: - // multiAsset: 0, - // amount: 1, - // } - // ); + } + + /* + await TransactionRouter.sendTokens( + identityContract, + { + keypair: activeAccount, + network: sourceChainId, + } as Sender, + { + addressRaw: decodeAddress(recipientAddress), + type: networks[destChainId].accountType, + network: destChainId, + } as Receiver, + 0, // Reserve paraId, FIXME + { + // FIXME: + multiAsset: 0, + amount: 1, + } + ); + */ }; return ( From 0bbbb3d1552ad3707e6acfbee9febaa0cebdaa8d Mon Sep 17 00:00:00 2001 From: cute0laf Date: Fri, 25 Aug 2023 09:04:42 -0700 Subject: [PATCH 09/30] bug fix, add input field for amount --- src/pages/transfer.tsx | 50 ++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index 534fa48..1e44cbc 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -15,6 +15,8 @@ import styles from '@styles/pages/transfer.module.scss'; import { useCallback, useEffect, useState } from 'react'; import AssetRegistry, { Asset } from '@/utils/assetRegistry'; +import IdentityKey from '@/utils/identityKey'; +import KeyStore from '@/utils/keyStore'; import { RELAY_CHAIN, ZERO, chainsSupportingXcmExecute } from '@/consts'; import { useRelayApi } from '@/contexts/RelayApi'; @@ -47,6 +49,7 @@ const TransferPage = () => { const [recipientId, setRecipientId] = useState(); const [recipientOk, setRecipientOk] = useState(false); const [recipientAddress, setRecipientAddress] = useState(); + const [amount, setAmount] = useState(); const chainsSelected = !loadingAssets && sourceChainId !== undefined && destChainId !== undefined; @@ -154,17 +157,29 @@ const TransferPage = () => { useEffect(() => { const checkIdentity = async () => { if (recipientId === undefined || destChainId === undefined) return; - const addresses = await getAddresses(recipientId); + const addresses = await getAddresses(identities[recipientId].identityNo); const index = addresses.findIndex( (address) => address.networkId === destChainId ); setRecipientOk(index !== -1); - setRecipientAddress(addresses[index].address); - - index === -1 && + if (index === -1) { toastError( `${identities[recipientId].nickName} does not have ${networks[destChainId].name} address.` ); + } else { + const identityKey = KeyStore.readIdentityKey(recipientId) || ''; + const destAddressRaw = addresses[index].address; + if (IdentityKey.containsNetworkId(identityKey, destChainId)) { + const decryptedAddress = IdentityKey.decryptAddress( + identityKey, + destChainId, + destAddressRaw + ); + setRecipientAddress(decryptedAddress); + } else { + toastError('Cannot find identity key for the recipient'); + } + } }; setRecipientOk(false); checkIdentity(); @@ -290,11 +305,11 @@ const TransferPage = () => { Select recipient setRecipientId(Number(e.target.value))} > {identities.map((identity, index) => ( From 23a68e11681bd81040a17c3d02964069e0be0059 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Sun, 27 Aug 2023 11:11:02 -0700 Subject: [PATCH 11/30] remove balance --- src/pages/_app.tsx | 32 +++++----- src/pages/transfer.tsx | 134 ++++++++++++++++++++--------------------- 2 files changed, 82 insertions(+), 84 deletions(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index bf9757e..2664910 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -43,11 +43,11 @@ export default function MyApp(props: MyAppProps) { {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} - + - - - - {getLayout()} - - - - + }} + > + + + + {getLayout()} + + + + + diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index 4bb3068..baa1a78 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -9,8 +9,6 @@ import { Select, TextField, } from '@mui/material'; -import { ApiPromise, WsProvider } from '@polkadot/api'; -import { useInkathon } from '@scio-labs/use-inkathon'; import styles from '@styles/pages/transfer.module.scss'; import { useCallback, useEffect, useState } from 'react'; @@ -18,7 +16,7 @@ import AssetRegistry, { Asset } from '@/utils/assetRegistry'; import IdentityKey from '@/utils/identityKey'; import KeyStore from '@/utils/keyStore'; -import { chainsSupportingXcmExecute, RELAY_CHAIN, ZERO } from '@/consts'; +import { chainsSupportingXcmExecute, RELAY_CHAIN } from '@/consts'; import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; @@ -26,12 +24,12 @@ import { useAddressBook } from '@/contracts/addressbook/context'; const TransferPage = () => { const { - networks, + chains, getAddresses, addresses, contract: identityContract, } = useIdentity(); - const { activeAccount } = useInkathon(); + // const { activeAccount } = useInkathon(); const { toastError } = useToast(); const { identities } = useAddressBook(); @@ -44,8 +42,8 @@ const TransferPage = () => { const [loadingAssets, setLoadingAssets] = useState(false); const [assets, setAssets] = useState([]); const [tokenId, setTokenId] = useState(); - const [loadingBalance, setLoadingBalance] = useState(false); - const [sourceBalance, setSourceBalance] = useState(ZERO); + // const [loadingBalance, setLoadingBalance] = useState(false); + // const [sourceBalance, setSourceBalance] = useState(ZERO); const [recipientId, setRecipientId] = useState(); const [recipientOk, setRecipientOk] = useState(false); const [recipientAddress, setRecipientAddress] = useState(); @@ -68,8 +66,8 @@ const TransferPage = () => { if (sourceChainId !== destChainId) { const hrmp = await relayApi.query.hrmp.hrmpChannels({ - sender: networks[sourceChainId].paraId, - recipient: networks[destChainId].paraId, + sender: chains[sourceChainId].paraId, + recipient: chains[destChainId].paraId, }); if (hrmp.isEmpty) { @@ -80,15 +78,15 @@ const TransferPage = () => { } else { const _assets = await AssetRegistry.assetsSupportedOnBothChains( RELAY_CHAIN, - networks[sourceChainId].paraId, - networks[destChainId].paraId + chains[sourceChainId].paraId, + chains[destChainId].paraId ); setAssets(_assets); } } else { const _assets = await AssetRegistry.getAssetsOnBlockchain( RELAY_CHAIN, - networks[sourceChainId].paraId + chains[sourceChainId].paraId ); setTokenId(''); setAssets(_assets); @@ -101,58 +99,58 @@ const TransferPage = () => { loadAssets(); }, [sourceChainId, destChainId]); - useEffect(() => { - const fetchAssetBalance = async ( - chainId: number, - tokenId: string, - account: string, - callback: (_value: bigint) => void - ): Promise => { - try { - const provider = new WsProvider(networks[chainId].rpcUrls[0]); - const api = new ApiPromise({ provider }); - - await api.isReady; - - const res = await api.query.assets?.account(tokenId, account); - if (res.isEmpty) callback(ZERO); - else callback(BigInt(res.toString())); - - await api.disconnect(); - } catch { - callback(ZERO); - } - }; - const fetchBalances = async () => { - if ( - sourceChainId === undefined || - !activeAccount || - tokenId === undefined - ) - return; - - setLoadingBalance(true); - - await fetchAssetBalance( - sourceChainId, - tokenId, - activeAccount.address, - (value) => setSourceBalance(value) - ); - - setLoadingBalance(false); - }; - - fetchBalances(); - }, [tokenId]); + // useEffect(() => { + // const fetchAssetBalance = async ( + // chainId: number, + // tokenId: string, + // account: string, + // callback: (_value: bigint) => void + // ): Promise => { + // try { + // const provider = new WsProvider(chains[chainId].rpcUrls[0]); + // const api = new ApiPromise({ provider }); + + // await api.isReady; + + // const res = await api.query.assets?.account(tokenId, account); + // if (res.isEmpty) callback(ZERO); + // else callback(BigInt(res.toString())); + + // await api.disconnect(); + // } catch { + // callback(ZERO); + // } + // }; + // const fetchBalances = async () => { + // if ( + // sourceChainId === undefined || + // !activeAccount || + // tokenId === undefined + // ) + // return; + + // setLoadingBalance(true); + + // await fetchAssetBalance( + // sourceChainId, + // tokenId, + // activeAccount.address, + // (value) => setSourceBalance(value) + // ); + + // setLoadingBalance(false); + // }; + + // fetchBalances(); + // }, [tokenId]); useEffect(() => { if (sourceChainId === undefined) return; const index = addresses.findIndex( - (address) => address.networkId === sourceChainId + (address) => address.chainId === sourceChainId ); index === -1 && - toastError(`You don't have ${networks[sourceChainId].name} address`); + toastError(`You don't have ${chains[sourceChainId].name} address`); }, [sourceChainId]); useEffect(() => { @@ -160,17 +158,17 @@ const TransferPage = () => { if (recipientId === undefined || destChainId === undefined) return; const addresses = await getAddresses(identities[recipientId].identityNo); const index = addresses.findIndex( - (address) => address.networkId === destChainId + (address) => address.chainId === destChainId ); setRecipientOk(index !== -1); if (index === -1) { toastError( - `${identities[recipientId].nickName} does not have ${networks[destChainId].name} address.` + `${identities[recipientId].nickName} does not have ${chains[destChainId].name} address.` ); } else { const identityKey = KeyStore.readIdentityKey(recipientId) || ''; const destAddressRaw = addresses[index].address; - if (IdentityKey.containsNetworkId(identityKey, destChainId)) { + if (IdentityKey.containsChainId(identityKey, destChainId)) { const decryptedAddress = IdentityKey.decryptAddress( identityKey, destChainId, @@ -229,7 +227,7 @@ const TransferPage = () => { } as Sender, { addressRaw: decodeAddress(recipientAddress), - type: networks[destChainId].accountType, + type: chains[destChainId].accountType, network: destChainId, } as Receiver, 0, // Reserve paraId, FIXME @@ -255,7 +253,7 @@ const TransferPage = () => { value={sourceChainId === undefined ? '' : sourceChainId} onChange={(e) => setSourceChainId(Number(e.target.value))} > - {Object.entries(networks).map(([chainId, network], index) => ( + {Object.entries(chains).map(([chainId, network], index) => ( {network.name} @@ -272,7 +270,7 @@ const TransferPage = () => { value={destChainId === undefined ? '' : destChainId} onChange={(e) => setDestChainId(Number(e.target.value))} > - {Object.entries(networks).map(([chainId, network], index) => ( + {Object.entries(chains).map(([chainId, network], index) => ( {network.name} @@ -295,14 +293,14 @@ const TransferPage = () => { ) : ( -
There are no assets supported on both networks.
+
There are no assets supported on both chains.
))} - {assetSelected && !loadingBalance && ( + {/* {assetSelected && !loadingBalance && (
Balance:
{sourceBalance.toString()}
- )} + )} */} {assetSelected && ( Select recipient @@ -338,7 +336,7 @@ const TransferPage = () => {
theme.zIndex.drawer + 1 }} - open={loadingAssets || loadingBalance} + open={loadingAssets} > From 924b00d70b7d0fcc7e095772e67455f67821b804 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Mon, 28 Aug 2023 17:52:14 +0200 Subject: [PATCH 12/30] progress --- src/pages/transfer.tsx | 111 ++++++++++++------ .../transactionRouter/teleportTransfer.ts | 3 +- src/utils/transactionRouter/transferAsset.ts | 5 +- src/utils/transactionRouter/types.ts | 3 +- 4 files changed, 86 insertions(+), 36 deletions(-) diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index baa1a78..5ece812 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -21,6 +21,10 @@ import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; import { useAddressBook } from '@/contracts/addressbook/context'; +import TransactionRouter from '@/utils/transactionRouter'; +import { useInkathon } from '@scio-labs/use-inkathon'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { AccountType } from 'types/types-arguments/identity'; const TransferPage = () => { const { @@ -29,7 +33,7 @@ const TransferPage = () => { addresses, contract: identityContract, } = useIdentity(); - // const { activeAccount } = useInkathon(); + const { activeAccount } = useInkathon(); const { toastError } = useToast(); const { identities } = useAddressBook(); @@ -41,7 +45,7 @@ const TransferPage = () => { const [loadingAssets, setLoadingAssets] = useState(false); const [assets, setAssets] = useState([]); - const [tokenId, setTokenId] = useState(); + const [selectedAssetXcmInterior, setSelectedXcmInterior] = useState(); // const [loadingBalance, setLoadingBalance] = useState(false); // const [sourceBalance, setSourceBalance] = useState(ZERO); const [recipientId, setRecipientId] = useState(); @@ -51,11 +55,9 @@ const TransferPage = () => { const chainsSelected = !loadingAssets && sourceChainId !== undefined && destChainId !== undefined; - const assetSelected = chainsSelected && Boolean(tokenId); + const assetSelected = chainsSelected && Boolean(selectedAssetXcmInterior); - const canTransfer = assetSelected && recipientId !== undefined; - - useEffect(() => setTokenId(undefined), [chainsSelected]); + useEffect(() => setSelectedXcmInterior(undefined), [chainsSelected]); useEffect(() => setRecipientId(undefined), [assetSelected]); useEffect(() => setAmount(undefined), [assetSelected]); @@ -88,7 +90,7 @@ const TransferPage = () => { RELAY_CHAIN, chains[sourceChainId].paraId ); - setTokenId(''); + setSelectedXcmInterior([]); setAssets(_assets); } @@ -102,7 +104,7 @@ const TransferPage = () => { // useEffect(() => { // const fetchAssetBalance = async ( // chainId: number, - // tokenId: string, + // selectedAssetXcmInterior: string, // account: string, // callback: (_value: bigint) => void // ): Promise => { @@ -112,7 +114,7 @@ const TransferPage = () => { // await api.isReady; - // const res = await api.query.assets?.account(tokenId, account); + // const res = await api.query.assets?.account(selectedAssetXcmInterior, account); // if (res.isEmpty) callback(ZERO); // else callback(BigInt(res.toString())); @@ -125,7 +127,7 @@ const TransferPage = () => { // if ( // sourceChainId === undefined || // !activeAccount || - // tokenId === undefined + // selectedAssetXcmInterior === undefined // ) // return; @@ -133,7 +135,7 @@ const TransferPage = () => { // await fetchAssetBalance( // sourceChainId, - // tokenId, + // selectedAssetXcmInterior, // activeAccount.address, // (value) => setSourceBalance(value) // ); @@ -142,7 +144,7 @@ const TransferPage = () => { // }; // fetchBalances(); - // }, [tokenId]); + // }, [selectedAssetXcmInterior]); useEffect(() => { if (sourceChainId === undefined) return; @@ -202,44 +204,86 @@ const TransferPage = () => { // We only need the origin chain to support XCM for any other type of transfer to // work. - if (isOriginSupportingLocalXCM) { + if (isOriginSupportingLocalXCM >= 0) { return true; } return false; }; - const transferAsset = () => { + const transferAsset = async () => { if ( recipientAddress === undefined || destChainId === undefined || - identityContract === undefined + identityContract === undefined || + sourceChainId === undefined || + activeAccount === undefined || + selectedAssetXcmInterior === undefined || + amount === undefined ) { return; } - /* + const reserveChainId = getParaIdFromXcmInterior(selectedAssetXcmInterior); + + const count = Math.min( + chains[sourceChainId].rpcUrls.length, + chains[destChainId].rpcUrls.length, + chains[reserveChainId].rpcUrls.length + ); + + const rpcIndex = Math.min(Math.floor(Math.random() * count), count - 1); + + const isSourceParachain = sourceChainId > 0; + + const textEncoder = new TextEncoder(); + const addressRaw = textEncoder.encode(recipientAddress); + + console.log(selectedAssetXcmInterior); + await TransactionRouter.sendTokens( - identityContract, { - keypair: activeAccount, - network: sourceChainId, - } as Sender, + keypair: activeAccount, // How to convert active account into a keypair? + chain: sourceChainId + }, { - addressRaw: decodeAddress(recipientAddress), - type: chains[destChainId].accountType, - network: destChainId, - } as Receiver, - 0, // Reserve paraId, FIXME + addressRaw, + chain: destChainId, + type: AccountType[chains[destChainId].accountType as keyof typeof AccountType] + }, + 0, { - // FIXME: - multiAsset: 0, - amount: 1, + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + selectedAssetXcmInterior, + isSourceParachain, + sourceChainId + ), + amount + }, + { + originApi: await getApi(chains[sourceChainId].rpcUrls[rpcIndex]), + destApi: await getApi(chains[destChainId].rpcUrls[rpcIndex]), + reserveApi: await getApi(chains[reserveChainId].rpcUrls[rpcIndex]) } - ); - */ + ) }; + const getParaIdFromXcmInterior = (xcmInterior: any[]): number => { + if (xcmInterior[1].hasOwnProperty('parachain')) { + return xcmInterior[1].parachain; + } else { + return 0; + } + } + + const getApi = async (rpc: string): Promise => { + const provider = new WsProvider(rpc); + const api = await ApiPromise.create({ provider }); + return api; + } + + const canTransfer = assetSelected && recipientId !== undefined && isTransferSupported(sourceChainId, destChainId); + return ( @@ -282,11 +326,11 @@ const TransferPage = () => { Select asset to transfer setSelectedXcmInterior(e.target.value)} + value={selectedAsset || ''} + onChange={(e: any) => setSelectedAsset(e.target.value)} > {assets.map((asset, index) => ( - + {asset.name} ))} @@ -366,12 +320,6 @@ const TransferPage = () => { ) : (
There are no assets supported on both chains.
))} - {/* {assetSelected && !loadingBalance && ( -
-
Balance:
-
{sourceBalance.toString()}
-
- )} */} {assetSelected && ( Select recipient @@ -391,8 +339,8 @@ const TransferPage = () => { <> setAmount(parseFloat(e.target.value))} /> + )}
From 5e86b80e6df4f7128b0d59d741ac757d6678df69 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Tue, 29 Aug 2023 12:23:35 +0200 Subject: [PATCH 26/30] kind of a fix --- src/pages/transfer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index fb70da1..5e67ff0 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -309,6 +309,8 @@ const TransferPage = () => { onChange={(e: any) => setSelectedAsset(e.target.value)} > {assets.map((asset, index) => ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore {asset.name} From 895152bc7b4e87193434de8875120771732e33bd Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Tue, 29 Aug 2023 14:26:51 +0200 Subject: [PATCH 27/30] fix tests --- src/pages/transfer.tsx | 4 +++- src/utils/transactionRouter/reserveTransfer.ts | 12 +++++++++--- src/utils/transactionRouter/teleportTransfer.ts | 4 +++- src/utils/transactionRouter/teleportableRoutes.ts | 2 +- src/utils/transactionRouter/transferAsset.ts | 3 ++- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index 5e67ff0..66b5568 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -67,6 +67,8 @@ const TransferPage = () => { const canTransfer = assetSelected && recipientId !== undefined; const loadAssets = useCallback(async () => { + if (!relayApi) return; + if (sourceChainId === undefined || destChainId === undefined) return; setLoadingAssets(true); @@ -101,7 +103,7 @@ const TransferPage = () => { } setLoadingAssets(false); - }, [sourceChainId, destChainId]); + }, [sourceChainId, destChainId, relayApi]); useEffect(() => { loadAssets(); diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index 468d395..24b44bf 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -38,9 +38,11 @@ class ReserveTransfer { if (signer) originApi.setSigner(signer); + const account = signer ? sender.keypair.address : sender.keypair; + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsub = await reserveTransfer.signAndSend(sender.keypair.address, (result: any) => { + const unsub = await reserveTransfer.signAndSend(account, (result: any) => { if (result.status.isFinalized) { unsub(); resolve(); @@ -75,9 +77,11 @@ class ReserveTransfer { if (signer) originApi.setSigner(signer); + const account = signer ? sender.keypair.address : sender.keypair; + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsub = await reserveTransfer.signAndSend(sender.keypair.address, (result: any) => { + const unsub = await reserveTransfer.signAndSend(account, (result: any) => { if (result.status.isFinalized) { unsub(); resolve(); @@ -112,9 +116,11 @@ class ReserveTransfer { if (signer) originApi.setSigner(signer); + const account = signer ? sender.keypair.address : sender.keypair; + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsub = await reserveTransfer.signAndSend(sender.keypair.address, (result: any) => { + const unsub = await reserveTransfer.signAndSend(account, (result: any) => { if (result.status.isFinalized) { unsub(); resolve(); diff --git a/src/utils/transactionRouter/teleportTransfer.ts b/src/utils/transactionRouter/teleportTransfer.ts index d2aa36a..a70cb45 100644 --- a/src/utils/transactionRouter/teleportTransfer.ts +++ b/src/utils/transactionRouter/teleportTransfer.ts @@ -43,9 +43,11 @@ class TeleportTransfer { if (signer) originApi.setSigner(signer); + const account = signer ? sender.address : sender; + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsub = await teleport.signAndSend(sender.address, (result: any) => { + const unsub = await teleport.signAndSend(account, (result: any) => { if (result.status.isFinalized) { unsub(); resolve(); diff --git a/src/utils/transactionRouter/teleportableRoutes.ts b/src/utils/transactionRouter/teleportableRoutes.ts index 7303fc9..3d322d5 100644 --- a/src/utils/transactionRouter/teleportableRoutes.ts +++ b/src/utils/transactionRouter/teleportableRoutes.ts @@ -1,7 +1,7 @@ // File containing all the possible assets on all possible routes that support asset // teleportation. -import { RELAY_CHAIN } from "@/consts"; +import { RELAY_CHAIN } from "../../consts"; import AssetRegistry, { Asset } from "../assetRegistry"; diff --git a/src/utils/transactionRouter/transferAsset.ts b/src/utils/transactionRouter/transferAsset.ts index 6794d0a..41f0889 100644 --- a/src/utils/transactionRouter/transferAsset.ts +++ b/src/utils/transactionRouter/transferAsset.ts @@ -40,9 +40,10 @@ class TransferAsset { if (signer) api.setSigner(signer); + const account = signer ? sender.address : sender; // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - const unsub = await xcmExecute.signAndSend(sender.address, (result: any) => { + const unsub = await xcmExecute.signAndSend(account, (result: any) => { if (result.status.isFinalized) { unsub(); resolve(); From fb12a85495bb30defeab90b77ea9852ac4111779 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Tue, 29 Aug 2023 16:46:31 +0200 Subject: [PATCH 28/30] fix linter errors --- src/pages/transfer.tsx | 3 +-- src/utils/transactionRouter/teleportableRoutes.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index 66b5568..fec94bf 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -1,8 +1,8 @@ +import { LoadingButton } from '@mui/lab'; import { Alert, Backdrop, Box, - Button, CircularProgress, FormControl, FormLabel, @@ -28,7 +28,6 @@ import { useRelayApi } from '@/contexts/RelayApi'; import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; import { useAddressBook } from '@/contracts/addressbook/context'; -import { LoadingButton } from '@mui/lab'; const TransferPage = () => { const { diff --git a/src/utils/transactionRouter/teleportableRoutes.ts b/src/utils/transactionRouter/teleportableRoutes.ts index 3d322d5..4aa87d3 100644 --- a/src/utils/transactionRouter/teleportableRoutes.ts +++ b/src/utils/transactionRouter/teleportableRoutes.ts @@ -1,9 +1,8 @@ // File containing all the possible assets on all possible routes that support asset // teleportation. -import { RELAY_CHAIN } from "../../consts"; - import AssetRegistry, { Asset } from "../assetRegistry"; +import { RELAY_CHAIN } from "../../consts"; export type TeleportableRoute = { relayChain: string, From 5ce1355b262d84ba92cdd1e8c9084ff7d8ad3f58 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Tue, 29 Aug 2023 16:47:52 +0200 Subject: [PATCH 29/30] remove old comments --- src/utils/transactionRouter/reserveTransfer.ts | 2 -- src/utils/transactionRouter/transferAsset.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index 24b44bf..3b1b787 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -171,7 +171,6 @@ class ReserveTransfer { }, reserve, xcm: [ - // TODO: the hardcoded number isn't really accurate to what we actually need. this.buyExecution(assetFromReservePerspective, 450000000000), this.depositReserveAsset({ Wild: "All" }, 1, { parents: 1, @@ -228,7 +227,6 @@ class ReserveTransfer { }, reserve, xcm: [ - // TODO: the hardcoded number isn't really accurate to what we actually need. this.buyExecution(assetFromReservePerspective, 450000000000), this.depositAsset({ Wild: "All" }, 1, beneficiary) ] diff --git a/src/utils/transactionRouter/transferAsset.ts b/src/utils/transactionRouter/transferAsset.ts index 41f0889..c4dbd7a 100644 --- a/src/utils/transactionRouter/transferAsset.ts +++ b/src/utils/transactionRouter/transferAsset.ts @@ -32,7 +32,6 @@ class TransferAsset { throw new Error("The blockchain does not support XCM"); }; - // TODO: come up with more precise weight estimations. const xcmExecute = xcmPallet.execute(xcm, { refTime: Math.pow(10, 9), proofSize: 10000, @@ -77,7 +76,6 @@ class TransferAsset { }; } - // TODO: should this have `BuyExecution`? const xcmMessage = { V2: [ { From 84a1d902d6dc1af3228795ab667f7728086a8ef8 Mon Sep 17 00:00:00 2001 From: cute0laf Date: Tue, 29 Aug 2023 09:05:52 -0700 Subject: [PATCH 30/30] amount --- src/pages/transfer.tsx | 51 ++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index fec94bf..022cff8 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -30,11 +30,7 @@ import { useIdentity } from '@/contracts'; import { useAddressBook } from '@/contracts/addressbook/context'; const TransferPage = () => { - const { - chains, - getAddresses, - contract: identityContract, - } = useIdentity(); + const { chains, getAddresses, contract: identityContract } = useIdentity(); const { activeAccount, activeSigner } = useInkathon(); const { toastError } = useToast(); const { identities } = useAddressBook(); @@ -149,7 +145,9 @@ const TransferPage = () => { ) { return false; } - const reserveParaId = getParaIdFromXcmInterior(selectedAsset.xcmInteriorKey); + const reserveParaId = getParaIdFromXcmInterior( + selectedAsset.xcmInteriorKey + ); // If the origin is the reserve chain that means that we can use the existing // `limitedReserveTransferAssets` or `limitedTeleportAssets` extrinsics which are // supported on all chains that have the xcm pallet. @@ -161,13 +159,18 @@ const TransferPage = () => { if ( sourceChainId !== destChainId && - isTeleport(sourceChainId, destChainId, getFungible(selectedAsset.xcmInteriorKey, isSourceParachain, 0)) + isTeleport( + sourceChainId, + destChainId, + getFungible(selectedAsset.xcmInteriorKey, isSourceParachain, 0) + ) ) { return true; } const isOriginSupportingLocalXCM = chainsSupportingXcmExecute.findIndex( - (chain) => chain.paraId == sourceChainId && chain.relayChain == RELAY_CHAIN + (chain) => + chain.paraId == sourceChainId && chain.relayChain == RELAY_CHAIN ); // We only need the origin chain to support XCM for any other type of transfer to @@ -192,7 +195,9 @@ const TransferPage = () => { return; } - const reserveChainId = getParaIdFromXcmInterior(selectedAsset.xcmInteriorKey); + const reserveChainId = getParaIdFromXcmInterior( + selectedAsset.xcmInteriorKey + ); const count = Math.min( chains[sourceChainId].rpcUrls.length, @@ -226,7 +231,11 @@ const TransferPage = () => { : AccountType.accountKey20, }, reserveChainId, - getFungible(selectedAsset.xcmInteriorKey, isSourceParachain, amount * Math.pow(10, selectedAsset.decimals)), + getFungible( + selectedAsset.xcmInteriorKey, + isSourceParachain, + amount * Math.pow(10, selectedAsset.decimals) + ), { originApi: await getApi(chains[sourceChainId].rpcUrls[rpcIndex]), destApi: await getApi(chains[destChainId].rpcUrls[rpcIndex]), @@ -252,8 +261,14 @@ const TransferPage = () => { return api; }; - const getFungible = (xcmInterior: any, isSourceParachain: boolean, amount: number): Fungible => { - xcmInterior = Array.isArray(xcmInterior) ? xcmInterior : JSON.parse(xcmInterior.toString()); + const getFungible = ( + xcmInterior: any, + isSourceParachain: boolean, + amount: number + ): Fungible => { + xcmInterior = Array.isArray(xcmInterior) + ? xcmInterior + : JSON.parse(xcmInterior.toString()); return { multiAsset: AssetRegistry.xcmInteriorToMultiAsset( xcmInterior, @@ -262,7 +277,7 @@ const TransferPage = () => { ), amount, }; - } + }; return ( @@ -339,14 +354,16 @@ const TransferPage = () => { {canTransfer && ( <> setAmount(parseFloat(e.target.value))} /> - {!isTransferSupported() && - This transfer route is currently not supported. - } + {!isTransferSupported() && ( + + This transfer route is currently not supported. + + )}