bridge_ui: v1 safety checks, wallet desync fix, basic terra token picker
Change-Id: I9e45ce77c573e6940e6280b52ab2a319e6c4472f
This commit is contained in:
parent
c47d32ba9c
commit
fc300f47e6
|
@ -18,6 +18,8 @@ import {
|
||||||
} from "../../utils/ethereum";
|
} from "../../utils/ethereum";
|
||||||
import { shortenAddress } from "../../utils/solana";
|
import { shortenAddress } from "../../utils/solana";
|
||||||
import OffsetButton from "./OffsetButton";
|
import OffsetButton from "./OffsetButton";
|
||||||
|
import { WormholeAbi__factory } from "@certusone/wormhole-sdk/lib/ethers-contracts/abi";
|
||||||
|
import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts";
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
const useStyles = makeStyles(() =>
|
||||||
createStyles({
|
createStyles({
|
||||||
|
@ -34,6 +36,14 @@ const useStyles = makeStyles(() =>
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isWormholev1 = (provider: any, address: string) => {
|
||||||
|
const connection = WormholeAbi__factory.connect(
|
||||||
|
WORMHOLE_V1_ETH_ADDRESS,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
return connection.isWrappedAsset(address);
|
||||||
|
};
|
||||||
|
|
||||||
type EthereumSourceTokenSelectorProps = {
|
type EthereumSourceTokenSelectorProps = {
|
||||||
value: ParsedTokenAccount | null;
|
value: ParsedTokenAccount | null;
|
||||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||||
|
@ -79,18 +89,64 @@ export default function EthereumSourceTokenSelector(
|
||||||
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
const [advancedModeSymbol, setAdvancedModeSymbol] = useState("");
|
||||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||||
|
|
||||||
|
const [autocompleteHolder, setAutocompleteHolder] =
|
||||||
|
useState<ParsedTokenAccount | null>(null);
|
||||||
|
const [autocompleteError, setAutocompleteError] = useState("");
|
||||||
|
|
||||||
const { provider, signerAddress } = useEthereumProvider();
|
const { provider, signerAddress } = useEthereumProvider();
|
||||||
|
|
||||||
|
// const wrappedTestToken = "0x8bf3c393b588bb6ad021e154654493496139f06d";
|
||||||
|
// const notWrappedTestToken = "0xaaaebe6fe48e54f431b0c390cfaf0b017d09d42d";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//If we receive a push from our parent, usually on component mount, we set the advancedModeString to synchronize.
|
//If we receive a push from our parent, usually on component mount, we set our internal value to synchronize
|
||||||
//This also kicks off the metadata load.
|
//This also kicks off the metadata load.
|
||||||
if (advancedMode && value && advancedModeHolderString !== value.mintKey) {
|
if (advancedMode && value && advancedModeHolderString !== value.mintKey) {
|
||||||
setAdvancedModeHolderString(value.mintKey);
|
setAdvancedModeHolderString(value.mintKey);
|
||||||
}
|
}
|
||||||
}, [value, advancedMode, advancedModeHolderString]);
|
if (!advancedMode && value && !autocompleteHolder) {
|
||||||
|
setAutocompleteHolder(value);
|
||||||
|
}
|
||||||
|
}, [value, advancedMode, advancedModeHolderString, autocompleteHolder]);
|
||||||
|
|
||||||
//This loads the parsedTokenAccount & symbol from the advancedModeString
|
//This effect is watching the autocomplete selection.
|
||||||
//TODO move to util or hook
|
//It checks to make sure the token is a valid choice before putting it on the state.
|
||||||
|
//At present, that just means it can't be wormholev1
|
||||||
|
useEffect(() => {
|
||||||
|
if (advancedMode || !autocompleteHolder || !provider) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
let cancelled = false;
|
||||||
|
setAutocompleteError("");
|
||||||
|
isWormholev1(provider, autocompleteHolder.mintKey).then(
|
||||||
|
(result) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
result
|
||||||
|
? setAutocompleteError(
|
||||||
|
"Wormhole v1 tokens cannot be transferred with this bridge."
|
||||||
|
)
|
||||||
|
: onChange(autocompleteHolder);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.log(error);
|
||||||
|
if (!cancelled) {
|
||||||
|
setAutocompleteError(
|
||||||
|
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
||||||
|
);
|
||||||
|
onChange(autocompleteHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [autocompleteHolder, provider, advancedMode, onChange]);
|
||||||
|
|
||||||
|
//This effect watches the advancedModeString, and checks that the selected asset is valid before putting
|
||||||
|
// it on the state.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
if (!advancedMode || !isValidEthereumAddress(advancedModeHolderString)) {
|
if (!advancedMode || !isValidEthereumAddress(advancedModeHolderString)) {
|
||||||
|
@ -106,7 +162,36 @@ export default function EthereumSourceTokenSelector(
|
||||||
!cancelled && setAdvancedModeError("");
|
!cancelled && setAdvancedModeError("");
|
||||||
!cancelled && setAdvancedModeSymbol("");
|
!cancelled && setAdvancedModeSymbol("");
|
||||||
try {
|
try {
|
||||||
getEthereumToken(advancedModeHolderString, provider).then((token) => {
|
//Validate that the token is not a wormhole v1 asset
|
||||||
|
const isWormholePromise = isWormholev1(
|
||||||
|
provider,
|
||||||
|
advancedModeHolderString
|
||||||
|
).then(
|
||||||
|
(result) => {
|
||||||
|
if (result && !cancelled) {
|
||||||
|
setAdvancedModeError(
|
||||||
|
"Wormhole v1 assets are not eligible for transfer."
|
||||||
|
);
|
||||||
|
setAdvancedModeLoading(false);
|
||||||
|
return Promise.reject();
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
!cancelled &&
|
||||||
|
setAdvancedModeError(
|
||||||
|
"Warning: please verify if this is a Wormhole v1 token address. V1 tokens should not be transferred with this bridge"
|
||||||
|
);
|
||||||
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
|
return Promise.resolve(); //Don't allow an error here to tank the workflow
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
//Then fetch the asset's information & transform to a parsed token account
|
||||||
|
isWormholePromise.then(() =>
|
||||||
|
getEthereumToken(advancedModeHolderString, provider).then(
|
||||||
|
(token) => {
|
||||||
ethTokenToParsedTokenAccount(token, signerAddress).then(
|
ethTokenToParsedTokenAccount(token, signerAddress).then(
|
||||||
(parsedTokenAccount) => {
|
(parsedTokenAccount) => {
|
||||||
!cancelled && onChange(parsedTokenAccount);
|
!cancelled && onChange(parsedTokenAccount);
|
||||||
|
@ -115,23 +200,31 @@ export default function EthereumSourceTokenSelector(
|
||||||
(error) => {
|
(error) => {
|
||||||
//These errors can maybe be consolidated
|
//These errors can maybe be consolidated
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
setAdvancedModeError("Failed to find the specified address");
|
setAdvancedModeError(
|
||||||
|
"Failed to find the specified address"
|
||||||
|
);
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Also attempt to store off the symbol
|
||||||
token.symbol().then(
|
token.symbol().then(
|
||||||
(result) => {
|
(result) => {
|
||||||
!cancelled && setAdvancedModeSymbol(result);
|
!cancelled && setAdvancedModeSymbol(result);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
setAdvancedModeError("Failed to find the specified address");
|
setAdvancedModeError(
|
||||||
|
"Failed to find the specified address"
|
||||||
|
);
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
} catch (error) {
|
(error) => {}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
!cancelled &&
|
!cancelled &&
|
||||||
setAdvancedModeError("Failed to find the specified address");
|
setAdvancedModeError("Failed to find the specified address");
|
||||||
!cancelled && setAdvancedModeLoading(false);
|
!cancelled && setAdvancedModeLoading(false);
|
||||||
|
@ -162,7 +255,10 @@ export default function EthereumSourceTokenSelector(
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return covalent?.data?.find((x) => x.contract_address === account.mintKey);
|
const item = covalent?.data?.find(
|
||||||
|
(x) => x.contract_address === account.mintKey
|
||||||
|
);
|
||||||
|
return item ? item.contract_ticker_symbol : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterConfig = createFilterOptions({
|
const filterConfig = createFilterOptions({
|
||||||
|
@ -176,19 +272,21 @@ export default function EthereumSourceTokenSelector(
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleAdvancedMode = () => {
|
const toggleAdvancedMode = () => {
|
||||||
|
setAdvancedModeHolderString("");
|
||||||
|
setAdvancedModeError("");
|
||||||
|
setAdvancedModeSymbol("");
|
||||||
setAdvancedMode(!advancedMode);
|
setAdvancedMode(!advancedMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAutocompleteChange = (newValue: ParsedTokenAccount | null) => {
|
||||||
|
setAutocompleteHolder(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
props.covalent?.isFetching || props.tokenAccounts?.isFetching;
|
props.covalent?.isFetching || props.tokenAccounts?.isFetching;
|
||||||
|
|
||||||
const symbolString = advancedModeSymbol
|
|
||||||
? advancedModeSymbol + " "
|
|
||||||
: getSymbol(value)
|
|
||||||
? getSymbol(value)?.contract_ticker_symbol + " "
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const autoComplete = (
|
const autoComplete = (
|
||||||
|
<>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
autoComplete
|
autoComplete
|
||||||
autoHighlight
|
autoHighlight
|
||||||
|
@ -197,9 +295,9 @@ export default function EthereumSourceTokenSelector(
|
||||||
clearOnBlur
|
clearOnBlur
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
filterOptions={filterConfig}
|
filterOptions={filterConfig}
|
||||||
value={value}
|
value={autocompleteHolder}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
onChange(newValue);
|
handleAutocompleteChange(newValue);
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
noOptionsText={"No ERC20 tokens found at the moment."}
|
noOptionsText={"No ERC20 tokens found at the moment."}
|
||||||
|
@ -216,11 +314,15 @@ export default function EthereumSourceTokenSelector(
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
const symbol = getSymbol(option);
|
const symbol = getSymbol(option);
|
||||||
return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
|
return `${symbol ? symbol : "Unknown"} (Address: ${shortenAddress(
|
||||||
option.publicKey
|
option.mintKey
|
||||||
)}, Address: ${shortenAddress(option.mintKey)})`;
|
)})`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{autocompleteError && (
|
||||||
|
<Typography color="error">{autocompleteError}</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const advancedModeToggleButton = (
|
const advancedModeToggleButton = (
|
||||||
|
@ -229,12 +331,19 @@ export default function EthereumSourceTokenSelector(
|
||||||
</OffsetButton>
|
</OffsetButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const symbol = getSymbol(value) || advancedModeSymbol;
|
||||||
|
|
||||||
const content = value ? (
|
const content = value ? (
|
||||||
<>
|
<>
|
||||||
<Typography>{symbolString + value.mintKey}</Typography>
|
<Typography>{(symbol ? symbol + " " : "") + value.mintKey}</Typography>
|
||||||
<OffsetButton onClick={handleClick} disabled={disabled}>
|
<OffsetButton onClick={handleClick} disabled={disabled}>
|
||||||
Clear
|
Clear
|
||||||
</OffsetButton>
|
</OffsetButton>
|
||||||
|
{!advancedMode && autocompleteError ? (
|
||||||
|
<Typography color="error">{autocompleteError}</Typography>
|
||||||
|
) : advancedMode && advancedModeError ? (
|
||||||
|
<Typography color="error">{advancedModeError}</Typography>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : isLoading ? (
|
) : isLoading ? (
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||||
import { Autocomplete } from "@material-ui/lab";
|
import { Autocomplete } from "@material-ui/lab";
|
||||||
import { createFilterOptions } from "@material-ui/lab/Autocomplete";
|
import { createFilterOptions } from "@material-ui/lab/Autocomplete";
|
||||||
import { TokenInfo } from "@solana/spl-token-registry";
|
import { TokenInfo } from "@solana/spl-token-registry";
|
||||||
import React, { useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { DataWrapper } from "../../store/helpers";
|
import { DataWrapper } from "../../store/helpers";
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||||
|
import { WORMHOLE_V1_MINT_AUTHORITY } from "../../utils/consts";
|
||||||
import { Metadata } from "../../utils/metaplex";
|
import { Metadata } from "../../utils/metaplex";
|
||||||
import { shortenAddress } from "../../utils/solana";
|
import { shortenAddress } from "../../utils/solana";
|
||||||
|
|
||||||
|
@ -31,44 +32,7 @@ type SolanaSourceTokenSelectorProps = {
|
||||||
solanaTokenMap: DataWrapper<TokenInfo[]> | undefined;
|
solanaTokenMap: DataWrapper<TokenInfo[]> | undefined;
|
||||||
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
metaplexData: any; //DataWrapper<(Metadata | undefined)[]> | undefined | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
mintAccounts: DataWrapper<Map<String, string | null>> | undefined;
|
||||||
|
|
||||||
const renderAccount = (
|
|
||||||
account: ParsedTokenAccount,
|
|
||||||
solanaTokenMap: Map<String, TokenInfo>,
|
|
||||||
metaplexData: Map<String, Metadata>,
|
|
||||||
classes: any
|
|
||||||
) => {
|
|
||||||
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
|
||||||
const metaplexValue = metaplexData.get(account.mintKey);
|
|
||||||
|
|
||||||
const mintPrettyString = shortenAddress(account.mintKey);
|
|
||||||
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
|
||||||
const uri = tokenMapData?.logoURI || metaplexValue?.data?.uri || undefined;
|
|
||||||
const symbol =
|
|
||||||
tokenMapData?.symbol || metaplexValue?.data.symbol || "Unknown";
|
|
||||||
const name = tokenMapData?.name || metaplexValue?.data?.name || "--";
|
|
||||||
return (
|
|
||||||
<div className={classes.tokenOverviewContainer}>
|
|
||||||
<div>
|
|
||||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="subtitle1">{symbol}</Typography>
|
|
||||||
<Typography variant="subtitle2">{name}</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="body1">{"Mint : " + mintPrettyString}</Typography>
|
|
||||||
<Typography variant="body1">
|
|
||||||
{"Account :" + accountAddressPrettyString}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography variant="body2">{"Balance"}</Typography>
|
|
||||||
<Typography variant="h6">{account.uiAmountString}</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SolanaSourceTokenSelector(
|
export default function SolanaSourceTokenSelector(
|
||||||
|
@ -135,11 +99,95 @@ export default function SolanaSourceTokenSelector(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isWormholev1 = useCallback(
|
||||||
|
(address: string) => {
|
||||||
|
//This is a v1 wormhole token on testnet
|
||||||
|
//const testAddress = "4QixXecTZ4zdZGa39KH8gVND5NZ2xcaB12wiBhE4S7rn";
|
||||||
|
|
||||||
|
if (!props.mintAccounts?.data) {
|
||||||
|
return true; //These should never be null by this point
|
||||||
|
}
|
||||||
|
const mintInfo = props.mintAccounts.data.get(address);
|
||||||
|
|
||||||
|
if (!mintInfo) {
|
||||||
|
return true; //We should never fail to pull the mint of an account.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mintInfo === WORMHOLE_V1_MINT_AUTHORITY) {
|
||||||
|
return true; //This means the mint was created by the wormhole v1 contract, and we want to disallow its transfer.
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[props.mintAccounts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderAccount = (
|
||||||
|
account: ParsedTokenAccount,
|
||||||
|
solanaTokenMap: Map<String, TokenInfo>,
|
||||||
|
metaplexData: Map<String, Metadata>,
|
||||||
|
classes: any
|
||||||
|
) => {
|
||||||
|
const tokenMapData = solanaTokenMap.get(account.mintKey);
|
||||||
|
const metaplexValue = metaplexData.get(account.mintKey);
|
||||||
|
|
||||||
|
const mintPrettyString = shortenAddress(account.mintKey);
|
||||||
|
const accountAddressPrettyString = shortenAddress(account.publicKey);
|
||||||
|
const uri = tokenMapData?.logoURI || metaplexValue?.data?.uri || undefined;
|
||||||
|
const symbol =
|
||||||
|
tokenMapData?.symbol || metaplexValue?.data.symbol || "Unknown";
|
||||||
|
const name = tokenMapData?.name || metaplexValue?.data?.name || "--";
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<div className={classes.tokenOverviewContainer}>
|
||||||
|
<div>
|
||||||
|
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle1">{symbol}</Typography>
|
||||||
|
<Typography variant="subtitle2">{name}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{"Mint : " + mintPrettyString}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{"Account :" + accountAddressPrettyString}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2">{"Balance"}</Typography>
|
||||||
|
<Typography variant="h6">{account.uiAmountString}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const v1Warning = (
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Wormhole v1 tokens are not eligible for transfer.
|
||||||
|
</Typography>
|
||||||
|
<div>{content}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isWormholev1(account.mintKey) ? v1Warning : content;
|
||||||
|
};
|
||||||
|
|
||||||
//The autocomplete doesn't rerender the option label unless the value changes.
|
//The autocomplete doesn't rerender the option label unless the value changes.
|
||||||
//Thus we should wait for the metadata to arrive before rendering it.
|
//Thus we should wait for the metadata to arrive before rendering it.
|
||||||
//TODO This can flicker dependent on how fast the useEffects in the getSourceAccounts hook complete.
|
//TODO This can flicker dependent on how fast the useEffects in the getSourceAccounts hook complete.
|
||||||
const isLoading =
|
const isLoading =
|
||||||
props.metaplexData.isFetching || props.solanaTokenMap?.isFetching;
|
props.metaplexData.isFetching ||
|
||||||
|
props.solanaTokenMap?.isFetching ||
|
||||||
|
props.mintAccounts?.isFetching;
|
||||||
|
|
||||||
|
const accountLoadError =
|
||||||
|
!(props.mintAccounts?.isFetching || props.mintAccounts?.data) &&
|
||||||
|
"Unable to retrieve your token accounts";
|
||||||
|
const error = accountLoadError;
|
||||||
|
|
||||||
//This exists to remove NFTs from the list of potential options. It requires reading the metaplex data, so it would be
|
//This exists to remove NFTs from the list of potential options. It requires reading the metaplex data, so it would be
|
||||||
//difficult to do before this point.
|
//difficult to do before this point.
|
||||||
|
@ -152,6 +200,10 @@ export default function SolanaSourceTokenSelector(
|
||||||
});
|
});
|
||||||
}, [memoizedMetaplex, props.accounts]);
|
}, [memoizedMetaplex, props.accounts]);
|
||||||
|
|
||||||
|
const isOptionDisabled = useMemo(() => {
|
||||||
|
return (value: ParsedTokenAccount) => isWormholev1(value.mintKey);
|
||||||
|
}, [isWormholev1]);
|
||||||
|
|
||||||
const autoComplete = (
|
const autoComplete = (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
autoComplete
|
autoComplete
|
||||||
|
@ -178,6 +230,7 @@ export default function SolanaSourceTokenSelector(
|
||||||
classes
|
classes
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
getOptionDisabled={isOptionDisabled}
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
const symbol = getSymbol(option);
|
const symbol = getSymbol(option);
|
||||||
return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
|
return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
|
||||||
|
@ -190,6 +243,7 @@ export default function SolanaSourceTokenSelector(
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{isLoading ? <CircularProgress /> : autoComplete}
|
{isLoading ? <CircularProgress /> : autoComplete}
|
||||||
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { TextField, Typography } from "@material-ui/core";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
|
import useGetSourceParsedTokens from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||||
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import {
|
import {
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferSourceParsedTokenAccount,
|
selectTransferSourceParsedTokenAccount,
|
||||||
|
@ -15,6 +16,7 @@ import {
|
||||||
import {
|
import {
|
||||||
ParsedTokenAccount,
|
ParsedTokenAccount,
|
||||||
setSourceParsedTokenAccount,
|
setSourceParsedTokenAccount,
|
||||||
|
setSourceWalletAddress,
|
||||||
} from "../../store/transferSlice";
|
} from "../../store/transferSlice";
|
||||||
import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
|
import EthereumSourceTokenSelector from "./EthereumSourceTokenSelector";
|
||||||
import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
|
import SolanaSourceTokenSelector from "./SolanaSourceTokenSelector";
|
||||||
|
@ -32,13 +34,19 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
const sourceParsedTokenAccount = useSelector(
|
const sourceParsedTokenAccount = useSelector(
|
||||||
selectTransferSourceParsedTokenAccount
|
selectTransferSourceParsedTokenAccount
|
||||||
);
|
);
|
||||||
const handleSolanaOnChange = useCallback(
|
const walletIsReady = useIsWalletReady(lookupChain);
|
||||||
|
|
||||||
|
const handleOnChange = useCallback(
|
||||||
(newTokenAccount: ParsedTokenAccount | null) => {
|
(newTokenAccount: ParsedTokenAccount | null) => {
|
||||||
if (newTokenAccount !== undefined) {
|
if (!newTokenAccount) {
|
||||||
dispatch(setSourceParsedTokenAccount(newTokenAccount || undefined));
|
dispatch(setSourceParsedTokenAccount(undefined));
|
||||||
|
dispatch(setSourceWalletAddress(undefined));
|
||||||
|
} else if (newTokenAccount !== undefined && walletIsReady.walletAddress) {
|
||||||
|
dispatch(setSourceParsedTokenAccount(newTokenAccount));
|
||||||
|
dispatch(setSourceWalletAddress(walletIsReady.walletAddress));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch, walletIsReady]
|
||||||
);
|
);
|
||||||
|
|
||||||
const maps = useGetSourceParsedTokens();
|
const maps = useGetSourceParsedTokens();
|
||||||
|
@ -54,17 +62,18 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
) : lookupChain === CHAIN_ID_SOLANA ? (
|
) : lookupChain === CHAIN_ID_SOLANA ? (
|
||||||
<SolanaSourceTokenSelector
|
<SolanaSourceTokenSelector
|
||||||
value={sourceParsedTokenAccount || null}
|
value={sourceParsedTokenAccount || null}
|
||||||
onChange={handleSolanaOnChange}
|
onChange={handleOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
accounts={maps?.tokenAccounts?.data || []}
|
accounts={maps?.tokenAccounts?.data || []}
|
||||||
solanaTokenMap={maps?.tokenMap}
|
solanaTokenMap={maps?.tokenMap}
|
||||||
metaplexData={maps?.metaplex}
|
metaplexData={maps?.metaplex}
|
||||||
|
mintAccounts={maps?.mintAccounts}
|
||||||
/>
|
/>
|
||||||
) : lookupChain === CHAIN_ID_ETH ? (
|
) : lookupChain === CHAIN_ID_ETH ? (
|
||||||
<EthereumSourceTokenSelector
|
<EthereumSourceTokenSelector
|
||||||
value={sourceParsedTokenAccount || null}
|
value={sourceParsedTokenAccount || null}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleSolanaOnChange}
|
onChange={handleOnChange}
|
||||||
covalent={maps?.covalent || undefined}
|
covalent={maps?.covalent || undefined}
|
||||||
tokenAccounts={maps?.tokenAccounts} //TODO standardize
|
tokenAccounts={maps?.tokenAccounts} //TODO standardize
|
||||||
/>
|
/>
|
||||||
|
@ -72,7 +81,8 @@ export const TokenSelector = (props: TokenSelectorProps) => {
|
||||||
<TerraSourceTokenSelector
|
<TerraSourceTokenSelector
|
||||||
value={sourceParsedTokenAccount || null}
|
value={sourceParsedTokenAccount || null}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleSolanaOnChange}
|
onChange={handleOnChange}
|
||||||
|
tokenMap={maps?.terraTokenMap}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
@ -1,20 +1,48 @@
|
||||||
import { TextField, Typography } from "@material-ui/core";
|
import {
|
||||||
|
createStyles,
|
||||||
|
makeStyles,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { Autocomplete, createFilterOptions } from "@material-ui/lab";
|
||||||
import { LCDClient } from "@terra-money/terra.js";
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
import {
|
import {
|
||||||
ConnectedWallet,
|
ConnectedWallet,
|
||||||
useConnectedWallet,
|
useConnectedWallet,
|
||||||
} from "@terra-money/wallet-provider";
|
} from "@terra-money/wallet-provider";
|
||||||
import { formatUnits } from "ethers/lib/utils";
|
import { formatUnits } from "ethers/lib/utils";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
import {
|
||||||
|
createParsedTokenAccount,
|
||||||
|
TerraTokenMap,
|
||||||
|
TerraTokenMetadata,
|
||||||
|
} from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||||
|
import { DataWrapper } from "../../store/helpers";
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||||
import { TERRA_HOST } from "../../utils/consts";
|
import { TERRA_HOST } from "../../utils/consts";
|
||||||
|
import { shortenAddress } from "../../utils/solana";
|
||||||
import OffsetButton from "./OffsetButton";
|
import OffsetButton from "./OffsetButton";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
selectInput: { minWidth: "10rem" },
|
||||||
|
tokenOverviewContainer: {
|
||||||
|
display: "flex",
|
||||||
|
"& div": {
|
||||||
|
margin: ".5rem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokenImage: {
|
||||||
|
maxHeight: "2.5rem",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
type TerraSourceTokenSelectorProps = {
|
type TerraSourceTokenSelectorProps = {
|
||||||
value: ParsedTokenAccount | null;
|
value: ParsedTokenAccount | null;
|
||||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
tokenMap: DataWrapper<TerraTokenMap> | undefined; //TODO better type
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO move elsewhere
|
//TODO move elsewhere
|
||||||
|
@ -58,12 +86,27 @@ const lookupTerraAddress = (
|
||||||
export default function TerraSourceTokenSelector(
|
export default function TerraSourceTokenSelector(
|
||||||
props: TerraSourceTokenSelectorProps
|
props: TerraSourceTokenSelectorProps
|
||||||
) {
|
) {
|
||||||
const { onChange, value, disabled } = props;
|
const classes = useStyles();
|
||||||
//const advancedMode = true; //const [advancedMode, setAdvancedMode] = useState(true);
|
const { onChange, value, disabled, tokenMap } = props;
|
||||||
|
const [advancedMode, setAdvancedMode] = useState(false);
|
||||||
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
const [advancedModeHolderString, setAdvancedModeHolderString] = useState("");
|
||||||
const [advancedModeError, setAdvancedModeError] = useState("");
|
const [advancedModeError, setAdvancedModeError] = useState("");
|
||||||
const terraWallet = useConnectedWallet();
|
const terraWallet = useConnectedWallet();
|
||||||
|
|
||||||
|
const isLoading = tokenMap?.isFetching || false;
|
||||||
|
|
||||||
|
const terraTokenArray = useMemo(() => {
|
||||||
|
const values = props.tokenMap?.data?.mainnet;
|
||||||
|
const items = Object.values(values || {});
|
||||||
|
return items || [];
|
||||||
|
}, [props.tokenMap]);
|
||||||
|
|
||||||
|
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
||||||
|
if (!fromProps) return undefined;
|
||||||
|
else {
|
||||||
|
return terraTokenArray.find((x) => x.token === fromProps.mintKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
setAdvancedModeHolderString("");
|
setAdvancedModeHolderString("");
|
||||||
|
@ -74,22 +117,93 @@ export default function TerraSourceTokenSelector(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = (address: string | undefined) => {
|
||||||
if (terraWallet === undefined) {
|
if (terraWallet === undefined || address === undefined) {
|
||||||
setAdvancedModeError("Terra wallet not connected.");
|
setAdvancedModeError("Terra wallet not connected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lookupTerraAddress(advancedModeHolderString, terraWallet).then(
|
setAdvancedModeError("");
|
||||||
|
lookupTerraAddress(address, terraWallet).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
onChange(result);
|
onChange(result);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
setAdvancedModeError("Unable to retrieve address.");
|
setAdvancedModeError("Unable to retrieve this address.");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setAdvancedModeError("");
|
setAdvancedModeError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filterConfig = createFilterOptions({
|
||||||
|
matchFrom: "any",
|
||||||
|
stringify: (option: TerraTokenMetadata) => {
|
||||||
|
const symbol = option.symbol + " " || "";
|
||||||
|
const mint = option.token + " " || "";
|
||||||
|
const name = option.protocol + " " || "";
|
||||||
|
|
||||||
|
return symbol + mint + name;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderOptionLabel = (option: TerraTokenMetadata) => {
|
||||||
|
return option.symbol + " (" + shortenAddress(option.token) + ")";
|
||||||
|
};
|
||||||
|
const renderOption = (option: TerraTokenMetadata) => {
|
||||||
|
return (
|
||||||
|
<div className={classes.tokenOverviewContainer}>
|
||||||
|
<div>
|
||||||
|
<img alt="" className={classes.tokenImage} src={option.icon} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h6">{option.symbol}</Typography>
|
||||||
|
<Typography variant="body2">{option.protocol}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body1">{option.token}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAdvancedMode = () => {
|
||||||
|
setAdvancedMode(!advancedMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const advancedModeToggleButton = (
|
||||||
|
<OffsetButton onClick={toggleAdvancedMode} disabled={disabled}>
|
||||||
|
{advancedMode ? "Toggle Token Picker" : "Toggle Override"}
|
||||||
|
</OffsetButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoComplete = (
|
||||||
|
<>
|
||||||
|
<Autocomplete
|
||||||
|
autoComplete
|
||||||
|
autoHighlight
|
||||||
|
autoSelect
|
||||||
|
blurOnSelect
|
||||||
|
clearOnBlur
|
||||||
|
fullWidth={false}
|
||||||
|
filterOptions={filterConfig}
|
||||||
|
value={valueToOption(value)}
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
handleConfirm(newValue?.token);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
noOptionsText={"No CW20 tokens found at the moment."}
|
||||||
|
options={terraTokenArray}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Token" variant="outlined" />
|
||||||
|
)}
|
||||||
|
renderOption={renderOption}
|
||||||
|
getOptionLabel={renderOptionLabel}
|
||||||
|
/>
|
||||||
|
{advancedModeError && (
|
||||||
|
<Typography color="error">{advancedModeError}</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const content = value ? (
|
const content = value ? (
|
||||||
<>
|
<>
|
||||||
<Typography>{value.mintKey}</Typography>
|
<Typography>{value.mintKey}</Typography>
|
||||||
|
@ -97,22 +211,32 @@ export default function TerraSourceTokenSelector(
|
||||||
Clear
|
Clear
|
||||||
</OffsetButton>
|
</OffsetButton>
|
||||||
</>
|
</>
|
||||||
|
) : !advancedMode ? (
|
||||||
|
autoComplete
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Asset Address"
|
label="Enter an asset address"
|
||||||
value={advancedModeHolderString}
|
value={advancedModeHolderString}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
error={advancedModeHolderString !== "" && !!advancedModeError}
|
error={advancedModeHolderString !== "" && !!advancedModeError}
|
||||||
helperText={advancedModeError === "" ? undefined : advancedModeError}
|
helperText={advancedModeError === "" ? undefined : advancedModeError}
|
||||||
/>
|
/>
|
||||||
<OffsetButton onClick={handleConfirm} disabled={disabled}>
|
<OffsetButton
|
||||||
|
onClick={() => handleConfirm(advancedModeHolderString)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</OffsetButton>
|
</OffsetButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <React.Fragment>{content}</React.Fragment>;
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{content}
|
||||||
|
{!value && !isLoading && advancedModeToggleButton}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useSelector } from "react-redux";
|
||||||
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import {
|
import {
|
||||||
|
selectSourceWalletAddress,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferTargetError,
|
selectTransferTargetError,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
|
@ -16,7 +17,18 @@ function Send() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
const error = useSelector(selectTransferTargetError);
|
const error = useSelector(selectTransferTargetError);
|
||||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
const { isReady, statusMessage, walletAddress } =
|
||||||
|
useIsWalletReady(sourceChain);
|
||||||
|
const sourceWalletAddress = useSelector(selectSourceWalletAddress);
|
||||||
|
//The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain.
|
||||||
|
const isWrongWallet =
|
||||||
|
sourceWalletAddress &&
|
||||||
|
walletAddress &&
|
||||||
|
sourceWalletAddress !== walletAddress;
|
||||||
|
const isDisabled = !isReady || isWrongWallet || disabled;
|
||||||
|
const errorMessage = isWrongWallet
|
||||||
|
? "A different wallet is connected than in Step 1."
|
||||||
|
: statusMessage || error || undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepDescription>
|
<StepDescription>
|
||||||
|
@ -30,10 +42,10 @@ function Send() {
|
||||||
complete the transfer.
|
complete the transfer.
|
||||||
</Alert>
|
</Alert>
|
||||||
<ButtonWithLoader
|
<ButtonWithLoader
|
||||||
disabled={!isReady || disabled}
|
disabled={isDisabled}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
showLoader={showLoader}
|
showLoader={showLoader}
|
||||||
error={statusMessage || error}
|
error={errorMessage}
|
||||||
>
|
>
|
||||||
Transfer
|
Transfer
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
|
|
@ -8,8 +8,8 @@ import React, {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
type Provider = ethers.providers.Web3Provider | undefined;
|
export type Provider = ethers.providers.Web3Provider | undefined;
|
||||||
type Signer = ethers.Signer | undefined;
|
export type Signer = ethers.Signer | undefined;
|
||||||
|
|
||||||
interface IEthereumProviderContext {
|
interface IEthereumProviderContext {
|
||||||
connect(): void;
|
connect(): void;
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
import {
|
||||||
|
CHAIN_ID_ETH,
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
CHAIN_ID_TERRA,
|
||||||
|
} from "@certusone/wormhole-sdk";
|
||||||
import { Dispatch } from "@reduxjs/toolkit";
|
import { Dispatch } from "@reduxjs/toolkit";
|
||||||
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||||
import { ENV, TokenListProvider } from "@solana/spl-token-registry";
|
import { ENV, TokenListProvider } from "@solana/spl-token-registry";
|
||||||
|
@ -17,27 +21,44 @@ import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { DataWrapper } from "../store/helpers";
|
import { DataWrapper } from "../store/helpers";
|
||||||
import {
|
import {
|
||||||
selectSolanaTokenMap,
|
selectSolanaTokenMap,
|
||||||
|
selectSourceWalletAddress,
|
||||||
|
selectTerraTokenMap,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferSourceParsedTokenAccounts,
|
selectTransferSourceParsedTokenAccounts,
|
||||||
} from "../store/selectors";
|
} from "../store/selectors";
|
||||||
import {
|
import {
|
||||||
errorSolanaTokenMap,
|
errorSolanaTokenMap,
|
||||||
|
errorTerraTokenMap,
|
||||||
fetchSolanaTokenMap,
|
fetchSolanaTokenMap,
|
||||||
|
fetchTerraTokenMap,
|
||||||
receiveSolanaTokenMap,
|
receiveSolanaTokenMap,
|
||||||
|
receiveTerraTokenMap,
|
||||||
} from "../store/tokenSlice";
|
} from "../store/tokenSlice";
|
||||||
import {
|
import {
|
||||||
errorSourceParsedTokenAccounts,
|
errorSourceParsedTokenAccounts,
|
||||||
fetchSourceParsedTokenAccounts,
|
fetchSourceParsedTokenAccounts,
|
||||||
ParsedTokenAccount,
|
ParsedTokenAccount,
|
||||||
receiveSourceParsedTokenAccounts,
|
receiveSourceParsedTokenAccounts,
|
||||||
|
setAmount,
|
||||||
|
setSourceParsedTokenAccount,
|
||||||
|
setSourceParsedTokenAccounts,
|
||||||
|
setSourceWalletAddress,
|
||||||
} from "../store/transferSlice";
|
} from "../store/transferSlice";
|
||||||
import { CLUSTER, COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts";
|
import {
|
||||||
|
CLUSTER,
|
||||||
|
COVALENT_GET_TOKENS_URL,
|
||||||
|
SOLANA_HOST,
|
||||||
|
TERRA_TOKEN_METADATA_URL,
|
||||||
|
} from "../utils/consts";
|
||||||
import {
|
import {
|
||||||
decodeMetadata,
|
decodeMetadata,
|
||||||
getMetadataAddress,
|
getMetadataAddress,
|
||||||
Metadata,
|
Metadata,
|
||||||
} from "../utils/metaplex";
|
} from "../utils/metaplex";
|
||||||
import { getMultipleAccountsRPC } from "../utils/solana";
|
import {
|
||||||
|
extractMintAuthorityInfo,
|
||||||
|
getMultipleAccountsRPC,
|
||||||
|
} from "../utils/solana";
|
||||||
|
|
||||||
export function createParsedTokenAccount(
|
export function createParsedTokenAccount(
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
|
@ -57,6 +78,19 @@ export function createParsedTokenAccount(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TerraTokenMetadata = {
|
||||||
|
protocol: string;
|
||||||
|
symbol: string;
|
||||||
|
token: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TerraTokenMap = {
|
||||||
|
mainnet: {
|
||||||
|
[address: string]: TerraTokenMetadata;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const createParsedTokenAccountFromInfo = (
|
const createParsedTokenAccountFromInfo = (
|
||||||
pubkey: PublicKey,
|
pubkey: PublicKey,
|
||||||
item: AccountInfo<ParsedAccountData>
|
item: AccountInfo<ParsedAccountData>
|
||||||
|
@ -211,6 +245,7 @@ function useGetAvailableTokens() {
|
||||||
|
|
||||||
const tokenAccounts = useSelector(selectTransferSourceParsedTokenAccounts);
|
const tokenAccounts = useSelector(selectTransferSourceParsedTokenAccounts);
|
||||||
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
const solanaTokenMap = useSelector(selectSolanaTokenMap);
|
||||||
|
const terraTokenMap = useSelector(selectTerraTokenMap);
|
||||||
|
|
||||||
const lookupChain = useSelector(selectTransferSourceChain);
|
const lookupChain = useSelector(selectTransferSourceChain);
|
||||||
const solanaWallet = useSolanaWallet();
|
const solanaWallet = useSolanaWallet();
|
||||||
|
@ -228,6 +263,38 @@ function useGetAvailableTokens() {
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [solanaMintAccounts, setSolanaMintAccounts] = useState<any>(undefined);
|
||||||
|
const [solanaMintAccountsLoading, setSolanaMintAccountsLoading] =
|
||||||
|
useState(false);
|
||||||
|
const [solanaMintAccountsError, setSolanaMintAccountsError] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const selectedSourceWalletAddress = useSelector(selectSourceWalletAddress);
|
||||||
|
const currentSourceWalletAddress: string | undefined =
|
||||||
|
lookupChain === CHAIN_ID_ETH
|
||||||
|
? signerAddress
|
||||||
|
: lookupChain === CHAIN_ID_SOLANA
|
||||||
|
? solPK?.toString()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
//TODO this useEffect could be somewhere else in the codebase
|
||||||
|
//It resets the SourceParsedTokens accounts when the wallet changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedSourceWalletAddress !== undefined &&
|
||||||
|
currentSourceWalletAddress !== undefined &&
|
||||||
|
currentSourceWalletAddress !== selectedSourceWalletAddress
|
||||||
|
) {
|
||||||
|
dispatch(setSourceWalletAddress(undefined));
|
||||||
|
dispatch(setSourceParsedTokenAccount(undefined));
|
||||||
|
dispatch(setSourceParsedTokenAccounts(undefined));
|
||||||
|
dispatch(setAmount(""));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}, [selectedSourceWalletAddress, currentSourceWalletAddress, dispatch]);
|
||||||
|
|
||||||
// Solana metaplex load
|
// Solana metaplex load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
@ -289,6 +356,53 @@ function useGetAvailableTokens() {
|
||||||
solanaTokenMap,
|
solanaTokenMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
//Solana Mint Accounts lookup
|
||||||
|
useEffect(() => {
|
||||||
|
if (lookupChain !== CHAIN_ID_SOLANA || !tokenAccounts.data?.length) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setSolanaMintAccountsLoading(true);
|
||||||
|
setSolanaMintAccountsError(undefined);
|
||||||
|
const mintAddresses = tokenAccounts.data.map((x) => x.mintKey);
|
||||||
|
//This is a known wormhole v1 token on testnet
|
||||||
|
//mintAddresses.push("4QixXecTZ4zdZGa39KH8gVND5NZ2xcaB12wiBhE4S7rn");
|
||||||
|
|
||||||
|
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||||
|
getMultipleAccountsRPC(
|
||||||
|
connection,
|
||||||
|
mintAddresses.map((x) => new PublicKey(x))
|
||||||
|
).then(
|
||||||
|
(results) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
const output = new Map<String, string | null>();
|
||||||
|
|
||||||
|
results.forEach((result, index) =>
|
||||||
|
output.set(
|
||||||
|
mintAddresses[index],
|
||||||
|
(result && extractMintAuthorityInfo(result)) || null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setSolanaMintAccounts(output);
|
||||||
|
setSolanaMintAccountsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setSolanaMintAccounts(undefined);
|
||||||
|
setSolanaMintAccountsLoading(false);
|
||||||
|
setSolanaMintAccountsError(
|
||||||
|
"Could not retrieve Solana mint accounts."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => (cancelled = true);
|
||||||
|
}, [tokenAccounts.data, lookupChain]);
|
||||||
|
|
||||||
//Ethereum accounts load
|
//Ethereum accounts load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
|
//const testWallet = "0xf60c2ea62edbfe808163751dd0d8693dcb30019c";
|
||||||
|
@ -333,10 +447,38 @@ function useGetAvailableTokens() {
|
||||||
}, [lookupChain, provider, signerAddress, dispatch]);
|
}, [lookupChain, provider, signerAddress, dispatch]);
|
||||||
|
|
||||||
//Terra accounts load
|
//Terra accounts load
|
||||||
|
//At present, we don't have any mechanism for doing this.
|
||||||
useEffect(() => {}, []);
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
//Terra metadata load
|
//Terra metadata load
|
||||||
useEffect(() => {}, []);
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
if (terraTokenMap.data || lookupChain !== CHAIN_ID_TERRA) {
|
||||||
|
return; //So we don't fetch the whole list on every mount.
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchTerraTokenMap());
|
||||||
|
axios.get(TERRA_TOKEN_METADATA_URL).then(
|
||||||
|
(response) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
//TODO parse this in a safer manner
|
||||||
|
dispatch(receiveTerraTokenMap(response.data as TerraTokenMap));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
dispatch(
|
||||||
|
errorTerraTokenMap("Failed to retrieve the Terra Token List.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [lookupChain, terraTokenMap.data, dispatch]);
|
||||||
|
|
||||||
return lookupChain === CHAIN_ID_SOLANA
|
return lookupChain === CHAIN_ID_SOLANA
|
||||||
? {
|
? {
|
||||||
|
@ -348,6 +490,12 @@ function useGetAvailableTokens() {
|
||||||
error: metaplexError,
|
error: metaplexError,
|
||||||
receivedAt: null, //TODO
|
receivedAt: null, //TODO
|
||||||
} as DataWrapper<Metadata[]>,
|
} as DataWrapper<Metadata[]>,
|
||||||
|
mintAccounts: {
|
||||||
|
data: solanaMintAccounts,
|
||||||
|
isFetching: solanaMintAccountsLoading,
|
||||||
|
error: solanaMintAccountsError,
|
||||||
|
receivedAt: null, //TODO
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: lookupChain === CHAIN_ID_ETH
|
: lookupChain === CHAIN_ID_ETH
|
||||||
? {
|
? {
|
||||||
|
@ -359,6 +507,10 @@ function useGetAvailableTokens() {
|
||||||
receivedAt: null, //TODO
|
receivedAt: null, //TODO
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
: lookupChain === CHAIN_ID_TERRA
|
||||||
|
? {
|
||||||
|
terraTokenMap: terraTokenMap,
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,20 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { CLUSTER, ETH_NETWORK_CHAIN_ID } from "../utils/consts";
|
import { CLUSTER, ETH_NETWORK_CHAIN_ID } from "../utils/consts";
|
||||||
|
|
||||||
const createWalletStatus = (isReady: boolean, statusMessage: string = "") => ({
|
const createWalletStatus = (
|
||||||
|
isReady: boolean,
|
||||||
|
statusMessage: string = "",
|
||||||
|
walletAddress?: string
|
||||||
|
) => ({
|
||||||
isReady,
|
isReady,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
|
walletAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useIsWalletReady(chainId: ChainId): {
|
function useIsWalletReady(chainId: ChainId): {
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
statusMessage: string;
|
statusMessage: string;
|
||||||
|
walletAddress?: string;
|
||||||
} {
|
} {
|
||||||
const solanaWallet = useSolanaWallet();
|
const solanaWallet = useSolanaWallet();
|
||||||
const solPK = solanaWallet?.publicKey;
|
const solPK = solanaWallet?.publicKey;
|
||||||
|
@ -33,16 +39,20 @@ function useIsWalletReady(chainId: ChainId): {
|
||||||
const hasCorrectEthNetwork = ethChainId === ETH_NETWORK_CHAIN_ID;
|
const hasCorrectEthNetwork = ethChainId === ETH_NETWORK_CHAIN_ID;
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (chainId === CHAIN_ID_TERRA && hasTerraWallet) {
|
if (
|
||||||
|
chainId === CHAIN_ID_TERRA &&
|
||||||
|
hasTerraWallet &&
|
||||||
|
terraWallet?.walletAddress
|
||||||
|
) {
|
||||||
// TODO: terraWallet does not update on wallet changes
|
// TODO: terraWallet does not update on wallet changes
|
||||||
return createWalletStatus(true);
|
return createWalletStatus(true, undefined, terraWallet.walletAddress);
|
||||||
}
|
}
|
||||||
if (chainId === CHAIN_ID_SOLANA && solPK) {
|
if (chainId === CHAIN_ID_SOLANA && solPK) {
|
||||||
return createWalletStatus(true);
|
return createWalletStatus(true, undefined, solPK.toString());
|
||||||
}
|
}
|
||||||
if (chainId === CHAIN_ID_ETH && hasEthInfo) {
|
if (chainId === CHAIN_ID_ETH && hasEthInfo && signerAddress) {
|
||||||
if (hasCorrectEthNetwork) {
|
if (hasCorrectEthNetwork) {
|
||||||
return createWalletStatus(true);
|
return createWalletStatus(true, undefined, signerAddress);
|
||||||
} else {
|
} else {
|
||||||
if (provider) {
|
if (provider) {
|
||||||
try {
|
try {
|
||||||
|
@ -53,7 +63,8 @@ function useIsWalletReady(chainId: ChainId): {
|
||||||
}
|
}
|
||||||
return createWalletStatus(
|
return createWalletStatus(
|
||||||
false,
|
false,
|
||||||
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}`
|
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}`,
|
||||||
|
undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,6 +77,8 @@ function useIsWalletReady(chainId: ChainId): {
|
||||||
hasEthInfo,
|
hasEthInfo,
|
||||||
hasCorrectEthNetwork,
|
hasCorrectEthNetwork,
|
||||||
provider,
|
provider,
|
||||||
|
signerAddress,
|
||||||
|
terraWallet,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,8 @@ export const selectTransferOriginChain = (state: RootState) =>
|
||||||
state.transfer.originChain;
|
state.transfer.originChain;
|
||||||
export const selectTransferOriginAsset = (state: RootState) =>
|
export const selectTransferOriginAsset = (state: RootState) =>
|
||||||
state.transfer.originAsset;
|
state.transfer.originAsset;
|
||||||
|
export const selectSourceWalletAddress = (state: RootState) =>
|
||||||
|
state.transfer.sourceWalletAddress;
|
||||||
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
||||||
state.transfer.sourceParsedTokenAccount;
|
state.transfer.sourceParsedTokenAccount;
|
||||||
export const selectTransferSourceParsedTokenAccounts = (state: RootState) =>
|
export const selectTransferSourceParsedTokenAccounts = (state: RootState) =>
|
||||||
|
@ -168,3 +170,7 @@ export const selectTransferShouldLockFields = (state: RootState) =>
|
||||||
export const selectSolanaTokenMap = (state: RootState) => {
|
export const selectSolanaTokenMap = (state: RootState) => {
|
||||||
return state.tokens.solanaTokenMap;
|
return state.tokens.solanaTokenMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectTerraTokenMap = (state: RootState) => {
|
||||||
|
return state.tokens.terraTokenMap;
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { TokenInfo } from "@solana/spl-token-registry";
|
import { TokenInfo } from "@solana/spl-token-registry";
|
||||||
|
import { TerraTokenMap } from "../hooks/useGetSourceParsedTokenAccounts";
|
||||||
import {
|
import {
|
||||||
DataWrapper,
|
DataWrapper,
|
||||||
errorDataWrapper,
|
errorDataWrapper,
|
||||||
|
@ -10,10 +11,12 @@ import {
|
||||||
|
|
||||||
export interface TokenMetadataState {
|
export interface TokenMetadataState {
|
||||||
solanaTokenMap: DataWrapper<TokenInfo[]>;
|
solanaTokenMap: DataWrapper<TokenInfo[]>;
|
||||||
|
terraTokenMap: DataWrapper<TerraTokenMap>; //TODO make a decent type for this.
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TokenMetadataState = {
|
const initialState: TokenMetadataState = {
|
||||||
solanaTokenMap: getEmptyDataWrapper(),
|
solanaTokenMap: getEmptyDataWrapper(),
|
||||||
|
terraTokenMap: getEmptyDataWrapper(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tokenSlice = createSlice({
|
export const tokenSlice = createSlice({
|
||||||
|
@ -29,6 +32,17 @@ export const tokenSlice = createSlice({
|
||||||
errorSolanaTokenMap: (state, action: PayloadAction<string>) => {
|
errorSolanaTokenMap: (state, action: PayloadAction<string>) => {
|
||||||
state.solanaTokenMap = errorDataWrapper(action.payload);
|
state.solanaTokenMap = errorDataWrapper(action.payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
receiveTerraTokenMap: (state, action: PayloadAction<TerraTokenMap>) => {
|
||||||
|
state.terraTokenMap = receiveDataWrapper(action.payload);
|
||||||
|
},
|
||||||
|
fetchTerraTokenMap: (state) => {
|
||||||
|
state.terraTokenMap = fetchDataWrapper();
|
||||||
|
},
|
||||||
|
errorTerraTokenMap: (state, action: PayloadAction<string>) => {
|
||||||
|
state.terraTokenMap = errorDataWrapper(action.payload);
|
||||||
|
},
|
||||||
|
|
||||||
reset: () => initialState,
|
reset: () => initialState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -37,6 +51,9 @@ export const {
|
||||||
receiveSolanaTokenMap,
|
receiveSolanaTokenMap,
|
||||||
fetchSolanaTokenMap,
|
fetchSolanaTokenMap,
|
||||||
errorSolanaTokenMap,
|
errorSolanaTokenMap,
|
||||||
|
receiveTerraTokenMap,
|
||||||
|
fetchTerraTokenMap,
|
||||||
|
errorTerraTokenMap,
|
||||||
reset,
|
reset,
|
||||||
} = tokenSlice.actions;
|
} = tokenSlice.actions;
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ export interface TransferState {
|
||||||
isSourceAssetWormholeWrapped: boolean | undefined;
|
isSourceAssetWormholeWrapped: boolean | undefined;
|
||||||
originChain: ChainId | undefined;
|
originChain: ChainId | undefined;
|
||||||
originAsset: string | undefined;
|
originAsset: string | undefined;
|
||||||
|
sourceWalletAddress: string | undefined;
|
||||||
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
||||||
sourceParsedTokenAccounts: DataWrapper<ParsedTokenAccount[]>;
|
sourceParsedTokenAccounts: DataWrapper<ParsedTokenAccount[]>;
|
||||||
amount: string;
|
amount: string;
|
||||||
|
@ -55,6 +56,7 @@ const initialState: TransferState = {
|
||||||
activeStep: 0,
|
activeStep: 0,
|
||||||
sourceChain: CHAIN_ID_SOLANA,
|
sourceChain: CHAIN_ID_SOLANA,
|
||||||
isSourceAssetWormholeWrapped: false,
|
isSourceAssetWormholeWrapped: false,
|
||||||
|
sourceWalletAddress: undefined,
|
||||||
sourceParsedTokenAccount: undefined,
|
sourceParsedTokenAccount: undefined,
|
||||||
sourceParsedTokenAccounts: getEmptyDataWrapper(),
|
sourceParsedTokenAccounts: getEmptyDataWrapper(),
|
||||||
originChain: undefined,
|
originChain: undefined,
|
||||||
|
@ -110,12 +112,26 @@ export const transferSlice = createSlice({
|
||||||
state.originAsset = undefined;
|
state.originAsset = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setSourceWalletAddress: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<string | undefined>
|
||||||
|
) => {
|
||||||
|
state.sourceWalletAddress = action.payload;
|
||||||
|
},
|
||||||
setSourceParsedTokenAccount: (
|
setSourceParsedTokenAccount: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<ParsedTokenAccount | undefined>
|
action: PayloadAction<ParsedTokenAccount | undefined>
|
||||||
) => {
|
) => {
|
||||||
state.sourceParsedTokenAccount = action.payload;
|
state.sourceParsedTokenAccount = action.payload;
|
||||||
},
|
},
|
||||||
|
setSourceParsedTokenAccounts: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<ParsedTokenAccount[] | undefined>
|
||||||
|
) => {
|
||||||
|
state.sourceParsedTokenAccounts = action.payload
|
||||||
|
? receiveDataWrapper(action.payload)
|
||||||
|
: getEmptyDataWrapper();
|
||||||
|
},
|
||||||
fetchSourceParsedTokenAccounts: (state) => {
|
fetchSourceParsedTokenAccounts: (state) => {
|
||||||
state.sourceParsedTokenAccounts = fetchDataWrapper();
|
state.sourceParsedTokenAccounts = fetchDataWrapper();
|
||||||
},
|
},
|
||||||
|
@ -195,7 +211,9 @@ export const {
|
||||||
setStep,
|
setStep,
|
||||||
setSourceChain,
|
setSourceChain,
|
||||||
setSourceWormholeWrappedInfo,
|
setSourceWormholeWrappedInfo,
|
||||||
|
setSourceWalletAddress,
|
||||||
setSourceParsedTokenAccount,
|
setSourceParsedTokenAccount,
|
||||||
|
setSourceParsedTokenAccounts,
|
||||||
receiveSourceParsedTokenAccounts,
|
receiveSourceParsedTokenAccounts,
|
||||||
errorSourceParsedTokenAccounts,
|
errorSourceParsedTokenAccounts,
|
||||||
fetchSourceParsedTokenAccounts,
|
fetchSourceParsedTokenAccounts,
|
||||||
|
|
|
@ -30,6 +30,10 @@ export const CHAINS =
|
||||||
id: CHAIN_ID_SOLANA,
|
id: CHAIN_ID_SOLANA,
|
||||||
name: "Solana",
|
name: "Solana",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: CHAIN_ID_TERRA,
|
||||||
|
name: "Terra",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
@ -62,11 +66,19 @@ export const ETH_NETWORK_CHAIN_ID =
|
||||||
CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337;
|
CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337;
|
||||||
export const SOLANA_HOST =
|
export const SOLANA_HOST =
|
||||||
CLUSTER === "testnet" ? clusterApiUrl("testnet") : "http://localhost:8899";
|
CLUSTER === "testnet" ? clusterApiUrl("testnet") : "http://localhost:8899";
|
||||||
export const TERRA_HOST = {
|
|
||||||
|
export const TERRA_HOST =
|
||||||
|
CLUSTER === "testnet"
|
||||||
|
? {
|
||||||
|
URL: "https://tequila-lcd.terra.dev",
|
||||||
|
chainID: "tequila-0004",
|
||||||
|
name: "testnet",
|
||||||
|
}
|
||||||
|
: {
|
||||||
URL: "http://localhost:1317",
|
URL: "http://localhost:1317",
|
||||||
chainID: "columbus-4",
|
chainID: "columbus-4",
|
||||||
name: "localterra",
|
name: "localterra",
|
||||||
};
|
};
|
||||||
export const ETH_TEST_TOKEN_ADDRESS = getAddress(
|
export const ETH_TEST_TOKEN_ADDRESS = getAddress(
|
||||||
CLUSTER === "testnet"
|
CLUSTER === "testnet"
|
||||||
? "0xcEE940033DA197F551BBEdED7F4aA55Ee55C582B"
|
? "0xcEE940033DA197F551BBEdED7F4aA55Ee55C582B"
|
||||||
|
@ -119,3 +131,24 @@ export const COVALENT_GET_TOKENS_URL = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COVALENT_ETHEREUM_MAINNET = "1";
|
export const COVALENT_ETHEREUM_MAINNET = "1";
|
||||||
|
|
||||||
|
export const WORMHOLE_V1_ETH_ADDRESS =
|
||||||
|
CLUSTER === "testnet"
|
||||||
|
? "0xdae0Cba01eFc4bfEc1F7Fece73Fe8b8d2Eda65B0"
|
||||||
|
: CLUSTER === "mainnet"
|
||||||
|
? "0xf92cD566Ea4864356C5491c177A430C222d7e678"
|
||||||
|
: "0xf92cD566Ea4864356C5491c177A430C222d7e678"; //TODO something that doesn't explode in localhost
|
||||||
|
export const WORMHOLE_V1_SOLANA_ADDRESS =
|
||||||
|
CLUSTER === "testnet"
|
||||||
|
? "BrdgiFmZN3BKkcY3danbPYyxPKwb8RhQzpM2VY5L97ED"
|
||||||
|
: "WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC";
|
||||||
|
|
||||||
|
export const TERRA_TOKEN_METADATA_URL =
|
||||||
|
"https://assets.terra.money/cw20/tokens.json";
|
||||||
|
|
||||||
|
export const WORMHOLE_V1_MINT_AUTHORITY =
|
||||||
|
CLUSTER === "mainnet"
|
||||||
|
? "9zyPU1mjgzaVyQsYwKJJ7AhVz5bgx5uc1NPABvAcUXsT"
|
||||||
|
: CLUSTER === "testnet"
|
||||||
|
? "BJa7dq3bRP216zaTdw4cdcV71WkPc1HXvmnGeFVDi5DC"
|
||||||
|
: "";
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { MintLayout } from "@solana/spl-token";
|
||||||
import { WalletContextState } from "@solana/wallet-adapter-react";
|
import { WalletContextState } from "@solana/wallet-adapter-react";
|
||||||
import {
|
import {
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
|
@ -17,6 +18,19 @@ export async function signSendAndConfirm(
|
||||||
return txid;
|
return txid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMintAuthorityInfo(
|
||||||
|
account: AccountInfo<Buffer>
|
||||||
|
): string | null {
|
||||||
|
const data = Buffer.from(account.data);
|
||||||
|
const mintInfo = MintLayout.decode(data);
|
||||||
|
|
||||||
|
const uintArray = mintInfo?.mintAuthority;
|
||||||
|
const pubkey = new PublicKey(uintArray);
|
||||||
|
const output = pubkey?.toString();
|
||||||
|
|
||||||
|
return output || null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMultipleAccountsRPC(
|
export async function getMultipleAccountsRPC(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
pubkeys: PublicKey[]
|
pubkeys: PublicKey[]
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
"lib/**/*"
|
"lib/**/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "npm run build-contracts",
|
"postinstall": "npm run build-abis && npm run build-contracts",
|
||||||
"build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
|
"build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
|
||||||
|
"build-abis": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts/abi src/abi/Wormhole.abi.json",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "tsc && node scripts/copyEthersTypes.js && node scripts/copyWasm.js",
|
"build": "tsc && node scripts/copyEthersTypes.js && node scripts/copyWasm.js",
|
||||||
"format": "prettier --write \"src/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
|
|
|
@ -7,3 +7,12 @@ fs.readdirSync("src/ethers-contracts").forEach((file) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fs.readdirSync("src/ethers-contracts/abi").forEach((file) => {
|
||||||
|
if (file.endsWith(".d.ts")) {
|
||||||
|
fs.copyFileSync(
|
||||||
|
`src/ethers-contracts/abi/${file}`,
|
||||||
|
`lib/ethers-contracts/abi/${file}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,388 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"internalType": "address[]",
|
||||||
|
"name": "keys",
|
||||||
|
"type": "address[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "expiration_time",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internalType": "struct Wormhole.GuardianSet",
|
||||||
|
"name": "initial_guardian_set",
|
||||||
|
"type": "tuple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "wrapped_asset_master",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "_guardian_set_expirity",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "oldGuardianIndex",
|
||||||
|
"type": "uint32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "newGuardianIndex",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "LogGuardianSetChanged",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "target_chain",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "token_chain",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "token_decimals",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "token",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "sender",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "recipient",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "nonce",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "LogTokensLocked",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stateMutability": "payable",
|
||||||
|
"type": "fallback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "",
|
||||||
|
"type": "bytes32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "consumedVAAs",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "idx",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "getGuardianSet",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"internalType": "address[]",
|
||||||
|
"name": "keys",
|
||||||
|
"type": "address[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "expiration_time",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internalType": "struct Wormhole.GuardianSet",
|
||||||
|
"name": "gs",
|
||||||
|
"type": "tuple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "guardian_set_expirity",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "guardian_set_index",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "guardian_sets",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "expiration_time",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "isWrappedAsset",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "asset",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "recipient",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "target_chain",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "nonce",
|
||||||
|
"type": "uint32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "refund_dust",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "lockAssets",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "recipient",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "target_chain",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "nonce",
|
||||||
|
"type": "uint32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "lockETH",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "payable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes",
|
||||||
|
"name": "vaa",
|
||||||
|
"type": "bytes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "parseAndVerifyVAA",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "version",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "hash",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "guardian_set_index",
|
||||||
|
"type": "uint32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint32",
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "uint32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "action",
|
||||||
|
"type": "uint8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bytes",
|
||||||
|
"name": "payload",
|
||||||
|
"type": "bytes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internalType": "struct Wormhole.ParsedVAA",
|
||||||
|
"name": "parsed_vaa",
|
||||||
|
"type": "tuple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes",
|
||||||
|
"name": "vaa",
|
||||||
|
"type": "bytes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "submitVAA",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "wrappedAssetMaster",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "",
|
||||||
|
"type": "bytes32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "wrappedAssets",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stateMutability": "payable",
|
||||||
|
"type": "receive"
|
||||||
|
}
|
||||||
|
]
|
Loading…
Reference in New Issue