From 7b1a0bf3ad70bdd2bba667944cf34d3c0b61c516 Mon Sep 17 00:00:00 2001 From: Chase Moran Date: Wed, 22 Sep 2021 15:07:21 -0400 Subject: [PATCH] bridge_ui: transfer & unwrap SOL fixes https://github.com/certusone/wormhole/issues/486 Change-Id: I81b97ff0e1358bf0b88567ba9872ee615344a27c --- bridge_ui/src/components/NFT/Recovery.tsx | 60 ++-------- bridge_ui/src/components/NFT/Target.tsx | 8 +- .../src/components/NFT/TargetPreview.tsx | 2 +- .../src/components/NFTOriginVerifier.tsx | 3 +- .../SolanaSourceTokenSelector.tsx | 33 ++++-- .../src/components/Transfer/Recovery.tsx | 32 ++--- bridge_ui/src/components/Transfer/Redeem.tsx | 19 ++- .../components/Transfer/RegisterNowButton.tsx | 2 +- bridge_ui/src/components/Transfer/Target.tsx | 7 +- .../src/components/Transfer/TargetPreview.tsx | 2 +- bridge_ui/src/hooks/useAttestSignedVAA.ts | 2 +- .../src/hooks/useCheckIfWormholeWrapped.ts | 2 +- bridge_ui/src/hooks/useFetchTargetAsset.ts | 3 +- .../hooks/useGetSourceParsedTokenAccounts.ts | 78 +++++++++---- bridge_ui/src/hooks/useHandleAttest.ts | 2 +- bridge_ui/src/hooks/useHandleNFTRedeem.ts | 6 +- bridge_ui/src/hooks/useHandleNFTTransfer.ts | 3 +- bridge_ui/src/hooks/useHandleRedeem.ts | 32 +++-- bridge_ui/src/hooks/useHandleTransfer.ts | 43 ++++--- bridge_ui/src/hooks/useNFTSignedVAA.ts | 2 +- bridge_ui/src/hooks/useNFTTargetAddress.ts | 2 +- bridge_ui/src/hooks/useSyncTargetAddress.ts | 2 +- bridge_ui/src/hooks/useTransferSignedVAA.ts | 2 +- .../src/hooks/useTransferTargetAddress.ts | 2 +- sdk/js/src/token_bridge/redeem.ts | 108 ++++++++++++++++- sdk/js/src/token_bridge/transfer.ts | 110 +++++++++++++++++- {bridge_ui => sdk/js}/src/utils/array.ts | 4 +- sdk/js/src/utils/consts.ts | 4 + sdk/js/src/utils/index.ts | 2 + sdk/js/src/utils/parseVaa.ts | 84 +++++++++++++ 30 files changed, 498 insertions(+), 163 deletions(-) rename {bridge_ui => sdk/js}/src/utils/array.ts (92%) create mode 100644 sdk/js/src/utils/parseVaa.ts diff --git a/bridge_ui/src/components/NFT/Recovery.tsx b/bridge_ui/src/components/NFT/Recovery.tsx index a6e47a14..3eb923a6 100644 --- a/bridge_ui/src/components/NFT/Recovery.tsx +++ b/bridge_ui/src/components/NFT/Recovery.tsx @@ -1,12 +1,15 @@ import { - ChainId, CHAIN_ID_BSC, CHAIN_ID_ETH, CHAIN_ID_SOLANA, getEmitterAddressEth, getEmitterAddressSolana, + hexToNativeString, + hexToUint8Array, + parseNFTPayload, parseSequenceFromLogEth, parseSequenceFromLogSolana, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { Box, @@ -27,7 +30,7 @@ import { import { Restore } from "@material-ui/icons"; import { Alert } from "@material-ui/lab"; import { Connection } from "@solana/web3.js"; -import { BigNumber, ethers } from "ethers"; +import { ethers } from "ethers"; import { useSnackbar } from "notistack"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -37,11 +40,6 @@ import { selectNFTSignedVAAHex, selectNFTSourceChain, } from "../../store/selectors"; -import { - hexToNativeString, - hexToUint8Array, - uint8ArrayToHex, -} from "../../utils/array"; import { CHAINS, ETH_BRIDGE_ADDRESS, @@ -51,7 +49,6 @@ import { WORMHOLE_RPC_HOSTS, } from "../../utils/consts"; import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry"; -import { METADATA_REPLACE } from "../../utils/metaplex"; import parseError from "../../utils/parseError"; 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({ onClose, disabled, @@ -266,7 +220,9 @@ function RecoveryDialogContent({ const parsedPayload = useMemo( () => recoveryParsedVAA?.payload - ? parsePayload(Buffer.from(new Uint8Array(recoveryParsedVAA.payload))) + ? parseNFTPayload( + Buffer.from(new Uint8Array(recoveryParsedVAA.payload)) + ) : null, [recoveryParsedVAA] ); diff --git a/bridge_ui/src/components/NFT/Target.tsx b/bridge_ui/src/components/NFT/Target.tsx index 4b7d47da..192228cf 100644 --- a/bridge_ui/src/components/NFT/Target.tsx +++ b/bridge_ui/src/components/NFT/Target.tsx @@ -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 { Alert } from "@material-ui/lab"; import { PublicKey } from "@solana/web3.js"; @@ -22,7 +27,6 @@ import { selectNFTTargetChain, selectNFTTargetError, } from "../../store/selectors"; -import { hexToNativeString, hexToUint8Array } from "../../utils/array"; import { CHAINS, CHAINS_BY_ID } from "../../utils/consts"; import ButtonWithLoader from "../ButtonWithLoader"; import KeyAndBalance from "../KeyAndBalance"; diff --git a/bridge_ui/src/components/NFT/TargetPreview.tsx b/bridge_ui/src/components/NFT/TargetPreview.tsx index ccb04943..f616c583 100644 --- a/bridge_ui/src/components/NFT/TargetPreview.tsx +++ b/bridge_ui/src/components/NFT/TargetPreview.tsx @@ -4,7 +4,7 @@ import { selectNFTTargetAddressHex, selectNFTTargetChain, } from "../../store/selectors"; -import { hexToNativeString } from "../../utils/array"; +import { hexToNativeString } from "@certusone/wormhole-sdk"; import { CHAINS_BY_ID } from "../../utils/consts"; import SmartAddress from "../SmartAddress"; diff --git a/bridge_ui/src/components/NFTOriginVerifier.tsx b/bridge_ui/src/components/NFTOriginVerifier.tsx index a6a90f47..268d049a 100644 --- a/bridge_ui/src/components/NFTOriginVerifier.tsx +++ b/bridge_ui/src/components/NFTOriginVerifier.tsx @@ -2,6 +2,8 @@ import { CHAIN_ID_BSC, CHAIN_ID_ETH, CHAIN_ID_SOLANA, + hexToNativeString, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { getOriginalAssetEth, @@ -27,7 +29,6 @@ import useIsWalletReady from "../hooks/useIsWalletReady"; import { getMetaplexData } from "../hooks/useMetaplexData"; import { COLORS } from "../muiTheme"; import { NFTParsedTokenAccount } from "../store/nftSlice"; -import { hexToNativeString, uint8ArrayToHex } from "../utils/array"; import { CHAINS, CHAINS_BY_ID, diff --git a/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx b/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx index b6d4f4a3..650771e0 100644 --- a/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx +++ b/bridge_ui/src/components/TokenSelectors/SolanaSourceTokenSelector.tsx @@ -85,6 +85,7 @@ export default function SolanaSourceTokenSelector( const getLogo = useCallback( (account: ParsedTokenAccount) => { return ( + (account.isNativeAsset && account.logo) || memoizedTokenMap.get(account.mintKey)?.logoURI || metaplex.data?.get(account.mintKey)?.data?.uri || undefined @@ -96,6 +97,7 @@ export default function SolanaSourceTokenSelector( const getSymbol = useCallback( (account: ParsedTokenAccount) => { return ( + (account.isNativeAsset && account.symbol) || memoizedTokenMap.get(account.mintKey)?.symbol || metaplex.data?.get(account.mintKey)?.data?.symbol || undefined @@ -107,6 +109,7 @@ export default function SolanaSourceTokenSelector( const getName = useCallback( (account: ParsedTokenAccount) => { return ( + (account.isNativeAsset && account.name) || memoizedTokenMap.get(account.mintKey)?.name || metaplex.data?.get(account.mintKey)?.data?.name || undefined @@ -175,12 +178,18 @@ export default function SolanaSourceTokenSelector( {name}
- - {"Mint : " + mintPrettyString} - - - {"Account :" + accountAddressPrettyString} - + {account.isNativeAsset ? ( + {"Native"} + ) : ( + <> + + {"Mint : " + mintPrettyString} + + + {"Account :" + accountAddressPrettyString} + + + )}
{"Balance"} @@ -282,11 +291,15 @@ export default function SolanaSourceTokenSelector( ); const getOptionLabel = useCallback( - (option) => { + (option: ParsedTokenAccount) => { const symbol = getSymbol(option); - return `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress( - option.publicKey - )}, Mint: ${shortenAddress(option.mintKey)})`; + const label = option.isNativeAsset + ? symbol || "" //default for non-null guarantee + : `${symbol ? symbol : "Unknown"} (Account: ${shortenAddress( + option.publicKey + )}, Mint: ${shortenAddress(option.mintKey)})`; + + return label; }, [getSymbol] ); diff --git a/bridge_ui/src/components/Transfer/Recovery.tsx b/bridge_ui/src/components/Transfer/Recovery.tsx index cb1d3aba..048193bd 100644 --- a/bridge_ui/src/components/Transfer/Recovery.tsx +++ b/bridge_ui/src/components/Transfer/Recovery.tsx @@ -1,5 +1,4 @@ import { - ChainId, CHAIN_ID_BSC, CHAIN_ID_ETH, CHAIN_ID_SOLANA, @@ -7,9 +6,13 @@ import { getEmitterAddressEth, getEmitterAddressSolana, getEmitterAddressTerra, + hexToNativeString, + hexToUint8Array, parseSequenceFromLogEth, parseSequenceFromLogSolana, parseSequenceFromLogTerra, + parseTransferPayload, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { Box, @@ -31,7 +34,7 @@ import { Restore } from "@material-ui/icons"; import { Alert } from "@material-ui/lab"; import { Connection } from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; -import { BigNumber, ethers } from "ethers"; +import { ethers } from "ethers"; import { useSnackbar } from "notistack"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -45,11 +48,6 @@ import { setStep, setTargetChain, } from "../../store/transferSlice"; -import { - hexToNativeString, - hexToUint8Array, - uint8ArrayToHex, -} from "../../utils/array"; import { CHAINS, 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({ onClose, disabled, @@ -288,7 +270,9 @@ function RecoveryDialogContent({ const parsedPayload = useMemo( () => recoveryParsedVAA?.payload - ? parsePayload(Buffer.from(new Uint8Array(recoveryParsedVAA.payload))) + ? parseTransferPayload( + Buffer.from(new Uint8Array(recoveryParsedVAA.payload)) + ) : null, [recoveryParsedVAA] ); diff --git a/bridge_ui/src/components/Transfer/Redeem.tsx b/bridge_ui/src/components/Transfer/Redeem.tsx index 1a4e1e6d..28b9a0e2 100644 --- a/bridge_ui/src/components/Transfer/Redeem.tsx +++ b/bridge_ui/src/components/Transfer/Redeem.tsx @@ -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 { useCallback, useState } from "react"; import { useSelector } from "react-redux"; @@ -18,13 +22,18 @@ function Redeem() { const { handleClick, handleNativeClick, disabled, showLoader } = useHandleRedeem(); const targetChain = useSelector(selectTransferTargetChain); - const targetAssetHex = useSelector(selectTransferTargetAsset); + const targetAsset = useSelector(selectTransferTargetAsset); const { isReady, statusMessage } = useIsWalletReady(targetChain); //TODO better check, probably involving a hook & the VAA - const isNativeEligible = + const isEthNative = targetChain === CHAIN_ID_ETH && - targetAssetHex && - targetAssetHex.toLowerCase() === WETH_ADDRESS.toLowerCase(); + targetAsset && + 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 toggleNativeRedeem = useCallback(() => { setUseNativeRedeem(!useNativeRedeem); diff --git a/bridge_ui/src/components/Transfer/RegisterNowButton.tsx b/bridge_ui/src/components/Transfer/RegisterNowButton.tsx index 93b4bb00..c7cab184 100644 --- a/bridge_ui/src/components/Transfer/RegisterNowButton.tsx +++ b/bridge_ui/src/components/Transfer/RegisterNowButton.tsx @@ -14,7 +14,7 @@ import { selectTransferOriginChain, selectTransferTargetChain, } from "../../store/selectors"; -import { hexToNativeString } from "../../utils/array"; +import { hexToNativeString } from "@certusone/wormhole-sdk"; export default function RegisterNowButton() { const dispatch = useDispatch(); diff --git a/bridge_ui/src/components/Transfer/Target.tsx b/bridge_ui/src/components/Transfer/Target.tsx index 9be1a4c6..497b9f0f 100644 --- a/bridge_ui/src/components/Transfer/Target.tsx +++ b/bridge_ui/src/components/Transfer/Target.tsx @@ -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 { Alert } from "@material-ui/lab"; import { useCallback, useMemo } from "react"; @@ -18,7 +22,6 @@ import { UNREGISTERED_ERROR_MESSAGE, } from "../../store/selectors"; import { incrementStep, setTargetChain } from "../../store/transferSlice"; -import { hexToNativeString } from "../../utils/array"; import { CHAINS, CHAINS_BY_ID } from "../../utils/consts"; import ButtonWithLoader from "../ButtonWithLoader"; import KeyAndBalance from "../KeyAndBalance"; diff --git a/bridge_ui/src/components/Transfer/TargetPreview.tsx b/bridge_ui/src/components/Transfer/TargetPreview.tsx index b0542451..63b826a4 100644 --- a/bridge_ui/src/components/Transfer/TargetPreview.tsx +++ b/bridge_ui/src/components/Transfer/TargetPreview.tsx @@ -1,10 +1,10 @@ +import { hexToNativeString } from "@certusone/wormhole-sdk"; import { makeStyles, Typography } from "@material-ui/core"; import { useSelector } from "react-redux"; import { selectTransferTargetAddressHex, selectTransferTargetChain, } from "../../store/selectors"; -import { hexToNativeString } from "../../utils/array"; import { CHAINS_BY_ID } from "../../utils/consts"; import SmartAddress from "../SmartAddress"; diff --git a/bridge_ui/src/hooks/useAttestSignedVAA.ts b/bridge_ui/src/hooks/useAttestSignedVAA.ts index d4c5bd66..9a905fac 100644 --- a/bridge_ui/src/hooks/useAttestSignedVAA.ts +++ b/bridge_ui/src/hooks/useAttestSignedVAA.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; import { selectAttestSignedVAAHex } from "../store/selectors"; -import { hexToUint8Array } from "../utils/array"; +import { hexToUint8Array } from "@certusone/wormhole-sdk"; export default function useAttestSignedVAA() { const signedVAAHex = useSelector(selectAttestSignedVAAHex); diff --git a/bridge_ui/src/hooks/useCheckIfWormholeWrapped.ts b/bridge_ui/src/hooks/useCheckIfWormholeWrapped.ts index 1f859057..6991018c 100644 --- a/bridge_ui/src/hooks/useCheckIfWormholeWrapped.ts +++ b/bridge_ui/src/hooks/useCheckIfWormholeWrapped.ts @@ -7,6 +7,7 @@ import { getOriginalAssetSol, getOriginalAssetTerra, WormholeWrappedInfo, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { getOriginalAssetEth as getOriginalAssetEthNFT, @@ -26,7 +27,6 @@ import { } from "../store/selectors"; import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice"; import { setSourceWormholeWrappedInfo as setTransferSourceWormholeWrappedInfo } from "../store/transferSlice"; -import { uint8ArrayToHex } from "../utils/array"; import { ETH_NFT_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS, diff --git a/bridge_ui/src/hooks/useFetchTargetAsset.ts b/bridge_ui/src/hooks/useFetchTargetAsset.ts index 5f12d77c..99bc1630 100644 --- a/bridge_ui/src/hooks/useFetchTargetAsset.ts +++ b/bridge_ui/src/hooks/useFetchTargetAsset.ts @@ -5,6 +5,8 @@ import { getForeignAssetEth, getForeignAssetSolana, getForeignAssetTerra, + hexToNativeString, + hexToUint8Array, } from "@certusone/wormhole-sdk"; import { getForeignAssetEth as getForeignAssetEthNFT, @@ -30,7 +32,6 @@ import { selectTransferTargetChain, } from "../store/selectors"; import { setTargetAsset as setTransferTargetAsset } from "../store/transferSlice"; -import { hexToNativeString, hexToUint8Array } from "../utils/array"; import { ETH_NFT_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS, diff --git a/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts b/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts index ded514f2..21cbdc18 100644 --- a/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts +++ b/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts @@ -2,6 +2,8 @@ import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, CHAIN_ID_TERRA, + WSOL_ADDRESS, + WSOL_DECIMALS, } from "@certusone/wormhole-sdk"; import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers"; 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 = ( provider: Provider, signerAddress: string | undefined @@ -271,7 +298,7 @@ const getEthereumAccountsCovalent = async ( } }; -const getSolanaParsedTokenAccounts = ( +const getSolanaParsedTokenAccounts = async ( walletAddress: string, dispatch: Dispatch, nft: boolean @@ -280,29 +307,40 @@ const getSolanaParsedTokenAccounts = ( dispatch( nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts() ); - return connection - .getParsedTokenAccountsByOwner(new PublicKey(walletAddress), { - programId: new PublicKey(TOKEN_PROGRAM_ID), - }) - .then( - (result) => { - const mappedItems = result.value.map((item) => + try { + //No matter what, we retrieve the spl tokens associated to this address. + let splParsedTokenAccounts = await connection + .getParsedTokenAccountsByOwner(new PublicKey(walletAddress), { + programId: new PublicKey(TOKEN_PROGRAM_ID), + }) + .then((result) => { + return result.value.map((item) => createParsedTokenAccountFromInfo(item.pubkey, item.account) ); - dispatch( - nft - ? receiveSourceParsedTokenAccountsNFT(mappedItems) - : receiveSourceParsedTokenAccounts(mappedItems) - ); - }, - (error) => { - dispatch( - nft - ? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata") - : errorSourceParsedTokenAccounts("Failed to load token metadata.") - ); + }); + + if (nft) { + //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) { + splParsedTokenAccounts.unshift(nativeAccount); } + dispatch(receiveSourceParsedTokenAccounts(splParsedTokenAccounts)); + } + } catch (e) { + console.error(e); + dispatch( + nft + ? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata") + : errorSourceParsedTokenAccounts("Failed to load token metadata.") ); + } }; /** diff --git a/bridge_ui/src/hooks/useHandleAttest.ts b/bridge_ui/src/hooks/useHandleAttest.ts index d4eb74d9..8ea36890 100644 --- a/bridge_ui/src/hooks/useHandleAttest.ts +++ b/bridge_ui/src/hooks/useHandleAttest.ts @@ -11,6 +11,7 @@ import { parseSequenceFromLogEth, parseSequenceFromLogSolana, parseSequenceFromLogTerra, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { WalletContextState } from "@solana/wallet-adapter-react"; import { Connection, PublicKey } from "@solana/web3.js"; @@ -36,7 +37,6 @@ import { selectAttestSourceAsset, selectAttestSourceChain, } from "../store/selectors"; -import { uint8ArrayToHex } from "../utils/array"; import { ETH_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS, diff --git a/bridge_ui/src/hooks/useHandleNFTRedeem.ts b/bridge_ui/src/hooks/useHandleNFTRedeem.ts index 75205f01..2136d15d 100644 --- a/bridge_ui/src/hooks/useHandleNFTRedeem.ts +++ b/bridge_ui/src/hooks/useHandleNFTRedeem.ts @@ -3,6 +3,8 @@ import { CHAIN_ID_SOLANA, getClaimAddressSolana, postVaaSolana, + parseNFTPayload, + hexToUint8Array, } from "@certusone/wormhole-sdk"; import { createMetaOnSolana, @@ -18,12 +20,10 @@ import { Signer } from "ethers"; import { useSnackbar } from "notistack"; import { useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { parsePayload } from "../components/NFT/Recovery"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import { setIsRedeeming, setRedeemTx } from "../store/nftSlice"; import { selectNFTIsRedeeming, selectNFTTargetChain } from "../store/selectors"; -import { hexToUint8Array } from "../utils/array"; import { ETH_NFT_BRIDGE_ADDRESS, SOLANA_HOST, @@ -102,7 +102,7 @@ async function solana( "@certusone/wormhole-sdk/lib/solana/core/bridge" ); const parsedVAA = parse_vaa(signedVAA); - const { originChain, originAddress, tokenId } = parsePayload( + const { originChain, originAddress, tokenId } = parseNFTPayload( Buffer.from(new Uint8Array(parsedVAA.payload)) ); const mintAddress = await getForeignAssetSol( diff --git a/bridge_ui/src/hooks/useHandleNFTTransfer.ts b/bridge_ui/src/hooks/useHandleNFTTransfer.ts index 8b98d0f4..8c9ff242 100644 --- a/bridge_ui/src/hooks/useHandleNFTTransfer.ts +++ b/bridge_ui/src/hooks/useHandleNFTTransfer.ts @@ -6,6 +6,8 @@ import { getEmitterAddressSolana, parseSequenceFromLogEth, parseSequenceFromLogSolana, + hexToUint8Array, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { transferFromEth, @@ -37,7 +39,6 @@ import { selectNFTSourceParsedTokenAccount, selectNFTTargetChain, } from "../store/selectors"; -import { hexToUint8Array, uint8ArrayToHex } from "../utils/array"; import { ETH_BRIDGE_ADDRESS, ETH_NFT_BRIDGE_ADDRESS, diff --git a/bridge_ui/src/hooks/useHandleRedeem.ts b/bridge_ui/src/hooks/useHandleRedeem.ts index 298c422b..a5535124 100644 --- a/bridge_ui/src/hooks/useHandleRedeem.ts +++ b/bridge_ui/src/hooks/useHandleRedeem.ts @@ -3,6 +3,7 @@ import { CHAIN_ID_SOLANA, CHAIN_ID_TERRA, postVaaSolana, + redeemAndUnwrapOnSolana, redeemOnEth, redeemOnEthNative, redeemOnSolana, @@ -63,7 +64,8 @@ async function solana( enqueueSnackbar: any, wallet: WalletContextState, payerAddress: string, //TODO: we may not need this since we have wallet - signedVAA: Uint8Array + signedVAA: Uint8Array, + isNative: boolean ) { dispatch(setIsRedeeming(true)); try { @@ -79,13 +81,21 @@ async function solana( Buffer.from(signedVAA) ); // TODO: how do we retry in between these steps - const transaction = await redeemOnSolana( - connection, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - payerAddress, - signedVAA - ); + const transaction = isNative + ? await redeemAndUnwrapOnSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + signedVAA + ) + : await redeemOnSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + signedVAA + ); const txid = await signSendAndConfirm(wallet, connection, transaction); // TODO: didn't want to make an info call we didn't need, can we get the block without it by modifying the above call? dispatch(setRedeemTx({ id: txid, block: 1 })); @@ -147,7 +157,8 @@ export function useHandleRedeem() { enqueueSnackbar, solanaWallet, solPK.toString(), - signedVAA + signedVAA, + false ); } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) { terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); @@ -181,7 +192,8 @@ export function useHandleRedeem() { enqueueSnackbar, solanaWallet, solPK.toString(), - signedVAA + signedVAA, + true ); } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) { terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); //TODO isNative = true diff --git a/bridge_ui/src/hooks/useHandleTransfer.ts b/bridge_ui/src/hooks/useHandleTransfer.ts index 2c7e42f2..d274685b 100644 --- a/bridge_ui/src/hooks/useHandleTransfer.ts +++ b/bridge_ui/src/hooks/useHandleTransfer.ts @@ -12,7 +12,10 @@ import { transferFromEth, transferFromEthNative, transferFromSolana, + transferNativeSol, transferFromTerra, + hexToUint8Array, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { WalletContextState } from "@solana/wallet-adapter-react"; import { Connection } from "@solana/web3.js"; @@ -44,7 +47,6 @@ import { setSignedVAAHex, setTransferTx, } from "../store/transferSlice"; -import { hexToUint8Array, uint8ArrayToHex } from "../utils/array"; import { ETH_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS, @@ -121,6 +123,7 @@ async function solana( decimals: number, targetChain: ChainId, targetAddress: Uint8Array, + isNative: boolean, originAddressStr?: string, originChain?: ChainId ) { @@ -131,19 +134,30 @@ async function solana( const originAddress = originAddressStr ? zeroPad(hexToUint8Array(originAddressStr), 32) : undefined; - const transaction = await transferFromSolana( - connection, - SOL_BRIDGE_ADDRESS, - SOL_TOKEN_BRIDGE_ADDRESS, - payerAddress, - fromAddress, - mintAddress, - amountParsed, - targetAddress, - targetChain, - originAddress, - originChain - ); + const promise = isNative + ? transferNativeSol( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + amountParsed, + targetAddress, + targetChain + ) + : transferFromSolana( + connection, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, + payerAddress, + fromAddress, + mintAddress, + amountParsed, + targetAddress, + targetChain, + originAddress, + originChain + ); + const transaction = await promise; const txid = await signSendAndConfirm(wallet, connection, transaction); enqueueSnackbar("Transaction confirmed", { variant: "success" }); const info = await connection.getTransaction(txid); @@ -289,6 +303,7 @@ export function useHandleTransfer() { decimals, targetChain, targetAddress, + isNative, originAsset, originChain ); diff --git a/bridge_ui/src/hooks/useNFTSignedVAA.ts b/bridge_ui/src/hooks/useNFTSignedVAA.ts index f34c9bd2..f5e4614b 100644 --- a/bridge_ui/src/hooks/useNFTSignedVAA.ts +++ b/bridge_ui/src/hooks/useNFTSignedVAA.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; import { selectNFTSignedVAAHex } from "../store/selectors"; -import { hexToUint8Array } from "../utils/array"; +import { hexToUint8Array } from "@certusone/wormhole-sdk"; export default function useNFTSignedVAA() { const signedVAAHex = useSelector(selectNFTSignedVAAHex); diff --git a/bridge_ui/src/hooks/useNFTTargetAddress.ts b/bridge_ui/src/hooks/useNFTTargetAddress.ts index dc5b6742..ed63ffd5 100644 --- a/bridge_ui/src/hooks/useNFTTargetAddress.ts +++ b/bridge_ui/src/hooks/useNFTTargetAddress.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; import { selectNFTTargetAddressHex } from "../store/selectors"; -import { hexToUint8Array } from "../utils/array"; +import { hexToUint8Array } from "@certusone/wormhole-sdk"; export default function useNFTTargetAddressHex() { const targetAddressHex = useSelector(selectNFTTargetAddressHex); diff --git a/bridge_ui/src/hooks/useSyncTargetAddress.ts b/bridge_ui/src/hooks/useSyncTargetAddress.ts index f6998275..72291853 100644 --- a/bridge_ui/src/hooks/useSyncTargetAddress.ts +++ b/bridge_ui/src/hooks/useSyncTargetAddress.ts @@ -3,6 +3,7 @@ import { CHAIN_ID_SOLANA, CHAIN_ID_TERRA, canonicalAddress, + uint8ArrayToHex, } from "@certusone/wormhole-sdk"; import { arrayify, zeroPad } from "@ethersproject/bytes"; import { @@ -25,7 +26,6 @@ import { } from "../store/selectors"; import { setTargetAddressHex as setNFTTargetAddressHex } from "../store/nftSlice"; import { setTargetAddressHex as setTransferTargetAddressHex } from "../store/transferSlice"; -import { uint8ArrayToHex } from "../utils/array"; function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) { const dispatch = useDispatch(); diff --git a/bridge_ui/src/hooks/useTransferSignedVAA.ts b/bridge_ui/src/hooks/useTransferSignedVAA.ts index d13b1f76..0b1af01c 100644 --- a/bridge_ui/src/hooks/useTransferSignedVAA.ts +++ b/bridge_ui/src/hooks/useTransferSignedVAA.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; import { selectTransferSignedVAAHex } from "../store/selectors"; -import { hexToUint8Array } from "../utils/array"; +import { hexToUint8Array } from "@certusone/wormhole-sdk"; export default function useTransferSignedVAA() { const signedVAAHex = useSelector(selectTransferSignedVAAHex); diff --git a/bridge_ui/src/hooks/useTransferTargetAddress.ts b/bridge_ui/src/hooks/useTransferTargetAddress.ts index ef3bdd0a..45a66c0b 100644 --- a/bridge_ui/src/hooks/useTransferTargetAddress.ts +++ b/bridge_ui/src/hooks/useTransferTargetAddress.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; import { selectTransferTargetAddressHex } from "../store/selectors"; -import { hexToUint8Array } from "../utils/array"; +import { hexToUint8Array } from "@certusone/wormhole-sdk"; export default function useTransferTargetAddressHex() { const targetAddressHex = useSelector(selectTransferTargetAddressHex); diff --git a/sdk/js/src/token_bridge/redeem.ts b/sdk/js/src/token_bridge/redeem.ts index 5fd208b0..ddbf4ebe 100644 --- a/sdk/js/src/token_bridge/redeem.ts +++ b/sdk/js/src/token_bridge/redeem.ts @@ -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 { ethers } from "ethers"; import { fromUint8Array } from "js-base64"; import { Bridge__factory } from "../ethers-contracts"; 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( 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( connection: Connection, bridgeAddress: string, diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts index 6568b168..79007e27 100644 --- a/sdk/js/src/token_bridge/transfer.ts +++ b/sdk/js/src/token_bridge/transfer.ts @@ -1,5 +1,11 @@ -import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; -import { Connection, Keypair, 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 { ethers } from "ethers"; import { @@ -7,7 +13,7 @@ import { TokenImplementation__factory, } from "../ethers-contracts"; 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( 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( connection: Connection, bridgeAddress: string, diff --git a/bridge_ui/src/utils/array.ts b/sdk/js/src/utils/array.ts similarity index 92% rename from bridge_ui/src/utils/array.ts rename to sdk/js/src/utils/array.ts index 18a7bbd7..c5bbf596 100644 --- a/bridge_ui/src/utils/array.ts +++ b/sdk/js/src/utils/array.ts @@ -3,8 +3,8 @@ import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, CHAIN_ID_TERRA, - humanAddress, -} from "@certusone/wormhole-sdk"; +} from "./consts"; +import { humanAddress } from "../terra"; import { PublicKey } from "@solana/web3.js"; import { hexValue, hexZeroPad } from "ethers/lib/utils"; diff --git a/sdk/js/src/utils/consts.ts b/sdk/js/src/utils/consts.ts index 7834dffc..97343d5c 100644 --- a/sdk/js/src/utils/consts.ts +++ b/sdk/js/src/utils/consts.ts @@ -3,3 +3,7 @@ export const CHAIN_ID_SOLANA: ChainId = 1; export const CHAIN_ID_ETH: ChainId = 2; export const CHAIN_ID_TERRA: ChainId = 3; export const CHAIN_ID_BSC: ChainId = 4; + +export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112"; +export const WSOL_DECIMALS = 9; +export const MAX_VAA_DECIMALS = 8; diff --git a/sdk/js/src/utils/index.ts b/sdk/js/src/utils/index.ts index f641ec7c..35f4b8dd 100644 --- a/sdk/js/src/utils/index.ts +++ b/sdk/js/src/utils/index.ts @@ -1,2 +1,4 @@ export * from "./consts"; export * from "./createNonce"; +export * from "./parseVaa"; +export * from "./array"; diff --git a/sdk/js/src/utils/parseVaa.ts b/sdk/js/src/utils/parseVaa.ts new file mode 100644 index 00000000..0ce39698 --- /dev/null +++ b/sdk/js/src/utils/parseVaa.ts @@ -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; +// }