mirror of https://github.com/certusone/oyster.git
Merge pull request #19 from yamijuan/transfer-ui
Added pair context state for chain and amount management
This commit is contained in:
commit
6768764fc0
|
@ -15,7 +15,6 @@ export const TokenSelectModal = (props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { tokens } = useEthereum();
|
const { tokens } = useEthereum();
|
||||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
|
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
|
||||||
const [selected, setSelected] = useState<string>('');
|
|
||||||
const [search, setSearch] = useState<string>('');
|
const [search, setSearch] = useState<string>('');
|
||||||
|
|
||||||
const tokenList = useMemo(() => {
|
const tokenList = useMemo(() => {
|
||||||
|
@ -38,11 +37,8 @@ export const TokenSelectModal = (props: {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
};
|
};
|
||||||
const firstToken = useMemo(() => {
|
const firstToken = useMemo(() => {
|
||||||
if (!selected) {
|
return tokens.find(el => el.address === props.asset);
|
||||||
return tokens.find(el => el.address === props.asset);
|
}, [tokens, props.asset]);
|
||||||
}
|
|
||||||
return tokens.find(el => el.address === selected);
|
|
||||||
}, [selected, tokens, props.asset]);
|
|
||||||
|
|
||||||
const delayedSearchChange = _.debounce(val => {
|
const delayedSearchChange = _.debounce(val => {
|
||||||
setSearch(val);
|
setSearch(val);
|
||||||
|
@ -58,7 +54,6 @@ export const TokenSelectModal = (props: {
|
||||||
title={token.name}
|
title={token.name}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onSelectToken(mint);
|
props.onSelectToken(mint);
|
||||||
setSelected(mint);
|
|
||||||
hideModal();
|
hideModal();
|
||||||
}}
|
}}
|
||||||
style={{ ...rowProps.style, cursor: 'pointer' }}
|
style={{ ...rowProps.style, cursor: 'pointer' }}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { TokenDisplay } from './../TokenDisplay';
|
||||||
import { WrappedAssetFactory } from '../../contracts/WrappedAssetFactory';
|
import { WrappedAssetFactory } from '../../contracts/WrappedAssetFactory';
|
||||||
import { WormholeFactory } from '../../contracts/WormholeFactory';
|
import { WormholeFactory } from '../../contracts/WormholeFactory';
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
|
import { useTokenChainPairState } from '../../contexts/chainPair';
|
||||||
|
|
||||||
const { useConnection } = contexts.Connection;
|
const { useConnection } = contexts.Connection;
|
||||||
const { useWallet } = contexts.Wallet;
|
const { useWallet } = contexts.Wallet;
|
||||||
|
@ -40,27 +41,45 @@ export const Transfer = () => {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const { wallet } = useWallet();
|
const { wallet } = useWallet();
|
||||||
const { provider, tokenMap, tokens } = useEthereum();
|
const { provider, tokenMap, tokens } = useEthereum();
|
||||||
|
const {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
mintAddress,
|
||||||
|
setMintAddress,
|
||||||
|
setLastTypedAccount,
|
||||||
|
} = useTokenChainPairState();
|
||||||
const [request, setRequest] = useState<TransferRequest>({
|
const [request, setRequest] = useState<TransferRequest>({
|
||||||
from: ASSET_CHAIN.Ethereum,
|
from: ASSET_CHAIN.Ethereum,
|
||||||
toChain: ASSET_CHAIN.Solana,
|
toChain: ASSET_CHAIN.Solana,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tokens && !request.asset) {
|
if (mintAddress && !request.asset) {
|
||||||
setRequest({
|
setRequest({
|
||||||
...request,
|
...request,
|
||||||
asset: tokens?.[0]?.address,
|
asset: mintAddress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [request, tokens, setRequest]);
|
}, [mintAddress]);
|
||||||
|
|
||||||
const setAssetInformation = async (asset: string) => {
|
const setAssetInformation = async (asset: string) => {
|
||||||
|
setMintAddress(asset);
|
||||||
setRequest({
|
setRequest({
|
||||||
...request,
|
...request,
|
||||||
asset,
|
asset: asset,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRequest({
|
||||||
|
...request,
|
||||||
|
amount: A.amount,
|
||||||
|
asset: mintAddress,
|
||||||
|
from: A.chain,
|
||||||
|
toChain: B.chain,
|
||||||
|
});
|
||||||
|
}, [A, B, mintAddress]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const asset = request.asset;
|
const asset = request.asset;
|
||||||
if (!asset || asset === request?.info?.address) {
|
if (!asset || asset === request?.info?.address) {
|
||||||
|
@ -125,29 +144,26 @@ export const Transfer = () => {
|
||||||
<Input
|
<Input
|
||||||
title={`From ${chainToName(request.from)}`}
|
title={`From ${chainToName(request.from)}`}
|
||||||
asset={request.asset}
|
asset={request.asset}
|
||||||
chain={request.from}
|
|
||||||
balance={request.info?.balanceAsNumber || 0}
|
balance={request.info?.balanceAsNumber || 0}
|
||||||
setAsset={asset => setAssetInformation(asset)}
|
setAsset={asset => setAssetInformation(asset)}
|
||||||
amount={request.amount}
|
chain={A.chain}
|
||||||
|
amount={A.amount}
|
||||||
onInputChange={amount => {
|
onInputChange={amount => {
|
||||||
setRequest({
|
setLastTypedAccount(A.chain);
|
||||||
...request,
|
A.setAmount(amount || 0);
|
||||||
amount: amount || 0,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
className="swap-button"
|
className="swap-button"
|
||||||
disabled={true}
|
disabled={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const from = request.toChain;
|
const from = A.chain;
|
||||||
const toChain = request.from;
|
const toChain = B.chain;
|
||||||
setRequest({
|
if (from !== undefined && toChain !== undefined) {
|
||||||
...request,
|
A.setChain(toChain);
|
||||||
from,
|
B.setChain(from);
|
||||||
toChain,
|
}
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⇅
|
⇅
|
||||||
|
@ -155,14 +171,12 @@ export const Transfer = () => {
|
||||||
<Input
|
<Input
|
||||||
title={`To ${chainToName(request.toChain)}`}
|
title={`To ${chainToName(request.toChain)}`}
|
||||||
asset={request.asset}
|
asset={request.asset}
|
||||||
chain={request.toChain}
|
|
||||||
setAsset={asset => setAssetInformation(asset)}
|
setAsset={asset => setAssetInformation(asset)}
|
||||||
amount={request.amount}
|
chain={B.chain}
|
||||||
|
amount={B.amount}
|
||||||
onInputChange={amount => {
|
onInputChange={amount => {
|
||||||
setRequest({
|
setLastTypedAccount(B.chain);
|
||||||
...request,
|
B.setAmount(amount || 0);
|
||||||
amount: amount || 0,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -182,7 +182,7 @@ const queryCustodyAccounts = async (
|
||||||
authorityKey: PublicKey,
|
authorityKey: PublicKey,
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
) => {
|
) => {
|
||||||
debugger;
|
//debugger;
|
||||||
const tokenAccounts = await connection
|
const tokenAccounts = await connection
|
||||||
.getTokenAccountsByOwner(authorityKey, {
|
.getTokenAccountsByOwner(authorityKey, {
|
||||||
programId: programIds().token,
|
programId: programIds().token,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { FaucetView, HomeView, TransferView } from './views';
|
||||||
import { CoingeckoProvider } from './contexts/coingecko';
|
import { CoingeckoProvider } from './contexts/coingecko';
|
||||||
import { BridgeProvider } from './contexts/bridge';
|
import { BridgeProvider } from './contexts/bridge';
|
||||||
import { UseWalletProvider } from 'use-wallet';
|
import { UseWalletProvider } from 'use-wallet';
|
||||||
|
import { TokenChainPairProvider } from './contexts/chainPair';
|
||||||
const { WalletProvider } = contexts.Wallet;
|
const { WalletProvider } = contexts.Wallet;
|
||||||
const { ConnectionProvider } = contexts.Connection;
|
const { ConnectionProvider } = contexts.Connection;
|
||||||
const { AccountsProvider } = contexts.Accounts;
|
const { AccountsProvider } = contexts.Accounts;
|
||||||
|
@ -28,21 +29,23 @@ export function Routes() {
|
||||||
<AccountsProvider>
|
<AccountsProvider>
|
||||||
<MarketProvider>
|
<MarketProvider>
|
||||||
<CoingeckoProvider>
|
<CoingeckoProvider>
|
||||||
<AppLayout>
|
<TokenChainPairProvider>
|
||||||
<Switch>
|
<AppLayout>
|
||||||
<Route
|
<Switch>
|
||||||
exact
|
<Route
|
||||||
path="/"
|
exact
|
||||||
component={() => <HomeView />}
|
path="/"
|
||||||
/>
|
component={() => <HomeView />}
|
||||||
<Route path="/move" children={<TransferView />} />
|
/>
|
||||||
<Route
|
<Route path="/move" children={<TransferView />} />
|
||||||
exact
|
<Route
|
||||||
path="/faucet"
|
exact
|
||||||
children={<FaucetView />}
|
path="/faucet"
|
||||||
/>
|
children={<FaucetView />}
|
||||||
</Switch>
|
/>
|
||||||
</AppLayout>
|
</Switch>
|
||||||
|
</AppLayout>
|
||||||
|
</TokenChainPairProvider>
|
||||||
</CoingeckoProvider>
|
</CoingeckoProvider>
|
||||||
</MarketProvider>
|
</MarketProvider>
|
||||||
</AccountsProvider>
|
</AccountsProvider>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import './itemStyle.less';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useWormholeAccounts } from '../../hooks/useWormholeAccounts';
|
import { useWormholeAccounts } from '../../hooks/useWormholeAccounts';
|
||||||
import { TokenDisplay } from '../../components/TokenDisplay';
|
import { TokenDisplay } from '../../components/TokenDisplay';
|
||||||
|
import { toChainSymbol } from '../../contexts/chainPair';
|
||||||
|
|
||||||
export const HomeView = () => {
|
export const HomeView = () => {
|
||||||
const {
|
const {
|
||||||
|
@ -25,12 +26,18 @@ export const HomeView = () => {
|
||||||
style: {},
|
style: {},
|
||||||
},
|
},
|
||||||
children: (
|
children: (
|
||||||
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
|
<Link
|
||||||
{record.logo && (
|
to={`/move?from=${toChainSymbol(record.chain)}&token=${
|
||||||
<TokenDisplay logo={record.logo} chain={record.chain} />
|
record.symbol
|
||||||
)}{' '}
|
}`}
|
||||||
{record.symbol}
|
>
|
||||||
</span>
|
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||||
|
{record.logo && (
|
||||||
|
<TokenDisplay logo={record.logo} chain={record.chain} />
|
||||||
|
)}{' '}
|
||||||
|
{record.symbol}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue