Merge pull request #19 from yamijuan/transfer-ui

Added pair context state for chain and amount management
This commit is contained in:
B 2021-03-10 17:24:33 -06:00 committed by GitHub
commit 6768764fc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 289 additions and 53 deletions

View File

@ -15,7 +15,6 @@ export const TokenSelectModal = (props: {
}) => {
const { tokens } = useEthereum();
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const [selected, setSelected] = useState<string>('');
const [search, setSearch] = useState<string>('');
const tokenList = useMemo(() => {
@ -38,11 +37,8 @@ export const TokenSelectModal = (props: {
setIsModalVisible(false);
};
const firstToken = useMemo(() => {
if (!selected) {
return tokens.find(el => el.address === props.asset);
}
return tokens.find(el => el.address === selected);
}, [selected, tokens, props.asset]);
return tokens.find(el => el.address === props.asset);
}, [tokens, props.asset]);
const delayedSearchChange = _.debounce(val => {
setSearch(val);
@ -58,7 +54,6 @@ export const TokenSelectModal = (props: {
title={token.name}
onClick={() => {
props.onSelectToken(mint);
setSelected(mint);
hideModal();
}}
style={{ ...rowProps.style, cursor: 'pointer' }}

View File

@ -16,6 +16,7 @@ import { TokenDisplay } from './../TokenDisplay';
import { WrappedAssetFactory } from '../../contracts/WrappedAssetFactory';
import { WormholeFactory } from '../../contracts/WormholeFactory';
import BN from 'bn.js';
import { useTokenChainPairState } from '../../contexts/chainPair';
const { useConnection } = contexts.Connection;
const { useWallet } = contexts.Wallet;
@ -40,27 +41,45 @@ export const Transfer = () => {
const connection = useConnection();
const { wallet } = useWallet();
const { provider, tokenMap, tokens } = useEthereum();
const {
A,
B,
mintAddress,
setMintAddress,
setLastTypedAccount,
} = useTokenChainPairState();
const [request, setRequest] = useState<TransferRequest>({
from: ASSET_CHAIN.Ethereum,
toChain: ASSET_CHAIN.Solana,
});
useEffect(() => {
if (tokens && !request.asset) {
if (mintAddress && !request.asset) {
setRequest({
...request,
asset: tokens?.[0]?.address,
asset: mintAddress,
});
}
}, [request, tokens, setRequest]);
}, [mintAddress]);
const setAssetInformation = async (asset: string) => {
setMintAddress(asset);
setRequest({
...request,
asset,
asset: asset,
});
};
useEffect(() => {
setRequest({
...request,
amount: A.amount,
asset: mintAddress,
from: A.chain,
toChain: B.chain,
});
}, [A, B, mintAddress]);
useEffect(() => {
const asset = request.asset;
if (!asset || asset === request?.info?.address) {
@ -125,29 +144,26 @@ export const Transfer = () => {
<Input
title={`From ${chainToName(request.from)}`}
asset={request.asset}
chain={request.from}
balance={request.info?.balanceAsNumber || 0}
setAsset={asset => setAssetInformation(asset)}
amount={request.amount}
chain={A.chain}
amount={A.amount}
onInputChange={amount => {
setRequest({
...request,
amount: amount || 0,
});
setLastTypedAccount(A.chain);
A.setAmount(amount || 0);
}}
/>
<Button
type="primary"
className="swap-button"
disabled={true}
disabled={false}
onClick={() => {
const from = request.toChain;
const toChain = request.from;
setRequest({
...request,
from,
toChain,
});
const from = A.chain;
const toChain = B.chain;
if (from !== undefined && toChain !== undefined) {
A.setChain(toChain);
B.setChain(from);
}
}}
>
@ -155,14 +171,12 @@ export const Transfer = () => {
<Input
title={`To ${chainToName(request.toChain)}`}
asset={request.asset}
chain={request.toChain}
setAsset={asset => setAssetInformation(asset)}
amount={request.amount}
chain={B.chain}
amount={B.amount}
onInputChange={amount => {
setRequest({
...request,
amount: amount || 0,
});
setLastTypedAccount(B.chain);
B.setAmount(amount || 0);
}}
/>
</div>

View File

@ -0,0 +1,217 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import bs58 from 'bs58';
import { useConnection } from '@oyster/common';
import { TokenInfo } from '@solana/spl-token-registry';
import { ASSET_CHAIN } from '../utils/assets';
import { useEthereum } from './ethereum';
export interface TokenChainContextState {
amount: number;
setAmount: (val: number) => void;
chain: ASSET_CHAIN;
setChain: (val: number) => void;
}
export interface TokenChainPairContextState {
A: TokenChainContextState;
B: TokenChainContextState;
mintAddress: string;
setMintAddress: (mintAddress: string) => void;
lastTypedAccount: number;
setLastTypedAccount: (chain: ASSET_CHAIN) => void;
}
const TokenChainPairContext = React.createContext<TokenChainPairContextState | null>(
null,
);
const isValidAddress = (address: string) => {
try {
const decoded = bs58.decode(address);
return decoded.length === 32;
} catch {
return false;
}
};
export const toChainSymbol = (chain: number | null) => {
if (chain === ASSET_CHAIN.Solana) {
return 'SOL';
}
return 'ETH';
};
function getDefaultTokens(tokens: TokenInfo[], search: string) {
let defaultChain = 'ETH';
let defaultToken = 'SRM';
const nameToToken = tokens.reduce((map, item) => {
map.set(item.symbol, item);
return map;
}, new Map<string, any>());
if (search) {
const urlParams = new URLSearchParams(search);
const from = urlParams.get('from');
defaultChain = from === 'SOL' ? from : 'ETH';
const token = urlParams.get('token') || 'SRM';
if (nameToToken.has(token) || isValidAddress(token)) {
defaultToken = token;
}
}
return {
defaultChain,
defaultToken,
};
}
export const useCurrencyLeg = () => {
const [amount, setAmount] = useState(0);
const [chain, setChain] = useState(ASSET_CHAIN.Ethereum);
return useMemo(
() => ({
amount: amount,
setAmount: setAmount,
chain: chain,
setChain: setChain,
}),
[amount, setAmount, chain, setChain],
);
};
export function TokenChainPairProvider({ children = null as any }) {
const { tokens } = useEthereum();
const history = useHistory();
const location = useLocation();
const [lastTypedAccount, setLastTypedAccount] = useState(0);
const [mintAddress, setMintAddress] = useState('');
const base = useCurrencyLeg();
const amountA = base.amount;
const setAmountA = base.setAmount;
const chainA = base.chain;
const setChainA = base.setChain;
const quote = useCurrencyLeg();
const amountB = quote.amount;
const setAmountB = quote.setAmount;
const chainB = quote.chain;
const setChainB = quote.setChain;
// updates browser history on token changes
useEffect(() => {
// set history
const token =
tokens.find(t => t.address === mintAddress)?.symbol || mintAddress;
if (token && chainA) {
history.push({
search: `?from=${toChainSymbol(chainA)}&token=${token}`,
});
} else {
if (mintAddress) {
history.push({
search: ``,
});
} else {
return;
}
}
}, [mintAddress, tokens, chainA]);
// Updates tokens on location change
useEffect(() => {
if (
(!location.search && mintAddress) ||
location.pathname.indexOf('move') < 0
) {
return;
}
let { defaultChain, defaultToken } = getDefaultTokens(
tokens,
location.search,
);
if (!defaultToken || !defaultChain) {
return;
}
setChainA(
defaultChain === 'ETH' ? ASSET_CHAIN.Ethereum : ASSET_CHAIN.Solana,
);
setChainB(
defaultChain === 'SOL' ? ASSET_CHAIN.Ethereum : ASSET_CHAIN.Solana,
);
setMintAddress(
tokens.find(t => t.symbol === defaultToken)?.address ||
(isValidAddress(defaultToken) ? defaultToken : '') ||
'',
);
// mintAddressA and mintAddressB are not included here to prevent infinite loop
// eslint-disable-next-line
}, [
location,
location.search,
location.pathname,
setMintAddress,
tokens,
setChainA,
setChainB,
]);
const calculateDependent = useCallback(async () => {
if (mintAddress) {
let setDependent;
let amount;
if (lastTypedAccount === base.chain) {
setDependent = setAmountB;
amount = amountA;
} else {
setDependent = setAmountA;
amount = amountB;
}
const result: number | string = amount;
if (typeof result === 'string') {
setDependent(parseFloat(result));
} else if (result !== undefined && Number.isFinite(result)) {
setDependent(result);
} else {
setDependent(0);
}
}
}, [mintAddress, setAmountA, setAmountB, amountA, amountB, lastTypedAccount]);
useEffect(() => {
calculateDependent();
}, [amountB, amountA, lastTypedAccount, calculateDependent]);
return (
<TokenChainPairContext.Provider
value={{
A: base,
B: quote,
mintAddress,
setMintAddress,
lastTypedAccount,
setLastTypedAccount,
}}
>
{children}
</TokenChainPairContext.Provider>
);
}
export const useTokenChainPairState = () => {
const context = useContext(TokenChainPairContext);
return context as TokenChainPairContextState;
};

View File

@ -182,7 +182,7 @@ const queryCustodyAccounts = async (
authorityKey: PublicKey,
connection: Connection,
) => {
debugger;
//debugger;
const tokenAccounts = await connection
.getTokenAccountsByOwner(authorityKey, {
programId: programIds().token,

View File

@ -12,6 +12,7 @@ import { FaucetView, HomeView, TransferView } from './views';
import { CoingeckoProvider } from './contexts/coingecko';
import { BridgeProvider } from './contexts/bridge';
import { UseWalletProvider } from 'use-wallet';
import { TokenChainPairProvider } from './contexts/chainPair';
const { WalletProvider } = contexts.Wallet;
const { ConnectionProvider } = contexts.Connection;
const { AccountsProvider } = contexts.Accounts;
@ -28,21 +29,23 @@ export function Routes() {
<AccountsProvider>
<MarketProvider>
<CoingeckoProvider>
<AppLayout>
<Switch>
<Route
exact
path="/"
component={() => <HomeView />}
/>
<Route path="/move" children={<TransferView />} />
<Route
exact
path="/faucet"
children={<FaucetView />}
/>
</Switch>
</AppLayout>
<TokenChainPairProvider>
<AppLayout>
<Switch>
<Route
exact
path="/"
component={() => <HomeView />}
/>
<Route path="/move" children={<TransferView />} />
<Route
exact
path="/faucet"
children={<FaucetView />}
/>
</Switch>
</AppLayout>
</TokenChainPairProvider>
</CoingeckoProvider>
</MarketProvider>
</AccountsProvider>

View File

@ -6,6 +6,7 @@ import './itemStyle.less';
import { Link } from 'react-router-dom';
import { useWormholeAccounts } from '../../hooks/useWormholeAccounts';
import { TokenDisplay } from '../../components/TokenDisplay';
import { toChainSymbol } from '../../contexts/chainPair';
export const HomeView = () => {
const {
@ -25,12 +26,18 @@ export const HomeView = () => {
style: {},
},
children: (
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
{record.logo && (
<TokenDisplay logo={record.logo} chain={record.chain} />
)}{' '}
{record.symbol}
</span>
<Link
to={`/move?from=${toChainSymbol(record.chain)}&token=${
record.symbol
}`}
>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
{record.logo && (
<TokenDisplay logo={record.logo} chain={record.chain} />
)}{' '}
{record.symbol}
</span>
</Link>
),
};
},