mirror of https://github.com/certusone/oyster.git
Added pair context state for chain and amount management
This commit is contained in:
parent
85c9fefe83
commit
a4a9895786
|
@ -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' }}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
connection: Connection,
|
||||
) => {
|
||||
debugger;
|
||||
//debugger;
|
||||
const tokenAccounts = await connection
|
||||
.getTokenAccountsByOwner(authorityKey, {
|
||||
programId: programIds().token,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
};
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue