Skip to content

Commit

Permalink
Cross-chain token transfer (#73)
Browse files Browse the repository at this point in the history
* Cross-chain token transfer

* native transfer works

* cross-chain transfers

* symbol

* lint fix

* refactor utililty methods

* update transfer page

* update purchase page

* small fixes

* add network icon

* add border radius

* add rococo

* fix

* update asset & chain selector

* fixes

* fixes

---------

Co-authored-by: topeth <supernathanliu@gmail.com>
  • Loading branch information
Szegoo and TopETH authored Apr 23, 2024
1 parent 2c3ff9d commit 20e9299
Show file tree
Hide file tree
Showing 19 changed files with 614 additions and 190 deletions.
Binary file added src/assets/networks/coretime.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/networks/kusama.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/networks/regionx.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/networks/rococo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions src/components/Elements/AmountInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Stack, TextField, Typography } from '@mui/material';
interface AmountInputProps {
amount: string;
currency: string;
title: string;
caption: string;
title?: string;
caption?: string;
setAmount: (_: string) => void;
}

Expand Down Expand Up @@ -45,8 +45,8 @@ export const AmountInput = ({
return (
<>
<Stack alignItems='center' direction='row' gap={1}>
<Typography variant='h6'>{title}</Typography>
<Typography>{caption}</Typography>
{title && <Typography variant='h6'>{title}</Typography>}
{caption && <Typography>{caption}</Typography>}
</Stack>
<TextField
value={`${amount} ${currency}`}
Expand Down
26 changes: 26 additions & 0 deletions src/components/Elements/Balance/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Typography, useTheme } from '@mui/material';

import { formatBalance } from '@/utils/functions';

interface BalanceProps {
coretimeBalance: number;
relayBalance: number;
symbol: string;
}

const Balance = ({ relayBalance, coretimeBalance, symbol }: BalanceProps) => {
const theme = useTheme();

return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography sx={{ color: theme.palette.text.primary, my: '0.5em' }}>
{`Relay chain: ${formatBalance(relayBalance.toString(), false)} ${symbol}`}
</Typography>
<Typography sx={{ color: theme.palette.text.primary, my: '0.5em' }}>
{`Coretime chain: ${formatBalance(coretimeBalance.toString(), false)} ${symbol}`}
</Typography>
</div>
);
};

export default Balance;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.options {
display: flex;
justify-content: center;
}

.option {
min-width: 250px;
margin: 1em 5em;
}
45 changes: 45 additions & 0 deletions src/components/Elements/Selectors/AssetSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ToggleButton, ToggleButtonGroup, useTheme } from '@mui/material';
import FormControl from '@mui/material/FormControl';

import { AssetType } from '@/models';

import styles from './index.module.scss';

interface AssetSelectorProps {
asset: AssetType;
setAsset: (_: AssetType) => void;
symbol: string;
}

export default function AssetSelector({
asset,
setAsset,
symbol,
}: AssetSelectorProps) {
const theme = useTheme();
return (
<FormControl>
<ToggleButtonGroup
value={asset}
exclusive // This ensures only one can be selected at a time
onChange={(e: any) => setAsset(parseInt(e.target.value) as AssetType)}
className={styles.options}
>
<ToggleButton
className={styles.option}
sx={{ color: theme.palette.text.primary }}
value={AssetType.TOKEN}
>
{symbol}
</ToggleButton>
<ToggleButton
className={styles.option}
sx={{ color: theme.palette.text.primary }}
value={AssetType.REGION}
>
Region
</ToggleButton>
</ToggleButtonGroup>
</FormControl>
);
}
56 changes: 50 additions & 6 deletions src/components/Elements/Selectors/ChainSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
import {
Box,
FormControl,
InputLabel,
MenuItem,
Select,
Typography,
} from '@mui/material';
import Image from 'next/image';

import CoretimeIcon from '@/assets/networks/coretime.png';
import RegionXIcon from '@/assets/networks/regionx.png';
// import KusamaIcon from '@/assets/networks/kusama.png';
import RococoIcon from '@/assets/networks/rococo.png';
import { ChainType } from '@/models';

