bridge_ui: transfer & unwrap SOL

fixes https://github.com/certusone/wormhole/issues/486

Change-Id: I81b97ff0e1358bf0b88567ba9872ee615344a27c
This commit is contained in:
Chase Moran 2021-09-22 15:07:21 -04:00 committed by Evan Gray
parent 302368d704
commit 7b1a0bf3ad
30 changed files with 498 additions and 163 deletions

View File

@ -1,12 +1,15 @@
import { import {
ChainId,
CHAIN_ID_BSC, CHAIN_ID_BSC,
CHAIN_ID_ETH, CHAIN_ID_ETH,
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
getEmitterAddressEth, getEmitterAddressEth,
getEmitterAddressSolana, getEmitterAddressSolana,
hexToNativeString,
hexToUint8Array,
parseNFTPayload,
parseSequenceFromLogEth, parseSequenceFromLogEth,
parseSequenceFromLogSolana, parseSequenceFromLogSolana,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
Box, Box,
@ -27,7 +30,7 @@ import {
import { Restore } from "@material-ui/icons"; import { Restore } from "@material-ui/icons";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import { Connection } from "@solana/web3.js"; import { Connection } from "@solana/web3.js";
import { BigNumber, ethers } from "ethers"; import { ethers } from "ethers";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@ -37,11 +40,6 @@ import {
selectNFTSignedVAAHex, selectNFTSignedVAAHex,
selectNFTSourceChain, selectNFTSourceChain,
} from "../../store/selectors"; } from "../../store/selectors";
import {
hexToNativeString,
hexToUint8Array,
uint8ArrayToHex,
} from "../../utils/array";
import { import {
CHAINS, CHAINS,
ETH_BRIDGE_ADDRESS, ETH_BRIDGE_ADDRESS,
@ -51,7 +49,6 @@ import {
WORMHOLE_RPC_HOSTS, WORMHOLE_RPC_HOSTS,
} from "../../utils/consts"; } from "../../utils/consts";
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry"; import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
import { METADATA_REPLACE } from "../../utils/metaplex";
import parseError from "../../utils/parseError"; import parseError from "../../utils/parseError";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
@ -111,49 +108,6 @@ async function solana(tx: string, enqueueSnackbar: any) {
} }
} }
// note: actual first byte is message type
// 0 [u8; 32] token_address
// 32 u16 token_chain
// 34 [u8; 32] symbol
// 66 [u8; 32] name
// 98 u256 tokenId
// 130 u8 uri_len
// 131 [u8;len] uri
// ? [u8; 32] recipient
// ? u16 recipient_chain
// TODO: move to wasm / sdk, share with solana
export const parsePayload = (arr: Buffer) => {
const originAddress = arr.slice(1, 1 + 32).toString("hex");
const originChain = arr.readUInt16BE(33) as ChainId;
const symbol = Buffer.from(arr.slice(35, 35 + 32))
.toString("utf8")
.replace(METADATA_REPLACE, "");
const name = Buffer.from(arr.slice(67, 67 + 32))
.toString("utf8")
.replace(METADATA_REPLACE, "");
const tokenId = BigNumber.from(arr.slice(99, 99 + 32));
const uri_len = arr.readUInt8(131);
const uri = Buffer.from(arr.slice(132, 132 + uri_len))
.toString("utf8")
.replace(METADATA_REPLACE, "");
const target_offset = 132 + uri_len;
const targetAddress = arr
.slice(target_offset, target_offset + 32)
.toString("hex");
const targetChain = arr.readUInt16BE(target_offset + 32) as ChainId;
return {
originAddress,
originChain,
symbol,
name,
tokenId,
uri,
targetAddress,
targetChain,
};
};
function RecoveryDialogContent({ function RecoveryDialogContent({
onClose, onClose,
disabled, disabled,
@ -266,7 +220,9 @@ function RecoveryDialogContent({
const parsedPayload = useMemo( const parsedPayload = useMemo(
() => () =>
recoveryParsedVAA?.payload recoveryParsedVAA?.payload
? parsePayload(Buffer.from(new Uint8Array(recoveryParsedVAA.payload))) ? parseNFTPayload(
Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
)
: null, : null,
[recoveryParsedVAA] [recoveryParsedVAA]
); );

View File

@ -1,4 +1,9 @@
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
hexToNativeString,
hexToUint8Array,
} from "@certusone/wormhole-sdk";
import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core"; import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js";
@ -22,7 +27,6 @@ import {
selectNFTTargetChain, selectNFTTargetChain,
selectNFTTargetError, selectNFTTargetError,
} from "../../store/selectors"; } from "../../store/selectors";
import { hexToNativeString, hexToUint8Array } from "../../utils/array";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";

View File

@ -4,7 +4,7 @@ import {
selectNFTTargetAddressHex, selectNFTTargetAddressHex,
selectNFTTargetChain, selectNFTTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { hexToNativeString } from "../../utils/array"; import { hexToNativeString } from "@certusone/wormhole-sdk";
import { CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS_BY_ID } from "../../utils/consts";
import SmartAddress from "../SmartAddress"; import SmartAddress from "../SmartAddress";

View File

@ -2,6 +2,8 @@ import {
CHAIN_ID_BSC, CHAIN_ID_BSC,
CHAIN_ID_ETH, CHAIN_ID_ETH,
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
hexToNativeString,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
getOriginalAssetEth, getOriginalAssetEth,
@ -27,7 +29,6 @@ import useIsWalletReady from "../hooks/useIsWalletReady";
import { getMetaplexData } from "../hooks/useMetaplexData"; import { getMetaplexData } from "../hooks/useMetaplexData";
import { COLORS } from "../muiTheme"; import { COLORS } from "../muiTheme";
import { NFTParsedTokenAccount } from "../store/nftSlice"; import { NFTParsedTokenAccount } from "../store/nftSlice";
import { hexToNativeString, uint8ArrayToHex } from "../utils/array";
import { import {
CHAINS, CHAINS,
CHAINS_BY_ID, CHAINS_BY_ID,

View File

@ -85,6 +85,7 @@ export default function SolanaSourceTokenSelector(
const getLogo = useCallback( const getLogo = useCallback(
(account: ParsedTokenAccount) => { (account: ParsedTokenAccount) => {
return ( return (
(account.isNativeAsset && account.logo) ||
memoizedTokenMap.get(account.mintKey)?.logoURI || memoizedTokenMap.get(account.mintKey)?.logoURI ||
metaplex.data?.get(account.mintKey)?.data?.uri || metaplex.data?.get(account.mintKey)?.data?.uri ||
undefined undefined
@ -96,6 +97,7 @@ export default function SolanaSourceTokenSelector(
const getSymbol = useCallback( const getSymbol = useCallback(
(account: ParsedTokenAccount) => { (account: ParsedTokenAccount) => {
return ( return (
(account.isNativeAsset && account.symbol) ||
memoizedTokenMap.get(account.mintKey)?.symbol || memoizedTokenMap.get(account.mintKey)?.symbol ||
metaplex.data?.get(account.mintKey)?.data?.symbol || metaplex.data?.get(account.mintKey)?.data?.symbol ||
undefined undefined
@ -107,6 +109,7 @@ export default function SolanaSourceTokenSelector(
const getName = useCallback( const getName = useCallback(
(account: ParsedTokenAccount) => { (account: ParsedTokenAccount) => {
return ( return (
(account.isNativeAsset && account.name) ||
memoizedTokenMap.get(account.mintKey)?.name || memoizedTokenMap.get(account.mintKey)?.name ||
metaplex.data?.get(account.mintKey)?.data?.name || metaplex.data?.get(account.mintKey)?.data?.name ||
undefined undefined
@ -175,12 +178,18 @@ export default function SolanaSourceTokenSelector(
<Typography variant="subtitle2">{name}</Typography> <Typography variant="subtitle2">{name}</Typography>
</div> </div>
<div> <div>
{account.isNativeAsset ? (
<Typography>{"Native"}</Typography>
) : (
<>
<Typography variant="body1"> <Typography variant="body1">
{"Mint : " + mintPrettyString} {"Mint : " + mintPrettyString}
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
{"Account :" + accountAddressPrettyString} {"Account :" + accountAddressPrettyString}
</Typography> </Typography>
</>
)}
</div> </div>
<div> <div>
<Typography variant="body2">{"Balance"}</Typography> <Typography variant="body2">{"Balance"}</Typography>
@ -282,11 +291,15 @@ export default function SolanaSourceTokenSelector(
); );
const getOptionLabel = useCallback( const getOptionLabel = useCallback(
(option) => { (option: ParsedTokenAccount) => {
const symbol = getSymbol(option); const symbol = getSymbol(option);
return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress( const label = option.isNativeAsset
? symbol || "" //default for non-null guarantee
: `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress(
option.publicKey option.publicKey
)}, Mint: ${shortenAddress(option.mintKey)})`; )}, Mint: ${shortenAddress(option.mintKey)})`;
return label;
}, },
[getSymbol] [getSymbol]
); );

View File

@ -1,5 +1,4 @@
import { import {
ChainId,
CHAIN_ID_BSC, CHAIN_ID_BSC,
CHAIN_ID_ETH, CHAIN_ID_ETH,
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
@ -7,9 +6,13 @@ import {
getEmitterAddressEth, getEmitterAddressEth,
getEmitterAddressSolana, getEmitterAddressSolana,
getEmitterAddressTerra, getEmitterAddressTerra,
hexToNativeString,
hexToUint8Array,
parseSequenceFromLogEth, parseSequenceFromLogEth,
parseSequenceFromLogSolana, parseSequenceFromLogSolana,
parseSequenceFromLogTerra, parseSequenceFromLogTerra,
parseTransferPayload,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
Box, Box,
@ -31,7 +34,7 @@ import { Restore } from "@material-ui/icons";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import { Connection } from "@solana/web3.js"; import { Connection } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js"; import { LCDClient } from "@terra-money/terra.js";
import { BigNumber, ethers } from "ethers"; import { ethers } from "ethers";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@ -45,11 +48,6 @@ import {
setStep, setStep,
setTargetChain, setTargetChain,
} from "../../store/transferSlice"; } from "../../store/transferSlice";
import {
hexToNativeString,
hexToUint8Array,
uint8ArrayToHex,
} from "../../utils/array";
import { import {
CHAINS, CHAINS,
ETH_BRIDGE_ADDRESS, ETH_BRIDGE_ADDRESS,
@ -145,22 +143,6 @@ async function terra(tx: string, enqueueSnackbar: any) {
} }
} }
// 0 u256 amount
// 32 [u8; 32] token_address
// 64 u16 token_chain
// 66 [u8; 32] recipient
// 98 u16 recipient_chain
// 100 u256 fee
// TODO: move to wasm / sdk, share with solana
const parsePayload = (arr: Buffer) => ({
amount: BigNumber.from(arr.slice(1, 1 + 32)).toBigInt(),
originAddress: arr.slice(33, 33 + 32).toString("hex"),
originChain: arr.readUInt16BE(65) as ChainId,
targetAddress: arr.slice(67, 67 + 32).toString("hex"),
targetChain: arr.readUInt16BE(99) as ChainId,
});
function RecoveryDialogContent({ function RecoveryDialogContent({
onClose, onClose,
disabled, disabled,
@ -288,7 +270,9 @@ function RecoveryDialogContent({
const parsedPayload = useMemo( const parsedPayload = useMemo(
() => () =>
recoveryParsedVAA?.payload recoveryParsedVAA?.payload
? parsePayload(Buffer.from(new Uint8Array(recoveryParsedVAA.payload))) ? parseTransferPayload(
Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
)
: null, : null,
[recoveryParsedVAA] [recoveryParsedVAA]
); );

View File

@ -1,4 +1,8 @@
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk"; import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
WSOL_ADDRESS,
} from "@certusone/wormhole-sdk";
import { Checkbox, FormControlLabel } from "@material-ui/core"; import { Checkbox, FormControlLabel } from "@material-ui/core";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@ -18,13 +22,18 @@ function Redeem() {
const { handleClick, handleNativeClick, disabled, showLoader } = const { handleClick, handleNativeClick, disabled, showLoader } =
useHandleRedeem(); useHandleRedeem();
const targetChain = useSelector(selectTransferTargetChain); const targetChain = useSelector(selectTransferTargetChain);
const targetAssetHex = useSelector(selectTransferTargetAsset); const targetAsset = useSelector(selectTransferTargetAsset);
const { isReady, statusMessage } = useIsWalletReady(targetChain); const { isReady, statusMessage } = useIsWalletReady(targetChain);
//TODO better check, probably involving a hook & the VAA //TODO better check, probably involving a hook & the VAA
const isNativeEligible = const isEthNative =
targetChain === CHAIN_ID_ETH && targetChain === CHAIN_ID_ETH &&
targetAssetHex && targetAsset &&
targetAssetHex.toLowerCase() === WETH_ADDRESS.toLowerCase(); targetAsset.toLowerCase() === WETH_ADDRESS.toLowerCase();
const isSolNative =
targetChain === CHAIN_ID_SOLANA &&
targetAsset &&
targetAsset === WSOL_ADDRESS;
const isNativeEligible = isEthNative || isSolNative;
const [useNativeRedeem, setUseNativeRedeem] = useState(true); const [useNativeRedeem, setUseNativeRedeem] = useState(true);
const toggleNativeRedeem = useCallback(() => { const toggleNativeRedeem = useCallback(() => {
setUseNativeRedeem(!useNativeRedeem); setUseNativeRedeem(!useNativeRedeem);

View File

@ -14,7 +14,7 @@ import {
selectTransferOriginChain, selectTransferOriginChain,
selectTransferTargetChain, selectTransferTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { hexToNativeString } from "../../utils/array"; import { hexToNativeString } from "@certusone/wormhole-sdk";
export default function RegisterNowButton() { export default function RegisterNowButton() {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@ -1,4 +1,8 @@
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; import {
CHAIN_ID_SOLANA,
CHAIN_ID_ETH,
hexToNativeString,
} from "@certusone/wormhole-sdk";
import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core"; import { makeStyles, MenuItem, TextField, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
@ -18,7 +22,6 @@ import {
UNREGISTERED_ERROR_MESSAGE, UNREGISTERED_ERROR_MESSAGE,
} from "../../store/selectors"; } from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice"; import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { hexToNativeString } from "../../utils/array";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";

View File

@ -1,10 +1,10 @@
import { hexToNativeString } from "@certusone/wormhole-sdk";
import { makeStyles, Typography } from "@material-ui/core"; import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
selectTransferTargetAddressHex, selectTransferTargetAddressHex,
selectTransferTargetChain, selectTransferTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { hexToNativeString } from "../../utils/array";
import { CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS_BY_ID } from "../../utils/consts";
import SmartAddress from "../SmartAddress"; import SmartAddress from "../SmartAddress";

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectAttestSignedVAAHex } from "../store/selectors"; import { selectAttestSignedVAAHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array"; import { hexToUint8Array } from "@certusone/wormhole-sdk";
export default function useAttestSignedVAA() { export default function useAttestSignedVAA() {
const signedVAAHex = useSelector(selectAttestSignedVAAHex); const signedVAAHex = useSelector(selectAttestSignedVAAHex);

View File

@ -7,6 +7,7 @@ import {
getOriginalAssetSol, getOriginalAssetSol,
getOriginalAssetTerra, getOriginalAssetTerra,
WormholeWrappedInfo, WormholeWrappedInfo,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
getOriginalAssetEth as getOriginalAssetEthNFT, getOriginalAssetEth as getOriginalAssetEthNFT,
@ -26,7 +27,6 @@ import {
} from "../store/selectors"; } from "../store/selectors";
import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice"; import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
import { setSourceWormholeWrappedInfo as setTransferSourceWormholeWrappedInfo } from "../store/transferSlice"; import { setSourceWormholeWrappedInfo as setTransferSourceWormholeWrappedInfo } from "../store/transferSlice";
import { uint8ArrayToHex } from "../utils/array";
import { import {
ETH_NFT_BRIDGE_ADDRESS, ETH_NFT_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS,

View File

@ -5,6 +5,8 @@ import {
getForeignAssetEth, getForeignAssetEth,
getForeignAssetSolana, getForeignAssetSolana,
getForeignAssetTerra, getForeignAssetTerra,
hexToNativeString,
hexToUint8Array,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
getForeignAssetEth as getForeignAssetEthNFT, getForeignAssetEth as getForeignAssetEthNFT,
@ -30,7 +32,6 @@ import {
selectTransferTargetChain, selectTransferTargetChain,
} from "../store/selectors"; } from "../store/selectors";
import { setTargetAsset as setTransferTargetAsset } from "../store/transferSlice"; import { setTargetAsset as setTransferTargetAsset } from "../store/transferSlice";
import { hexToNativeString, hexToUint8Array } from "../utils/array";
import { import {
ETH_NFT_BRIDGE_ADDRESS, ETH_NFT_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS,

View File

@ -2,6 +2,8 @@ import {
CHAIN_ID_ETH, CHAIN_ID_ETH,
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
WSOL_ADDRESS,
WSOL_DECIMALS,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers"; import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
import { Dispatch } from "@reduxjs/toolkit"; import { Dispatch } from "@reduxjs/toolkit";
@ -155,6 +157,31 @@ const createParsedTokenAccountFromCovalent = (
}; };
}; };
const createNativeSolParsedTokenAccount = async (
connection: Connection,
walletAddress: string
) => {
const fetchAccounts = await getMultipleAccountsRPC(connection, [
new PublicKey(walletAddress),
]);
if (!fetchAccounts || !fetchAccounts.length || !fetchAccounts[0]) {
return null;
} else {
return createParsedTokenAccount(
walletAddress, //publicKey
WSOL_ADDRESS, //Mint key
fetchAccounts[0].lamports.toString(), //amount
WSOL_DECIMALS, //decimals, 9
parseFloat(formatUnits(fetchAccounts[0].lamports, WSOL_DECIMALS)),
formatUnits(fetchAccounts[0].lamports, WSOL_DECIMALS).toString(),
"SOL",
"Solana",
undefined, //TODO logo. It's in the solana token map, so we could potentially use that URL.
true
);
}
};
const createNativeEthParsedTokenAccount = ( const createNativeEthParsedTokenAccount = (
provider: Provider, provider: Provider,
signerAddress: string | undefined signerAddress: string | undefined
@ -271,7 +298,7 @@ const getEthereumAccountsCovalent = async (
} }
}; };
const getSolanaParsedTokenAccounts = ( const getSolanaParsedTokenAccounts = async (
walletAddress: string, walletAddress: string,
dispatch: Dispatch, dispatch: Dispatch,
nft: boolean nft: boolean
@ -280,29 +307,40 @@ const getSolanaParsedTokenAccounts = (
dispatch( dispatch(
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts() nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
); );
return connection try {
//No matter what, we retrieve the spl tokens associated to this address.
let splParsedTokenAccounts = await connection
.getParsedTokenAccountsByOwner(new PublicKey(walletAddress), { .getParsedTokenAccountsByOwner(new PublicKey(walletAddress), {
programId: new PublicKey(TOKEN_PROGRAM_ID), programId: new PublicKey(TOKEN_PROGRAM_ID),
}) })
.then( .then((result) => {
(result) => { return result.value.map((item) =>
const mappedItems = result.value.map((item) =>
createParsedTokenAccountFromInfo(item.pubkey, item.account) createParsedTokenAccountFromInfo(item.pubkey, item.account)
); );
dispatch( });
nft
? receiveSourceParsedTokenAccountsNFT(mappedItems) if (nft) {
: receiveSourceParsedTokenAccounts(mappedItems) //In the case of NFTs, we are done, and we set the accounts in redux
dispatch(receiveSourceParsedTokenAccountsNFT(splParsedTokenAccounts));
} else {
//In the transfer case, we also pull the SOL balance of the wallet, and prepend it at the beginning of the list.
const nativeAccount = await createNativeSolParsedTokenAccount(
connection,
walletAddress
); );
}, if (nativeAccount !== null) {
(error) => { splParsedTokenAccounts.unshift(nativeAccount);
}
dispatch(receiveSourceParsedTokenAccounts(splParsedTokenAccounts));
}
} catch (e) {
console.error(e);
dispatch( dispatch(
nft nft
? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata") ? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata")
: errorSourceParsedTokenAccounts("Failed to load token metadata.") : errorSourceParsedTokenAccounts("Failed to load token metadata.")
); );
} }
);
}; };
/** /**

View File

@ -11,6 +11,7 @@ import {
parseSequenceFromLogEth, parseSequenceFromLogEth,
parseSequenceFromLogSolana, parseSequenceFromLogSolana,
parseSequenceFromLogTerra, parseSequenceFromLogTerra,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { WalletContextState } from "@solana/wallet-adapter-react"; import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection, PublicKey } from "@solana/web3.js"; import { Connection, PublicKey } from "@solana/web3.js";
@ -36,7 +37,6 @@ import {
selectAttestSourceAsset, selectAttestSourceAsset,
selectAttestSourceChain, selectAttestSourceChain,
} from "../store/selectors"; } from "../store/selectors";
import { uint8ArrayToHex } from "../utils/array";
import { import {
ETH_BRIDGE_ADDRESS, ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS,

View File

@ -3,6 +3,8 @@ import {
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
getClaimAddressSolana, getClaimAddressSolana,
postVaaSolana, postVaaSolana,
parseNFTPayload,
hexToUint8Array,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
createMetaOnSolana, createMetaOnSolana,
@ -18,12 +20,10 @@ import { Signer } from "ethers";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { parsePayload } from "../components/NFT/Recovery";
import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { setIsRedeeming, setRedeemTx } from "../store/nftSlice"; import { setIsRedeeming, setRedeemTx } from "../store/nftSlice";
import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors"; import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors";
import { hexToUint8Array } from "../utils/array";
import { import {
ETH_NFT_BRIDGE_ADDRESS, ETH_NFT_BRIDGE_ADDRESS,
SOLANA_HOST, SOLANA_HOST,
@ -102,7 +102,7 @@ async function solana(
"@certusone/wormhole-sdk/lib/solana/core/bridge" "@certusone/wormhole-sdk/lib/solana/core/bridge"
); );
const parsedVAA = parse_vaa(signedVAA); const parsedVAA = parse_vaa(signedVAA);
const { originChain, originAddress, tokenId } = parsePayload( const { originChain, originAddress, tokenId } = parseNFTPayload(
Buffer.from(new Uint8Array(parsedVAA.payload)) Buffer.from(new Uint8Array(parsedVAA.payload))
); );
const mintAddress = await getForeignAssetSol( const mintAddress = await getForeignAssetSol(

View File

@ -6,6 +6,8 @@ import {
getEmitterAddressSolana, getEmitterAddressSolana,
parseSequenceFromLogEth, parseSequenceFromLogEth,
parseSequenceFromLogSolana, parseSequenceFromLogSolana,
hexToUint8Array,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { import {
transferFromEth, transferFromEth,
@ -37,7 +39,6 @@ import {
selectNFTSourceParsedTokenAccount, selectNFTSourceParsedTokenAccount,
selectNFTTargetChain, selectNFTTargetChain,
} from "../store/selectors"; } from "../store/selectors";
import { hexToUint8Array, uint8ArrayToHex } from "../utils/array";
import { import {
ETH_BRIDGE_ADDRESS, ETH_BRIDGE_ADDRESS,
ETH_NFT_BRIDGE_ADDRESS, ETH_NFT_BRIDGE_ADDRESS,

View File

@ -3,6 +3,7 @@ import {
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
postVaaSolana, postVaaSolana,
redeemAndUnwrapOnSolana,
redeemOnEth, redeemOnEth,
redeemOnEthNative, redeemOnEthNative,
redeemOnSolana, redeemOnSolana,
@ -63,7 +64,8 @@ async function solana(
enqueueSnackbar: any, enqueueSnackbar: any,
wallet: WalletContextState, wallet: WalletContextState,
payerAddress: string, //TODO: we may not need this since we have wallet payerAddress: string, //TODO: we may not need this since we have wallet
signedVAA: Uint8Array signedVAA: Uint8Array,
isNative: boolean
) { ) {
dispatch(setIsRedeeming(true)); dispatch(setIsRedeeming(true));
try { try {
@ -79,7 +81,15 @@ async function solana(
Buffer.from(signedVAA) Buffer.from(signedVAA)
); );
// TODO: how do we retry in between these steps // TODO: how do we retry in between these steps
const transaction = await redeemOnSolana( const transaction = isNative
? await redeemAndUnwrapOnSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA
)
: await redeemOnSolana(
connection, connection,
SOL_BRIDGE_ADDRESS, SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS, SOL_TOKEN_BRIDGE_ADDRESS,
@ -147,7 +157,8 @@ export function useHandleRedeem() {
enqueueSnackbar, enqueueSnackbar,
solanaWallet, solanaWallet,
solPK.toString(), solPK.toString(),
signedVAA signedVAA,
false
); );
} else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) { } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); terra(dispatch, enqueueSnackbar, terraWallet, signedVAA);
@ -181,7 +192,8 @@ export function useHandleRedeem() {
enqueueSnackbar, enqueueSnackbar,
solanaWallet, solanaWallet,
solPK.toString(), solPK.toString(),
signedVAA signedVAA,
true
); );
} else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) { } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); //TODO isNative = true terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); //TODO isNative = true

View File

@ -12,7 +12,10 @@ import {
transferFromEth, transferFromEth,
transferFromEthNative, transferFromEthNative,
transferFromSolana, transferFromSolana,
transferNativeSol,
transferFromTerra, transferFromTerra,
hexToUint8Array,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { WalletContextState } from "@solana/wallet-adapter-react"; import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js"; import { Connection } from "@solana/web3.js";
@ -44,7 +47,6 @@ import {
setSignedVAAHex, setSignedVAAHex,
setTransferTx, setTransferTx,
} from "../store/transferSlice"; } from "../store/transferSlice";
import { hexToUint8Array, uint8ArrayToHex } from "../utils/array";
import { import {
ETH_BRIDGE_ADDRESS, ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS,
@ -121,6 +123,7 @@ async function solana(
decimals: number, decimals: number,
targetChain: ChainId, targetChain: ChainId,
targetAddress: Uint8Array, targetAddress: Uint8Array,
isNative: boolean,
originAddressStr?: string, originAddressStr?: string,
originChain?: ChainId originChain?: ChainId
) { ) {
@ -131,7 +134,17 @@ async function solana(
const originAddress = originAddressStr const originAddress = originAddressStr
? zeroPad(hexToUint8Array(originAddressStr), 32) ? zeroPad(hexToUint8Array(originAddressStr), 32)
: undefined; : undefined;
const transaction = await transferFromSolana( const promise = isNative
? transferNativeSol(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
amountParsed,
targetAddress,
targetChain
)
: transferFromSolana(
connection, connection,
SOL_BRIDGE_ADDRESS, SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS, SOL_TOKEN_BRIDGE_ADDRESS,
@ -144,6 +157,7 @@ async function solana(
originAddress, originAddress,
originChain originChain
); );
const transaction = await promise;
const txid = await signSendAndConfirm(wallet, connection, transaction); const txid = await signSendAndConfirm(wallet, connection, transaction);
enqueueSnackbar("Transaction confirmed", { variant: "success" }); enqueueSnackbar("Transaction confirmed", { variant: "success" });
const info = await connection.getTransaction(txid); const info = await connection.getTransaction(txid);
@ -289,6 +303,7 @@ export function useHandleTransfer() {
decimals, decimals,
targetChain, targetChain,
targetAddress, targetAddress,
isNative,
originAsset, originAsset,
originChain originChain
); );

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectNFTSignedVAAHex } from "../store/selectors"; import { selectNFTSignedVAAHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array"; import { hexToUint8Array } from "@certusone/wormhole-sdk";
export default function useNFTSignedVAA() { export default function useNFTSignedVAA() {
const signedVAAHex = useSelector(selectNFTSignedVAAHex); const signedVAAHex = useSelector(selectNFTSignedVAAHex);

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectNFTTargetAddressHex } from "../store/selectors"; import { selectNFTTargetAddressHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array"; import { hexToUint8Array } from "@certusone/wormhole-sdk";
export default function useNFTTargetAddressHex() { export default function useNFTTargetAddressHex() {
const targetAddressHex = useSelector(selectNFTTargetAddressHex); const targetAddressHex = useSelector(selectNFTTargetAddressHex);

View File

@ -3,6 +3,7 @@ import {
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
canonicalAddress, canonicalAddress,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { arrayify, zeroPad } from "@ethersproject/bytes"; import { arrayify, zeroPad } from "@ethersproject/bytes";
import { import {
@ -25,7 +26,6 @@ import {
} from "../store/selectors"; } from "../store/selectors";
import { setTargetAddressHex as setNFTTargetAddressHex } from "../store/nftSlice"; import { setTargetAddressHex as setNFTTargetAddressHex } from "../store/nftSlice";
import { setTargetAddressHex as setTransferTargetAddressHex } from "../store/transferSlice"; import { setTargetAddressHex as setTransferTargetAddressHex } from "../store/transferSlice";
import { uint8ArrayToHex } from "../utils/array";
function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) { function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectTransferSignedVAAHex } from "../store/selectors"; import { selectTransferSignedVAAHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array"; import { hexToUint8Array } from "@certusone/wormhole-sdk";
export default function useTransferSignedVAA() { export default function useTransferSignedVAA() {
const signedVAAHex = useSelector(selectTransferSignedVAAHex); const signedVAAHex = useSelector(selectTransferSignedVAAHex);

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectTransferTargetAddressHex } from "../store/selectors"; import { selectTransferTargetAddressHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array"; import { hexToUint8Array } from "@certusone/wormhole-sdk";
export default function useTransferTargetAddressHex() { export default function useTransferTargetAddressHex() {
const targetAddressHex = useSelector(selectTransferTargetAddressHex); const targetAddressHex = useSelector(selectTransferTargetAddressHex);

View File

@ -1,10 +1,24 @@
import { Connection, PublicKey, Transaction } from "@solana/web3.js"; import { AccountLayout, Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js"; import { MsgExecuteContract } from "@terra-money/terra.js";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { fromUint8Array } from "js-base64"; import { fromUint8Array } from "js-base64";
import { Bridge__factory } from "../ethers-contracts"; import { Bridge__factory } from "../ethers-contracts";
import { ixFromRust } from "../solana"; import { ixFromRust } from "../solana";
import { CHAIN_ID_SOLANA } from "../utils"; import {
CHAIN_ID_SOLANA,
WSOL_ADDRESS,
WSOL_DECIMALS,
MAX_VAA_DECIMALS,
} from "../utils";
import { hexToNativeString } from "../utils/array";
import { parseTransferPayload } from "../utils/parseVaa";
export async function redeemOnEth( export async function redeemOnEth(
tokenBridgeAddress: string, tokenBridgeAddress: string,
@ -45,6 +59,96 @@ export async function redeemOnTerra(
); );
} }
export async function redeemAndUnwrapOnSolana(
connection: Connection,
bridgeAddress: string,
tokenBridgeAddress: string,
payerAddress: string,
signedVAA: Uint8Array
) {
const { parse_vaa } = await import("../solana/core/bridge");
const { complete_transfer_native_ix } = await import(
"../solana/token/token_bridge"
);
const parsedVAA = parse_vaa(signedVAA);
const parsedPayload = parseTransferPayload(
Buffer.from(new Uint8Array(parsedVAA.payload))
);
const targetAddress = hexToNativeString(
parsedPayload.targetAddress,
CHAIN_ID_SOLANA
);
if (!targetAddress) {
throw new Error("Failed to read the target address.");
}
const targetPublicKey = new PublicKey(targetAddress);
const targetAmount =
parsedPayload.amount *
BigInt(WSOL_DECIMALS - MAX_VAA_DECIMALS) *
BigInt(10);
const rentBalance = await Token.getMinBalanceRentForExemptAccount(connection);
const mintPublicKey = new PublicKey(WSOL_ADDRESS);
const payerPublicKey = new PublicKey(payerAddress);
const ancillaryKeypair = Keypair.generate();
const completeTransferIx = ixFromRust(
complete_transfer_native_ix(
tokenBridgeAddress,
bridgeAddress,
payerAddress,
signedVAA
)
);
//This will create a temporary account where the wSOL will be moved
const createAncillaryAccountIx = SystemProgram.createAccount({
fromPubkey: payerPublicKey,
newAccountPubkey: ancillaryKeypair.publicKey,
lamports: rentBalance, //spl token accounts need rent exemption
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
});
//Initialize the account as a WSOL account, with the original payerAddress as owner
const initAccountIx = await Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
mintPublicKey,
ancillaryKeypair.publicKey,
payerPublicKey
);
//Send in the amount of wSOL which we want converted to SOL
const balanceTransferIx = Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
targetPublicKey,
ancillaryKeypair.publicKey,
payerPublicKey,
[],
new u64(targetAmount.toString(16), 16)
);
//Close the ancillary account for cleanup. Payer address receives any remaining funds
const closeAccountIx = Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
ancillaryKeypair.publicKey, //account to close
payerPublicKey, //Remaining funds destination
payerPublicKey, //authority
[]
);
const { blockhash } = await connection.getRecentBlockhash();
const transaction = new Transaction();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerAddress);
transaction.add(completeTransferIx);
transaction.add(createAncillaryAccountIx);
transaction.add(initAccountIx);
transaction.add(balanceTransferIx);
transaction.add(closeAccountIx);
transaction.partialSign(ancillaryKeypair);
return transaction;
}
export async function redeemOnSolana( export async function redeemOnSolana(
connection: Connection, connection: Connection,
bridgeAddress: string, bridgeAddress: string,

View File

@ -1,5 +1,11 @@
import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; import { AccountLayout, Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js"; import { MsgExecuteContract } from "@terra-money/terra.js";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { import {
@ -7,7 +13,7 @@ import {
TokenImplementation__factory, TokenImplementation__factory,
} from "../ethers-contracts"; } from "../ethers-contracts";
import { getBridgeFeeIx, ixFromRust } from "../solana"; import { getBridgeFeeIx, ixFromRust } from "../solana";
import { ChainId, CHAIN_ID_SOLANA, createNonce } from "../utils"; import { ChainId, CHAIN_ID_SOLANA, createNonce, WSOL_ADDRESS } from "../utils";
export async function getAllowanceEth( export async function getAllowanceEth(
tokenBridgeAddress: string, tokenBridgeAddress: string,
@ -117,6 +123,104 @@ export async function transferFromTerra(
]; ];
} }
export async function transferNativeSol(
connection: Connection,
bridgeAddress: string,
tokenBridgeAddress: string,
payerAddress: string,
amount: BigInt,
targetAddress: Uint8Array,
targetChain: ChainId
) {
//https://github.com/solana-labs/solana-program-library/blob/master/token/js/client/token.js
const rentBalance = await Token.getMinBalanceRentForExemptAccount(connection);
const mintPublicKey = new PublicKey(WSOL_ADDRESS);
const payerPublicKey = new PublicKey(payerAddress);
const ancillaryKeypair = Keypair.generate();
//This will create a temporary account where the wSOL will be created.
const createAncillaryAccountIx = SystemProgram.createAccount({
fromPubkey: payerPublicKey,
newAccountPubkey: ancillaryKeypair.publicKey,
lamports: rentBalance, //spl token accounts need rent exemption
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
});
//Send in the amount of SOL which we want converted to wSOL
const initialBalanceTransferIx = SystemProgram.transfer({
fromPubkey: payerPublicKey,
lamports: Number(amount),
toPubkey: ancillaryKeypair.publicKey,
});
//Initialize the account as a WSOL account, with the original payerAddress as owner
const initAccountIx = await Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
mintPublicKey,
ancillaryKeypair.publicKey,
payerPublicKey
);
//Normal approve & transfer instructions, except that the wSOL is sent from the ancillary account.
const { transfer_native_ix, approval_authority_address } = await import(
"../solana/token/token_bridge"
);
const nonce = createNonce().readUInt32LE(0);
const fee = BigInt(0); // for now, this won't do anything, we may add later
const transferIx = await getBridgeFeeIx(
connection,
bridgeAddress,
payerAddress
);
const approvalIx = Token.createApproveInstruction(
TOKEN_PROGRAM_ID,
ancillaryKeypair.publicKey,
new PublicKey(approval_authority_address(tokenBridgeAddress)),
payerPublicKey, //owner
[],
new u64(amount.toString(16), 16)
);
let messageKey = Keypair.generate();
const ix = ixFromRust(
transfer_native_ix(
tokenBridgeAddress,
bridgeAddress,
payerAddress,
messageKey.publicKey.toString(),
ancillaryKeypair.publicKey.toString(),
WSOL_ADDRESS,
nonce,
amount,
fee,
targetAddress,
targetChain
)
);
//Close the ancillary account for cleanup. Payer address receives any remaining funds
const closeAccountIx = Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
ancillaryKeypair.publicKey, //account to close
payerPublicKey, //Remaining funds destination
payerPublicKey, //authority
[]
);
const { blockhash } = await connection.getRecentBlockhash();
const transaction = new Transaction();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerAddress);
transaction.add(createAncillaryAccountIx);
transaction.add(initialBalanceTransferIx);
transaction.add(initAccountIx);
transaction.add(transferIx, approvalIx, ix);
transaction.add(closeAccountIx);
transaction.partialSign(messageKey);
transaction.partialSign(ancillaryKeypair);
return transaction;
}
export async function transferFromSolana( export async function transferFromSolana(
connection: Connection, connection: Connection,
bridgeAddress: string, bridgeAddress: string,

View File

@ -3,8 +3,8 @@ import {
CHAIN_ID_ETH, CHAIN_ID_ETH,
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
humanAddress, } from "./consts";
} from "@certusone/wormhole-sdk"; import { humanAddress } from "../terra";
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js";
import { hexValue, hexZeroPad } from "ethers/lib/utils"; import { hexValue, hexZeroPad } from "ethers/lib/utils";

View File

@ -3,3 +3,7 @@ export const CHAIN_ID_SOLANA: ChainId = 1;
export const CHAIN_ID_ETH: ChainId = 2; export const CHAIN_ID_ETH: ChainId = 2;
export const CHAIN_ID_TERRA: ChainId = 3; export const CHAIN_ID_TERRA: ChainId = 3;
export const CHAIN_ID_BSC: ChainId = 4; export const CHAIN_ID_BSC: ChainId = 4;
export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112";
export const WSOL_DECIMALS = 9;
export const MAX_VAA_DECIMALS = 8;

View File

@ -1,2 +1,4 @@
export * from "./consts"; export * from "./consts";
export * from "./createNonce"; export * from "./createNonce";
export * from "./parseVaa";
export * from "./array";

View File

@ -0,0 +1,84 @@
import { BigNumber } from "@ethersproject/bignumber";
import { ChainId } from "./consts";
export const METADATA_REPLACE = new RegExp("\u0000", "g");
// note: actual first byte is message type
// 0 [u8; 32] token_address
// 32 u16 token_chain
// 34 [u8; 32] symbol
// 66 [u8; 32] name
// 98 u256 tokenId
// 130 u8 uri_len
// 131 [u8;len] uri
// ? [u8; 32] recipient
// ? u16 recipient_chain
export const parseNFTPayload = (arr: Buffer) => {
const originAddress = arr.slice(1, 1 + 32).toString("hex");
const originChain = arr.readUInt16BE(33) as ChainId;
const symbol = Buffer.from(arr.slice(35, 35 + 32))
.toString("utf8")
.replace(METADATA_REPLACE, "");
const name = Buffer.from(arr.slice(67, 67 + 32))
.toString("utf8")
.replace(METADATA_REPLACE, "");
const tokenId = BigNumber.from(arr.slice(99, 99 + 32));
const uri_len = arr.readUInt8(131);
const uri = Buffer.from(arr.slice(132, 132 + uri_len))
.toString("utf8")
.replace(METADATA_REPLACE, "");
const target_offset = 132 + uri_len;
const targetAddress = arr
.slice(target_offset, target_offset + 32)
.toString("hex");
const targetChain = arr.readUInt16BE(target_offset + 32) as ChainId;
return {
originAddress,
originChain,
symbol,
name,
tokenId,
uri,
targetAddress,
targetChain,
};
};
// 0 u256 amount
// 32 [u8; 32] token_address
// 64 u16 token_chain
// 66 [u8; 32] recipient
// 98 u16 recipient_chain
// 100 u256 fee
export const parseTransferPayload = (arr: Buffer) => ({
amount: BigNumber.from(arr.slice(1, 1 + 32)).toBigInt(),
originAddress: arr.slice(33, 33 + 32).toString("hex"),
originChain: arr.readUInt16BE(65) as ChainId,
targetAddress: arr.slice(67, 67 + 32).toString("hex"),
targetChain: arr.readUInt16BE(99) as ChainId,
});
//This returns a corrected amount, which accounts for the difference between the VAA
//decimals, and the decimals of the asset.
// const normalizeVaaAmount = (
// amount: bigint,
// assetDecimals: number
// ): bigint => {
// const MAX_VAA_DECIMALS = 8;
// if (assetDecimals <= MAX_VAA_DECIMALS) {
// return amount;
// }
// const decimalStringVaa = formatUnits(amount, MAX_VAA_DECIMALS);
// const normalizedAmount = parseUnits(decimalStringVaa, assetDecimals);
// const normalizedBigInt = BigInt(truncate(normalizedAmount.toString(), 0));
// return normalizedBigInt;
// };
// function truncate(str: string, maxDecimalDigits: number) {
// if (str.includes(".")) {
// const parts = str.split(".");
// return parts[0] + "." + parts[1].slice(0, maxDecimalDigits);
// }
// return str;
// }