interface ChainSelectorProps {
chain: string;
setChain: (_: string) => void;
chain: ChainType;
setChain: (_: ChainType) => void;
}

export const ChainSelector = ({ chain, setChain }: ChainSelectorProps) => {
const menuItems = [
{
icon: RococoIcon,
label: 'Relay Chain',
value: ChainType.RELAY,
},
{
icon: CoretimeIcon,
label: 'Coretime Chain',
value: ChainType.CORETIME,
},

{
icon: RegionXIcon,
label: 'RegionX Chain',
value: ChainType.REGIONX,
},
];
return (
<FormControl fullWidth>
<InputLabel id='origin-selector-label'>Chain</InputLabel>
Expand All @@ -14,10 +46,22 @@ export const ChainSelector = ({ chain, setChain }: ChainSelectorProps) => {
id='origin-selector'
value={chain}
label='Origin'
onChange={(e) => setChain(e.target.value)}
onChange={(e) => setChain(e.target.value as ChainType)}
>
<MenuItem value='CoretimeChain'>Coretime Chain</MenuItem>
<MenuItem value='RegionXChain'>RegionX Chain</MenuItem>
{menuItems.map(({ icon, label, value }, index) => (
<MenuItem value={value} key={index}>
<Box sx={{ display: 'flex', gap: '0.5rem' }}>
<Image
src={icon}
alt='icon'
style={{ width: '2rem', height: '2rem', borderRadius: '100%' }}
/>
<Typography sx={{ lineHeight: 1.5, fontSize: '1.25rem' }}>
{label}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
);
Expand Down
5 changes: 5 additions & 0 deletions src/components/Layout/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@
flex-grow: 1;
min-height: calc(100vh - 9rem);
max-height: calc(100vh - 9rem);
overflow-y: scroll;
}

.main::-webkit-scrollbar {
display: none;
}
65 changes: 48 additions & 17 deletions src/hooks/balance.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,73 @@
import { ApiPromise } from '@polkadot/api';
import { useInkathon } from '@scio-labs/use-inkathon';
import { useCallback, useEffect, useState } from 'react';

import { useCoretimeApi } from '@/contexts/apis';
import { parseHNString } from '@/utils/functions';

import { useCoretimeApi, useRelayApi } from '@/contexts/apis';
import { ApiState } from '@/contexts/apis/types';
import { useToast } from '@/contexts/toast';

// React hook for fetching balance.
const useBalance = () => {
const {
state: { api, apiState, symbol },
state: { api: coretimeApi, apiState: coretimeApiState, symbol },
} = useCoretimeApi();
const {
state: { api: relayApi, apiState: relayApiState },
} = useRelayApi();

const { activeAccount } = useInkathon();

const [balance, setBalance] = useState(0);
const [coretimeBalance, setCoretimeBalance] = useState(0);
const [relayBalance, setRelayBalance] = useState(0);

const { toastWarning } = useToast();

const fetchBalance = useCallback(async () => {
if (api && apiState == ApiState.READY && activeAccount) {
const fetchBalance = useCallback(
async (api: ApiPromise): Promise<number | undefined> => {
if (!activeAccount) return;

const accountData: any = (
await api.query.system.account(activeAccount.address)
).toHuman();
const balance = parseFloat(accountData.data.free.toString());
setBalance(balance);

if (balance === 0) {
toastWarning(
`The selected account does not have any ${symbol} tokens on the Coretime chain.`
);
}
const balance = parseHNString(accountData.data.free.toString());

return balance;
},
[activeAccount]
);

const fetchBalances = useCallback(() => {
if (coretimeApi && coretimeApiState == ApiState.READY) {
fetchBalance(coretimeApi).then((balance) => {
balance !== undefined && setCoretimeBalance(balance);
balance === 0 &&
toastWarning(
`The selected account does not have any ${symbol} tokens on the Coretime chain.`
);
});
}
if (relayApi && relayApiState == ApiState.READY) {
fetchBalance(relayApi).then((balance) => {
balance !== undefined && setRelayBalance(balance);
});
}
}, [api, apiState, activeAccount, toastWarning, symbol]);
}, [
fetchBalance,
toastWarning,
symbol,
coretimeApi,
coretimeApiState,
relayApi,
relayApiState,
]);

useEffect(() => {
fetchBalance();
}, [fetchBalance]);
fetchBalances();
});

return balance;
return { coretimeBalance, relayBalance, fetchBalances };
};

export default useBalance;
21 changes: 21 additions & 0 deletions src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ export type ParaId = number;

export type BlockNumber = number;

export enum AssetType {
// eslint-disable-next-line no-unused-vars
NONE = 0,
// eslint-disable-next-line no-unused-vars
TOKEN = 1,
// eslint-disable-next-line no-unused-vars
REGION = 2,
}

export enum ChainType {
// eslint-disable-next-line no-unused-vars
NONE = 0,
// eslint-disable-next-line no-unused-vars
CORETIME = 1,
// eslint-disable-next-line no-unused-vars
RELAY = 2,
// eslint-disable-next-line no-unused-vars
REGIONX = 3,
}

export type Sender = {
address: string;
signer: Signer;
Expand All @@ -27,6 +47,7 @@ export type TxStatusHandlers = {
finalized: () => void;
success: () => void;
error: () => void;
finally?: () => void;
};

export enum RegionLocation {
Expand Down
53 changes: 20 additions & 33 deletions src/pages/purchase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { useState } from 'react';
import useBalance from '@/hooks/balance';
import useSalePhase from '@/hooks/salePhase';
import useSalePrice from '@/hooks/salePrice';
import { formatBalance } from '@/utils/functions';
import { sendTx } from '@/utils/functions';

import { CoreDetailsPanel, ProgressButton, SaleInfoPanel } from '@/components';
import Balance from '@/components/Elements/Balance';

import { useCoretimeApi } from '@/contexts/apis';
import { ApiState } from '@/contexts/apis/types';
Expand All @@ -35,7 +36,7 @@ const Purchase = () => {

const { fetchRegions } = useRegions();

const balance = useBalance();
const { coretimeBalance, relayBalance } = useBalance();
const currentPrice = useSalePrice();
const { currentPhase, progress, saleStartTimestamp, saleEndTimestamp } =
useSalePhase();
Expand All @@ -46,31 +47,18 @@ const Purchase = () => {

const txPurchase = api.tx.broker.purchase(currentPrice);

try {
setWorking(true);
await txPurchase.signAndSend(
activeAccount.address,
{ signer: activeSigner },
({ status, events }) => {
if (status.isReady) toastInfo('Transaction was initiated');
else if (status.isInBlock) toastInfo(`In Block`);
else if (status.isFinalized) {
setWorking(false);
events.forEach(({ event: { method } }) => {
if (method === 'ExtrinsicSuccess') {
toastSuccess('Transaction successful');
fetchRegions();
} else if (method === 'ExtrinsicFailed') {
toastError(`Failed to purchase the region`);
}
});
}
}
);
} catch (e) {
toastError(`Failed to purchase the region. ${e}`);
setWorking(false);
}
sendTx(txPurchase, activeAccount.address, activeSigner, {
ready: () => toastInfo('Transaction was initiated'),
inBlock: () => toastInfo(`In Block`),
finalized: () => setWorking(false),
success: () => {
toastSuccess('Transaction successful');
fetchRegions();
},
error: () => {
toastError(`Failed to purchase the region`);
},
});
};

return (
Expand All @@ -96,12 +84,11 @@ const Purchase = () => {
Buy a core straight from the Coretime chain
</Typography>
</Box>
<Typography variant='h6' sx={{ color: theme.palette.text.primary }}>
{`Your balance: ${formatBalance(
balance.toString(),
false
)} ${symbol}`}
</Typography>
<Balance
coretimeBalance={coretimeBalance}
relayBalance={relayBalance}
symbol={symbol}
/>
</Box>
<Box>
{loading ||
Expand Down
Loading

0 comments on commit 20e9299

Please sign in to comment.