diff --git a/docs/docs/faqs/liquid-markets.mdx b/docs/docs/faqs/liquid-markets.mdx index d92491833..587bcfaa6 100644 --- a/docs/docs/faqs/liquid-markets.mdx +++ b/docs/docs/faqs/liquid-markets.mdx @@ -31,6 +31,8 @@ import EthAptos from './_liquid-markets/eth-aptos.md' import EthAlgorand from './_liquid-markets/eth-algorand.md' import SolAlgorand from './_liquid-markets/sol-algorand.md' import AvaxAlgorand from './_liquid-markets/avax-algorand.md' +import SolInjective from './_liquid-markets/sol-injective.md' +import EthInjective from './_liquid-markets/eth-injective.md' import EthSui from './_liquid-markets/eth-sui.md' import SolSui from './_liquid-markets/sol-sui.md' import AptosOsmosis from './_liquid-markets/aptos-osmosis.md' @@ -199,6 +201,17 @@ Check out the [Wormhole Token list](https://github.com/certusone/wormhole-token- +## Target chain: Injective + + + + + + + + + + ## Target chain: Sui diff --git a/package-lock.json b/package-lock.json index 29d4d7146..1dc21e4d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,10 @@ "@certusone/wormhole-sdk": "^0.10.17", "@cosmjs/cosmwasm-stargate": "^0.32.3", "@cosmjs/tendermint-rpc": "^0.32.3", + "@injectivelabs/networks": "^1.14.4", + "@injectivelabs/sdk-ts": "^1.14.4", + "@injectivelabs/ts-types": "^1.14.4", + "@injectivelabs/wallet-ts": "^1.14.4", "@manahippo/aptos-wallet-adapter": "^1.0.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.11.2", diff --git a/package.json b/package.json index 3e5252969..bc472a67d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,10 @@ "@certusone/wormhole-sdk": "^0.10.17", "@cosmjs/cosmwasm-stargate": "^0.32.3", "@cosmjs/tendermint-rpc": "^0.32.3", + "@injectivelabs/networks": "^1.14.4", + "@injectivelabs/sdk-ts": "^1.14.4", + "@injectivelabs/ts-types": "^1.14.4", + "@injectivelabs/wallet-ts": "^1.14.4", "@manahippo/aptos-wallet-adapter": "^1.0.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.11.2", diff --git a/src/components/KeyAndBalance.tsx b/src/components/KeyAndBalance.tsx index f88fa2d2d..bbe761af1 100644 --- a/src/components/KeyAndBalance.tsx +++ b/src/components/KeyAndBalance.tsx @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_XPLA, @@ -21,6 +22,7 @@ function isChainAllowed(chainId: ChainId) { chainId === CHAIN_ID_NEAR || chainId === CHAIN_ID_XPLA || chainId === CHAIN_ID_APTOS || + chainId === CHAIN_ID_INJECTIVE || chainId === CHAIN_ID_SUI || chainId === CHAIN_ID_SEI ); diff --git a/src/components/Recovery.tsx b/src/components/Recovery.tsx index 15aba528d..cce679ddb 100644 --- a/src/components/Recovery.tsx +++ b/src/components/Recovery.tsx @@ -3,6 +3,7 @@ import { CHAIN_ID_ACALA, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_KARURA, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, @@ -11,6 +12,7 @@ import { CHAIN_ID_SEI, getEmitterAddressAlgorand, getEmitterAddressEth, + getEmitterAddressInjective, getEmitterAddressSolana, getEmitterAddressTerra, getEmitterAddressXpla, @@ -23,12 +25,14 @@ import { parseNFTPayload, parseSequenceFromLogAlgorand, parseSequenceFromLogEth, + parseSequenceFromLogInjective, parseSequenceFromLogSolana, parseSequenceFromLogTerra, parseSequenceFromLogXpla, parseTransferPayload, parseVaa, queryExternalId, + queryExternalIdInjective, TerraChainId, uint8ArrayToHex, CHAIN_ID_SUI, @@ -111,6 +115,10 @@ import { getEmitterAddressAndSequenceFromResult, } from "../utils/aptos"; import { Types } from "aptos"; +import { + getInjectiveTxClient, + getInjectiveWasmClient, +} from "../utils/injective"; import { getSuiProvider } from "../utils/sui"; import { getEmitterAddressAndSequenceFromResponseSui, @@ -377,6 +385,26 @@ async function xpla(tx: string, enqueueSnackbar: any) { } } +async function injective(txHash: string, enqueueSnackbar: any) { + try { + const client = getInjectiveTxClient(); + const tx = await client.fetchTx(txHash); + if (!tx) { + throw new Error("Unable to fetch transaction"); + } + const sequence = parseSequenceFromLogInjective(tx); + if (!sequence) { + throw new Error("Sequence not found"); + } + const emitterAddress = await getEmitterAddressInjective( + getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE) + ); + return await fetchSignedVAA(CHAIN_ID_INJECTIVE, emitterAddress, sequence); + } catch (e) { + return handleError(e, enqueueSnackbar); + } +} + async function sui(digest: string, enqueueSnackbar: any) { try { const provider = getSuiProvider(); @@ -621,6 +649,21 @@ export default function Recovery() { } })(); } + if (parsedPayload && parsedPayload.targetChain === CHAIN_ID_INJECTIVE) { + (async () => { + const client = getInjectiveWasmClient(); + const tokenBridgeAddress = + getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE); + const tokenId = await queryExternalIdInjective( + client as any, + tokenBridgeAddress, + parsedPayload.originAddress + ); + if (!cancelled) { + setTokenId(tokenId || ""); + } + })(); + } if (parsedPayload && parsedPayload.targetChain === CHAIN_ID_SUI) { (async () => { @@ -802,6 +845,26 @@ export default function Recovery() { setIsVAAPending(isPending); } })(); + } else if (recoverySourceChain === CHAIN_ID_INJECTIVE) { + setRecoverySourceTxError(""); + setRecoverySourceTxIsLoading(true); + setTokenId(""); + (async () => { + const { vaa, isPending, error } = await injective( + recoverySourceTx, + enqueueSnackbar + ); + if (!cancelled) { + setRecoverySourceTxIsLoading(false); + if (vaa) { + setRecoverySignedVAA(vaa); + } + if (error) { + setRecoverySourceTxError(error); + } + setIsVAAPending(isPending); + } + })(); } else if (recoverySourceChain === CHAIN_ID_SUI) { setRecoverySourceTxError(""); setRecoverySourceTxIsLoading(true); @@ -1166,7 +1229,8 @@ export default function Recovery() { value={ parsedPayload ? parsedPayload.targetChain === CHAIN_ID_TERRA2 || - parsedPayload.targetChain === CHAIN_ID_XPLA + parsedPayload.targetChain === CHAIN_ID_XPLA || + parsedPayload.targetChain === CHAIN_ID_INJECTIVE ? tokenId : hexToNativeAssetString( parsedPayload.originAddress, diff --git a/src/components/ShowTx.tsx b/src/components/ShowTx.tsx index 1954644da..e7f35b881 100644 --- a/src/components/ShowTx.tsx +++ b/src/components/ShowTx.tsx @@ -19,6 +19,7 @@ import { CHAIN_ID_XPLA, CHAIN_ID_APTOS, CHAIN_ID_ARBITRUM, + CHAIN_ID_INJECTIVE, CHAIN_ID_OPTIMISM, CHAIN_ID_SUI, CHAIN_ID_BASE, @@ -168,6 +169,10 @@ export default function ShowTx({ ? `https://${ CLUSTER === "testnet" ? "goerli." : "" }arbiscan.io/tx/${tx?.id}` + : chainId === CHAIN_ID_INJECTIVE + ? `https://${ + CLUSTER === "testnet" ? "testnet." : "" + }explorer.injective.network/transaction/${tx.id}` : chainId === CHAIN_ID_OPTIMISM ? `https://${ CLUSTER === "testnet" ? "goerli-optimism." : "optimistic." diff --git a/src/components/SmartAddress.tsx b/src/components/SmartAddress.tsx index 27634d5bd..e8eb66290 100644 --- a/src/components/SmartAddress.tsx +++ b/src/components/SmartAddress.tsx @@ -22,6 +22,7 @@ import { CHAIN_ID_APTOS, isValidAptosType, CHAIN_ID_ARBITRUM, + CHAIN_ID_INJECTIVE, terra, CHAIN_ID_OPTIMISM, CHAIN_ID_SUI, @@ -229,6 +230,14 @@ export default function SmartAddress({ ? `https://${CLUSTER === "testnet" ? "goerli." : ""}arbiscan.io/${ isAsset ? "token" : "address" }/${useableAddress}` + : chainId === CHAIN_ID_INJECTIVE + ? `https://${ + CLUSTER === "testnet" ? "testnet." : "" + }explorer.injective.network/${ + isAsset + ? `asset/?tokenType=${isNative ? "native" : "cw20"}&tokenIdentifier=` + : "account/" + }${useableAddress}` : chainId === CHAIN_ID_OPTIMISM ? `https://${ CLUSTER === "testnet" ? "goerli-optimism." : "optimistic." diff --git a/src/components/TokenSelectors/InjectiveTokenPicker.tsx b/src/components/TokenSelectors/InjectiveTokenPicker.tsx new file mode 100644 index 000000000..390411b37 --- /dev/null +++ b/src/components/TokenSelectors/InjectiveTokenPicker.tsx @@ -0,0 +1,168 @@ +import { + CHAIN_ID_INJECTIVE, + isNativeDenomInjective, + parseSmartContractStateResponse, +} from "@certusone/wormhole-sdk"; +import { formatUnits } from "@ethersproject/units"; +import { useCallback, useMemo, useRef } from "react"; +import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts"; +import useIsWalletReady from "../../hooks/useIsWalletReady"; +import useInjectiveNativeBalances from "../../hooks/useInjectiveNativeBalances"; +import { DataWrapper } from "../../store/helpers"; +import { NFTParsedTokenAccount } from "../../store/nftSlice"; +import { ParsedTokenAccount } from "../../store/transferSlice"; +import TokenPicker, { BasicAccountRender } from "./TokenPicker"; +import { + formatNativeDenom, + getInjectiveWasmClient, + INJECTIVE_NATIVE_DENOM, + isValidInjectiveAddress, + NATIVE_INJECTIVE_DECIMALS, +} from "../../utils/injective"; +import injectiveIcon from "../../icons/injective.svg"; + +type InjectiveTokenPickerProps = { + value: ParsedTokenAccount | null; + onChange: (newValue: ParsedTokenAccount | null) => void; + tokenAccounts: DataWrapper | undefined; + disabled: boolean; + resetAccounts: (() => void) | undefined; +}; + +const returnsFalse = () => false; + +export default function InjectiveTokenPicker(props: InjectiveTokenPickerProps) { + const { value, onChange, disabled } = props; + const { walletAddress } = useIsWalletReady(CHAIN_ID_INJECTIVE); + const nativeRefresh = useRef<() => void>(() => {}); + const { balances, isLoading: nativeIsLoading } = useInjectiveNativeBalances( + walletAddress, + nativeRefresh + ); + + const resetAccountWrapper = useCallback(() => { + //we can currently skip calling this as we don't read from sourceParsedTokenAccounts + //resetAccounts && resetAccounts(); + nativeRefresh.current(); + }, []); + const isLoading = nativeIsLoading; // || (tokenMap?.isFetching || false); + + const onChangeWrapper = useCallback( + async (account: NFTParsedTokenAccount | null) => { + if (account === null) { + onChange(null); + return Promise.resolve(); + } + onChange(account); + return Promise.resolve(); + }, + [onChange] + ); + + const injTokenArray = useMemo(() => { + const balancesItems = + balances && walletAddress + ? Object.keys(balances).map((denom) => + //This token account makes a lot of assumptions + createParsedTokenAccount( + walletAddress, + denom, + balances[denom], //amount + NATIVE_INJECTIVE_DECIMALS, //TODO actually get decimals rather than hardcode + 0, //uiAmount is unused + formatUnits(balances[denom], NATIVE_INJECTIVE_DECIMALS), //uiAmountString + formatNativeDenom(denom), // symbol + undefined, //name + injectiveIcon, + true //is native asset + ) + ) + : []; + return balancesItems.filter( + (metadata) => metadata.mintKey === INJECTIVE_NATIVE_DENOM + ); + }, [walletAddress, balances]); + + //TODO this only supports non-native assets. Native assets come from the hook. + //TODO correlate against token list to get metadata + const lookupInjectiveAddress = useCallback( + (lookupAsset: string) => { + if (!walletAddress) { + return Promise.reject("Wallet not connected"); + } + const client = getInjectiveWasmClient(); + return client + .fetchSmartContractState( + lookupAsset, + Buffer.from( + JSON.stringify({ + token_info: {}, + }) + ).toString("base64") + ) + .then((infoData) => + client + .fetchSmartContractState( + lookupAsset, + Buffer.from( + JSON.stringify({ + balance: { + address: walletAddress, + }, + }) + ).toString("base64") + ) + .then((balanceData) => { + if (infoData && balanceData) { + const balance = parseSmartContractStateResponse(balanceData); + const info = parseSmartContractStateResponse(infoData); + return createParsedTokenAccount( + walletAddress, + lookupAsset, + balance.balance.toString(), + info.decimals, + Number(formatUnits(balance.balance, info.decimals)), + formatUnits(balance.balance, info.decimals), + info.symbol, + info.name + ); + } else { + throw new Error("Failed to retrieve Injective account."); + } + }) + ) + .catch((e) => { + return Promise.reject(e); + }); + }, + [walletAddress] + ); + + const isSearchableAddress = useCallback((address: string) => { + return isValidInjectiveAddress(address) && !isNativeDenomInjective(address); + }, []); + + const RenderComp = useCallback( + ({ account }: { account: NFTParsedTokenAccount }) => { + return BasicAccountRender(account, returnsFalse, false); + }, + [] + ); + + return ( + + ); +} diff --git a/src/components/TokenSelectors/SourceTokenSelector.tsx b/src/components/TokenSelectors/SourceTokenSelector.tsx index d3e50f208..a744e913f 100644 --- a/src/components/TokenSelectors/SourceTokenSelector.tsx +++ b/src/components/TokenSelectors/SourceTokenSelector.tsx @@ -2,6 +2,7 @@ import { CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_SUI, @@ -38,6 +39,7 @@ import RefreshButtonWrapper from "./RefreshButtonWrapper"; import SolanaTokenPicker from "./SolanaTokenPicker"; import TerraTokenPicker from "./TerraTokenPicker"; import XplaTokenPicker from "./XplaTokenPicker"; +import InjectiveTokenPicker from "./InjectiveTokenPicker"; import SuiTokenPicker from "./SuiTokenPicker"; import SeiTokenPicker from "./SeiTokenPicker"; @@ -160,6 +162,14 @@ export const TokenSelector = (props: TokenSelectorProps) => { tokenAccounts={maps?.tokenAccounts} nft={nft} /> + ) : lookupChain === CHAIN_ID_INJECTIVE ? ( + ) : lookupChain === CHAIN_ID_SUI ? ( { + const isValidNetwork = INJECTIVE_NETWORKS.includes( + process.env.REACT_APP_CLUSTER || "" + ); + if (!isValidNetwork) return []; + + const network = getInjectiveNetworkName(); + const networkEndpoints = getNetworkInfo(network); + + const opts = { + networkChainId: getInjectiveNetworkChainId(), + broadcasterOptions: { + network, + networkEndpoints, + }, + }; + + return [new KeplrWallet(opts)]; +}; + +export interface IInjectiveContext { + wallet?: InjectiveWallet; + address?: string; +} + +export const useInjectiveContext = (): IInjectiveContext => { + const wallet = useWallet(CHAIN_ID_INJECTIVE); + + const address = useMemo(() => wallet?.getAddress(), [wallet]); + + return useMemo( + () => ({ + wallet, + address, + }), + [wallet, address] + ); +}; diff --git a/src/hooks/useCheckIfWormholeWrapped.ts b/src/hooks/useCheckIfWormholeWrapped.ts index 8185ba063..6e616a6ad 100644 --- a/src/hooks/useCheckIfWormholeWrapped.ts +++ b/src/hooks/useCheckIfWormholeWrapped.ts @@ -16,6 +16,8 @@ import { isTerraChain, uint8ArrayToHex, WormholeWrappedInfo, + CHAIN_ID_INJECTIVE, + getOriginalAssetInjective, CHAIN_ID_SUI, getOriginalAssetSui, CHAIN_ID_ETH, @@ -62,6 +64,7 @@ import { import { getOriginalAssetNear, makeNearAccount } from "../utils/near"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { getAptosClient } from "../utils/aptos"; +import { getInjectiveWasmClient } from "../utils/injective"; import { getSuiProvider } from "../utils/sui"; import { getOriginalAssetSei, getSeiWasmClient } from "../utils/sei"; import { base58 } from "ethers/lib/utils"; @@ -297,6 +300,17 @@ function useCheckIfWormholeWrapped(nft?: boolean) { } } catch (e) {} } + if (sourceChain === CHAIN_ID_INJECTIVE && sourceAsset) { + try { + const client = getInjectiveWasmClient(); + const wrappedInfo = makeStateSafe( + await getOriginalAssetInjective(sourceAsset, client as any) + ); + if (!cancelled) { + dispatch(setSourceWormholeWrappedInfo(wrappedInfo)); + } + } catch (e) {} + } if (sourceChain === CHAIN_ID_SUI && sourceAsset) { try { const wrappedInfo = makeStateSafe( diff --git a/src/hooks/useFetchForeignAsset.ts b/src/hooks/useFetchForeignAsset.ts index 1d949fc3a..9ab2efdc9 100644 --- a/src/hooks/useFetchForeignAsset.ts +++ b/src/hooks/useFetchForeignAsset.ts @@ -3,6 +3,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_TERRA2, @@ -11,6 +12,7 @@ import { getForeignAssetAlgorand, getForeignAssetAptos, getForeignAssetEth, + getForeignAssetInjective, getForeignAssetSolana, getForeignAssetTerra, getForeignAssetXpla, @@ -50,6 +52,7 @@ import { import { useNearContext } from "../contexts/NearWalletContext"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { getAptosClient } from "../utils/aptos"; +import { getInjectiveWasmClient } from "../utils/injective"; import { getSuiProvider } from "../utils/sui"; import { getForeignAssetSei, getSeiWasmClient } from "../utils/sei"; @@ -224,6 +227,16 @@ function useFetchForeignAsset( ) .catch(() => Promise.reject("Failed to make Near account")); } + : foreignChain === CHAIN_ID_INJECTIVE + ? () => { + const client = getInjectiveWasmClient(); + return getForeignAssetInjective( + getTokenBridgeAddressForChain(foreignChain), + client as any, + originChain, + hexToUint8Array(originAssetHex) + ); + } : foreignChain === CHAIN_ID_SUI ? () => { return getForeignAssetSui( diff --git a/src/hooks/useFetchTargetAsset.ts b/src/hooks/useFetchTargetAsset.ts index 13962f1e4..84dc373dd 100644 --- a/src/hooks/useFetchTargetAsset.ts +++ b/src/hooks/useFetchTargetAsset.ts @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_TERRA2, @@ -11,6 +12,7 @@ import { getForeignAssetAlgorand, getForeignAssetAptos, getForeignAssetEth, + getForeignAssetInjective, getForeignAssetSolana, getForeignAssetTerra, getForeignAssetXpla, @@ -20,6 +22,7 @@ import { isEVMChain, isTerraChain, queryExternalId, + queryExternalIdInjective, CHAIN_ID_SUI, getForeignAssetSui, } from "@certusone/wormhole-sdk"; @@ -82,6 +85,7 @@ import { } from "../utils/near"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { getAptosClient } from "../utils/aptos"; +import { getInjectiveWasmClient } from "../utils/injective"; import { getSuiProvider } from "../utils/sui"; import { getForeignAssetSei, @@ -279,6 +283,25 @@ function useFetchTargetAsset(nft?: boolean) { ); } } + } else if (originChain === CHAIN_ID_INJECTIVE) { + const client = getInjectiveWasmClient(); + const tokenBridgeAddress = + getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE); + const tokenId = await queryExternalIdInjective( + client as any, + tokenBridgeAddress, + originAsset || "" + ); + if (!cancelled) { + dispatch( + setTargetAsset( + receiveDataWrapper({ + doesExist: true, + address: tokenId, + }) + ) + ); + } } else if (originChain === CHAIN_ID_SUI) { const coinType = await getForeignAssetSui( getSuiProvider(), @@ -625,6 +648,36 @@ function useFetchTargetAsset(nft?: boolean) { } } } + if (targetChain === CHAIN_ID_INJECTIVE && originChain && originAsset) { + dispatch(setTargetAsset(fetchDataWrapper())); + try { + const client = getInjectiveWasmClient(); + const asset = await getForeignAssetInjective( + getTokenBridgeAddressForChain(targetChain), + client as any, + originChain, + hexToUint8Array(originAsset) + ); + if (!cancelled) { + dispatch( + setTargetAsset( + receiveDataWrapper({ doesExist: !!asset, address: asset }) + ) + ); + setArgs(); + } + } catch (e) { + if (!cancelled) { + dispatch( + setTargetAsset( + errorDataWrapper( + "Unable to determine existence of wrapped asset" + ) + ) + ); + } + } + } if (targetChain === CHAIN_ID_SUI && originChain && originAsset) { dispatch(setTargetAsset(fetchDataWrapper())); try { diff --git a/src/hooks/useGetIsTransferCompleted.ts b/src/hooks/useGetIsTransferCompleted.ts index ee3c6034d..e6c60f52f 100644 --- a/src/hooks/useGetIsTransferCompleted.ts +++ b/src/hooks/useGetIsTransferCompleted.ts @@ -1,6 +1,7 @@ import { CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_SUI, @@ -9,6 +10,7 @@ import { getIsTransferCompletedAlgorand, getIsTransferCompletedAptos, getIsTransferCompletedEth, + getIsTransferCompletedInjective, getIsTransferCompletedSolana, getIsTransferCompletedSui, getIsTransferCompletedTerra, @@ -43,6 +45,7 @@ import useIsWalletReady from "./useIsWalletReady"; import useTransferSignedVAA from "./useTransferSignedVAA"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { getAptosClient } from "../utils/aptos"; +import { getInjectiveWasmClient } from "../utils/injective"; import { getSuiProvider } from "../utils/sui"; import { getIsTransferCompletedSei, getSeiWasmClient } from "../utils/sei"; @@ -240,6 +243,24 @@ export default function useGetIsTransferCompleted( setIsLoading(false); } })(); + } else if (targetChain === CHAIN_ID_INJECTIVE) { + setIsLoading(true); + (async () => { + try { + const client = getInjectiveWasmClient(); + transferCompleted = await getIsTransferCompletedInjective( + getTokenBridgeAddressForChain(targetChain), + signedVAA, + client as any + ); + } catch (error) { + console.error(error); + } + if (!cancelled) { + setIsTransferCompleted(transferCompleted); + setIsLoading(false); + } + })(); } else if (targetChain === CHAIN_ID_SUI) { setIsLoading(true); (async () => { diff --git a/src/hooks/useGetSourceParsedTokenAccounts.ts b/src/hooks/useGetSourceParsedTokenAccounts.ts index f0961c095..f9217b4c1 100644 --- a/src/hooks/useGetSourceParsedTokenAccounts.ts +++ b/src/hooks/useGetSourceParsedTokenAccounts.ts @@ -23,6 +23,7 @@ import { ethers_contracts, WSOL_ADDRESS, WSOL_DECIMALS, + CHAIN_ID_INJECTIVE, CHAIN_ID_SUI, CHAIN_ID_ARBITRUM, CHAIN_ID_BASE, @@ -2005,6 +2006,10 @@ function useGetAvailableTokens(nft: boolean = false) { tokenAccounts, resetAccounts: resetSourceAccounts, } + : lookupChain === CHAIN_ID_INJECTIVE + ? { + resetAccounts: resetSourceAccounts, + } : lookupChain === CHAIN_ID_SUI ? { tokenAccounts, diff --git a/src/hooks/useGetTargetParsedTokenAccounts.ts b/src/hooks/useGetTargetParsedTokenAccounts.ts index 202fe3306..2d0a12f66 100644 --- a/src/hooks/useGetTargetParsedTokenAccounts.ts +++ b/src/hooks/useGetTargetParsedTokenAccounts.ts @@ -1,6 +1,7 @@ import { CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_SUI, @@ -9,8 +10,10 @@ import { ensureHexPrefix, ethers_contracts, isEVMChain, + isNativeDenomInjective, isNativeDenomXpla, isTerraChain, + parseSmartContractStateResponse, terra, } from "@certusone/wormhole-sdk"; import { Connection, PublicKey } from "@solana/web3.js"; @@ -49,6 +52,12 @@ import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { NATIVE_XPLA_DECIMALS } from "../utils/xpla"; import { useAptosContext } from "../contexts/AptosWalletContext"; import { getAptosClient } from "../utils/aptos"; +import { + getInjectiveBankClient, + NATIVE_INJECTIVE_DECIMALS, + getInjectiveWasmClient, +} from "../utils/injective"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { useSuiWallet } from "../contexts/SuiWalletContext"; import { getSuiProvider } from "../utils/sui"; @@ -80,6 +89,7 @@ function useGetTargetParsedTokenAccounts() { const { address: algoAccount } = useAlgorandWallet(); const { accountId: nearAccountId } = useNearContext(); const { account: aptosAddress } = useAptosContext(); + const { address: injAddress } = useInjectiveContext(); const seiWallet = useSeiWallet(); const seiAddress = seiWallet?.getAddress(); const suiWallet = useSuiWallet(); @@ -579,6 +589,87 @@ function useGetTargetParsedTokenAccounts() { } } } + if (targetChain === CHAIN_ID_INJECTIVE && injAddress) { + if (isNativeDenomInjective(targetAsset)) { + const client = getInjectiveBankClient(); + client + .fetchBalance({ accountAddress: injAddress, denom: targetAsset }) + .then(({ amount }) => { + if (!cancelled) { + dispatch( + setTargetParsedTokenAccount( + createParsedTokenAccount( + "", + "", + amount, + NATIVE_INJECTIVE_DECIMALS, + Number(formatUnits(amount, NATIVE_INJECTIVE_DECIMALS)), + formatUnits(amount, NATIVE_INJECTIVE_DECIMALS), + symbol, + tokenName, + logo + ) + ) + ); + } + }) + .catch(() => { + if (!cancelled) { + // TODO: error state + } + }); + } else { + const client = getInjectiveWasmClient(); + client + .fetchSmartContractState( + targetAsset, + Buffer.from( + JSON.stringify({ + token_info: {}, + }) + ).toString("base64") + ) + .then((infoData) => + client + .fetchSmartContractState( + targetAsset, + Buffer.from( + JSON.stringify({ + balance: { + address: injAddress, + }, + }) + ).toString("base64") + ) + .then((balanceData) => { + if (infoData && balanceData && !cancelled) { + const balance = parseSmartContractStateResponse(balanceData); + const info = parseSmartContractStateResponse(infoData); + dispatch( + setTargetParsedTokenAccount( + createParsedTokenAccount( + "", + "", + balance.balance.toString(), + info.decimals, + Number(formatUnits(balance.balance, info.decimals)), + formatUnits(balance.balance, info.decimals), + symbol, + tokenName, + logo + ) + ) + ); + } + }) + ) + .catch((e) => { + if (!cancelled) { + // TODO: error state + } + }); + } + } return () => { cancelled = true; @@ -602,6 +693,7 @@ function useGetTargetParsedTokenAccounts() { nearAccountId, xplaWallet, aptosAddress, + injAddress, seiAddress, suiAddress, ]); diff --git a/src/hooks/useHandleAttest.tsx b/src/hooks/useHandleAttest.tsx index 16b286925..563718040 100644 --- a/src/hooks/useHandleAttest.tsx +++ b/src/hooks/useHandleAttest.tsx @@ -2,12 +2,14 @@ import { attestFromAlgorand, attestFromAptos, attestFromEth, + attestFromInjective, attestFromSolana, attestFromTerra, attestFromXpla, ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_KLAYTN, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, @@ -15,6 +17,7 @@ import { CHAIN_ID_SEI, getEmitterAddressAlgorand, getEmitterAddressEth, + getEmitterAddressInjective, getEmitterAddressSolana, getEmitterAddressTerra, getEmitterAddressXpla, @@ -23,6 +26,7 @@ import { isTerraChain, parseSequenceFromLogAlgorand, parseSequenceFromLogEth, + parseSequenceFromLogInjective, parseSequenceFromLogSolana, parseSequenceFromLogTerra, parseSequenceFromLogXpla, @@ -91,9 +95,12 @@ import parseError from "../utils/parseError"; import { signSendAndConfirm } from "../utils/solana"; import { postWithFees, waitForTerraExecution } from "../utils/terra"; import { postWithFeesXpla, waitForXplaExecution } from "../utils/xpla"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; +import { broadcastInjectiveTx } from "../utils/injective"; import { AlgorandWallet } from "@xlabs-libs/wallet-aggregator-algorand"; import { SolanaWallet } from "@xlabs-libs/wallet-aggregator-solana"; import { AptosWallet } from "@xlabs-libs/wallet-aggregator-aptos"; +import { InjectiveWallet } from "@xlabs-libs/wallet-aggregator-injective"; import { NearWallet } from "@xlabs-libs/wallet-aggregator-near"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { TerraWallet } from "@xlabs-libs/wallet-aggregator-terra"; @@ -508,6 +515,63 @@ async function terra( } } +async function injective( + dispatch: any, + enqueueSnackbar: any, + wallet: InjectiveWallet, + walletAddress: string, + asset: string, + onError: (error: any) => void, + onStart: (extra?: Partial) => void +) { + dispatch(setIsSending(true)); + try { + const tokenBridgeAddress = + getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE); + const msg = await attestFromInjective( + tokenBridgeAddress, + walletAddress, + asset + ); + const tx = await broadcastInjectiveTx( + wallet, + walletAddress, + msg, + "Attest Token" + ); + onStart?.({ txId: tx.txHash }); + dispatch(setAttestTx({ id: tx.txHash, block: tx.height })); + enqueueSnackbar(null, { + content: Transaction confirmed, + }); + const sequence = parseSequenceFromLogInjective(tx); + if (!sequence) { + throw new Error("Sequence not found"); + } + const emitterAddress = await getEmitterAddressInjective(tokenBridgeAddress); + enqueueSnackbar(null, { + content: Fetching VAA, + }); + const { vaaBytes } = await getSignedVAAWithRetry( + WORMHOLE_RPC_HOSTS, + CHAIN_ID_INJECTIVE, + emitterAddress, + sequence + ); + dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes))); + enqueueSnackbar(null, { + content: Fetched Signed VAA, + }); + } catch (e) { + console.error(e); + enqueueSnackbar(null, { + content: {parseError(e)}, + }); + dispatch(setIsSending(false)); + onError(e); + } +} + async function sui( dispatch: any, enqueueSnackbar: any, @@ -665,6 +729,7 @@ export function useHandleAttest() { const { address: algoAccount, wallet: algoWallet } = useAlgorandWallet(); const { account: aptosAddress, wallet: aptosWallet } = useAptosContext(); const { accountId: nearAccountId, wallet } = useNearContext(); + const { wallet: injWallet, address: injAddress } = useInjectiveContext(); const seiWallet = useSeiWallet(); const seiAddress = seiWallet?.getAddress(); const suiWallet = useSuiWallet(); @@ -757,6 +822,16 @@ export function useHandleAttest() { onError, onStart ); + } else if (sourceChain === CHAIN_ID_INJECTIVE && injWallet && injAddress) { + injective( + dispatch, + enqueueSnackbar, + injWallet, + injAddress, + sourceAsset, + onError, + onStart + ); } else if (sourceChain === CHAIN_ID_SEI && seiWallet && seiAddress) { sei(dispatch, enqueueSnackbar, seiWallet, sourceAsset, onError, onStart); } else if ( @@ -780,6 +855,8 @@ export function useHandleAttest() { aptosAddress, nearAccountId, wallet, + injWallet, + injAddress, seiWallet, seiAddress, suiWallet, diff --git a/src/hooks/useHandleCreateWrapped.tsx b/src/hooks/useHandleCreateWrapped.tsx index 56c96bb7a..c3ebdc12d 100644 --- a/src/hooks/useHandleCreateWrapped.tsx +++ b/src/hooks/useHandleCreateWrapped.tsx @@ -3,6 +3,7 @@ import { CHAIN_ID_ACALA, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_KARURA, CHAIN_ID_KLAYTN, CHAIN_ID_NEAR, @@ -11,6 +12,7 @@ import { createWrappedOnAlgorand, createWrappedOnAptos, createWrappedOnEth, + createWrappedOnInjective, createWrappedOnSolana, createWrappedOnTerra, createWrappedOnXpla, @@ -19,6 +21,7 @@ import { isTerraChain, TerraChainId, updateWrappedOnEth, + updateWrappedOnInjective, updateWrappedOnSolana, updateWrappedOnTerra, updateWrappedOnXpla, @@ -75,9 +78,12 @@ import parseError from "../utils/parseError"; import { postVaa, signSendAndConfirm } from "../utils/solana"; import { postWithFees } from "../utils/terra"; import useAttestSignedVAA from "./useAttestSignedVAA"; +import { broadcastInjectiveTx } from "../utils/injective"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; import { AlgorandWallet } from "@xlabs-libs/wallet-aggregator-algorand"; import { SolanaWallet } from "@xlabs-libs/wallet-aggregator-solana"; import { AptosWallet } from "@xlabs-libs/wallet-aggregator-aptos"; +import { InjectiveWallet } from "@xlabs-libs/wallet-aggregator-injective"; import { NearWallet } from "@xlabs-libs/wallet-aggregator-near"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { TerraWallet } from "@xlabs-libs/wallet-aggregator-terra"; @@ -543,6 +549,50 @@ async function terra( } } +async function injective( + dispatch: any, + enqueueSnackbar: any, + wallet: InjectiveWallet, + walletAddress: string, + signedVAA: Uint8Array, + shouldUpdate: boolean, + onError: (error: any) => void, + onStart: (extra?: Partial) => void +) { + dispatch(setIsCreating(true)); + const tokenBridgeAddress = getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE); + try { + const msg = shouldUpdate + ? await updateWrappedOnInjective( + tokenBridgeAddress, + walletAddress, + signedVAA + ) + : await createWrappedOnInjective( + tokenBridgeAddress, + walletAddress, + signedVAA + ); + const tx = await broadcastInjectiveTx( + wallet, + walletAddress, + msg, + "Wormhole - Create Wrapped" + ); + onStart({ txId: tx.txHash }); + dispatch(setCreateTx({ id: tx.txHash, block: tx.height })); + enqueueSnackbar(null, { + content: Transaction confirmed, + }); + } catch (e) { + enqueueSnackbar(null, { + content: {parseError(e)}, + }); + dispatch(setIsCreating(false)); + onError(e); + } +} + async function sui( dispatch: any, enqueueSnackbar: any, @@ -598,7 +648,7 @@ async function sui( } const wrappedAssetSetupEvent = suiPrepareRegistrationTxRes.objectChanges?.find( - (oc: any) => + (oc) => oc.type === "created" && oc.objectType.includes("WrappedAssetSetup") ); const wrappedAssetSetupType = @@ -757,6 +807,7 @@ export function useHandleCreateWrapped( const xplaWallet = useXplaWallet(); const { address: algoAccount, wallet: algoWallet } = useAlgorandWallet(); const { account: aptosAddress, wallet: aptosWallet } = useAptosContext(); + const { wallet: injWallet, address: injAddress } = useInjectiveContext(); const { accountId: nearAccountId, wallet } = useNearContext(); const suiWallet = useSuiWallet(); const seiWallet = useSeiWallet(); @@ -886,6 +937,22 @@ export function useHandleCreateWrapped( onError, onStart ); + } else if ( + targetChain === CHAIN_ID_INJECTIVE && + injWallet && + injAddress && + !!signedVAA + ) { + injective( + dispatch, + enqueueSnackbar, + injWallet, + injAddress, + signedVAA, + shouldUpdate, + onError, + onStart + ); } else if ( targetChain === CHAIN_ID_SUI && suiWallet && @@ -939,6 +1006,8 @@ export function useHandleCreateWrapped( xplaWallet, aptosAddress, aptosWallet, + injWallet, + injAddress, foreignAddress, suiWallet, seiWallet, diff --git a/src/hooks/useHandleRedeem.tsx b/src/hooks/useHandleRedeem.tsx index 2b4402a42..e06d70a91 100644 --- a/src/hooks/useHandleRedeem.tsx +++ b/src/hooks/useHandleRedeem.tsx @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_KLAYTN, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, @@ -11,6 +12,7 @@ import { redeemOnAlgorand, redeemOnEth, redeemOnEthNative, + redeemOnInjective, redeemOnSolana, redeemOnTerra, redeemOnXpla, @@ -79,9 +81,12 @@ import { postVaa, signSendAndConfirm } from "../utils/solana"; import { postWithFees } from "../utils/terra"; import useTransferSignedVAA from "./useTransferSignedVAA"; import { postWithFeesXpla } from "../utils/xpla"; +import { broadcastInjectiveTx } from "../utils/injective"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; import { AlgorandWallet } from "@xlabs-libs/wallet-aggregator-algorand"; import { SolanaWallet } from "@xlabs-libs/wallet-aggregator-solana"; import { AptosWallet } from "@xlabs-libs/wallet-aggregator-aptos"; +import { InjectiveWallet } from "@xlabs-libs/wallet-aggregator-injective"; import { NearWallet } from "@xlabs-libs/wallet-aggregator-near"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { TerraWallet } from "@xlabs-libs/wallet-aggregator-terra"; @@ -335,7 +340,7 @@ async function sei( try { const vaa = parseVaa(signedVAA); const transfer = parseTokenTransferPayload(vaa.payload); - const receiver = cosmos.humanAddress("sei", new Uint8Array(transfer.to)); + const receiver = cosmos.humanAddress("sei", transfer.to); const contractAddress = receiver === SEI_TRANSLATOR ? SEI_TRANSLATOR @@ -397,6 +402,40 @@ async function sei( } } +async function injective( + dispatch: any, + enqueueSnackbar: any, + wallet: InjectiveWallet, + walletAddress: string, + signedVAA: Uint8Array, + onSuccess?: () => void +) { + dispatch(setIsRedeeming(true)); + try { + const msg = await redeemOnInjective( + getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE), + walletAddress, + signedVAA + ); + const tx = await broadcastInjectiveTx( + wallet, + walletAddress, + msg, + "Wormhole - Complete Transfer" + ); + dispatch(setRedeemTx({ id: tx.txHash, block: tx.height })); + enqueueSnackbar(null, { + content: Transaction confirmed, + }); + onSuccess?.(); + } catch (e) { + enqueueSnackbar(null, { + content: {parseError(e)}, + }); + dispatch(setIsRedeeming(false)); + } +} + async function solana( dispatch: any, enqueueSnackbar: any, @@ -566,6 +605,7 @@ export function useHandleRedeem() { const { address: algoAccount, wallet: algoWallet } = useAlgorandWallet(); const { accountId: nearAccountId, wallet } = useNearContext(); const { account: aptosAddress, wallet: aptosWallet } = useAptosContext(); + const { wallet: injWallet, address: injAddress } = useInjectiveContext(); const suiWallet = useSuiWallet(); const seiWallet = useSeiWallet(); const seiAddress = seiWallet?.getAddress(); @@ -674,6 +714,20 @@ export function useHandleRedeem() { wallet, onSuccess ); + } else if ( + targetChain === CHAIN_ID_INJECTIVE && + injWallet && + injAddress && + signedVAA + ) { + injective( + dispatch, + enqueueSnackbar, + injWallet, + injAddress, + signedVAA, + onSuccess + ); } else if ( targetChain === CHAIN_ID_SUI && suiWallet?.getAddress() && @@ -695,6 +749,8 @@ export function useHandleRedeem() { algoAccount, nearAccountId, wallet, + injWallet, + injAddress, suiWallet, dispatch, enqueueSnackbar, @@ -749,6 +805,20 @@ export function useHandleRedeem() { !!signedVAA ) { algo(dispatch, enqueueSnackbar, algoWallet, signedVAA, onSuccess); + } else if ( + targetChain === CHAIN_ID_INJECTIVE && + injWallet && + injAddress && + signedVAA + ) { + injective( + dispatch, + enqueueSnackbar, + injWallet, + injAddress, + signedVAA, + onSuccess + ); } else if ( targetChain === CHAIN_ID_SEI && seiWallet && @@ -771,6 +841,8 @@ export function useHandleRedeem() { solPK, terraWallet, algoAccount, + injWallet, + injAddress, seiWallet, seiAddress, suiWallet, diff --git a/src/hooks/useHandleTransfer.tsx b/src/hooks/useHandleTransfer.tsx index 1dec01294..8ea67c908 100644 --- a/src/hooks/useHandleTransfer.tsx +++ b/src/hooks/useHandleTransfer.tsx @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_KLAYTN, CHAIN_ID_SOLANA, CHAIN_ID_XPLA, @@ -10,6 +11,7 @@ import { createNonce, getEmitterAddressAlgorand, getEmitterAddressEth, + getEmitterAddressInjective, getEmitterAddressSolana, getEmitterAddressTerra, getEmitterAddressXpla, @@ -18,6 +20,7 @@ import { isTerraChain, parseSequenceFromLogAlgorand, parseSequenceFromLogEth, + parseSequenceFromLogInjective, parseSequenceFromLogSolana, parseSequenceFromLogTerra, parseSequenceFromLogXpla, @@ -25,6 +28,7 @@ import { transferFromAlgorand, transferFromEth, transferFromEthNative, + transferFromInjective, transferFromTerra, transferFromXpla, transferFromAptos, @@ -117,9 +121,12 @@ import { signSendAndConfirm } from "../utils/solana"; import { postWithFees, waitForTerraExecution } from "../utils/terra"; import useTransferTargetAddressHex from "./useTransferTargetAddress"; import { postWithFeesXpla, waitForXplaExecution } from "../utils/xpla"; +import { broadcastInjectiveTx } from "../utils/injective"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; import { AlgorandWallet } from "@xlabs-libs/wallet-aggregator-algorand"; import { SolanaWallet } from "@xlabs-libs/wallet-aggregator-solana"; import { AptosWallet } from "@xlabs-libs/wallet-aggregator-aptos"; +import { InjectiveWallet } from "@xlabs-libs/wallet-aggregator-injective"; import { NearWallet } from "@xlabs-libs/wallet-aggregator-near"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { TerraWallet } from "@xlabs-libs/wallet-aggregator-terra"; @@ -796,6 +803,68 @@ async function terra( } } +async function injective( + dispatch: any, + enqueueSnackbar: any, + wallet: InjectiveWallet, + walletAddress: string, + asset: string, + amount: string, + decimals: number, + targetChain: ChainId, + targetAddress: Uint8Array, + maybeAdditionalPayload: MaybeAdditionalPayloadFn, + relayerFee?: string, + onError?: (error: any) => void, + onStart?: (extra?: Partial) => void +) { + dispatch(setIsSending(true)); + try { + const baseAmountParsed = parseUnits(amount, decimals); + const feeParsed = parseUnits(relayerFee || "0", decimals); + const transferAmountParsed = baseAmountParsed.add(feeParsed); + const additionalPayload = maybeAdditionalPayload(); + const tokenBridgeAddress = + getTokenBridgeAddressForChain(CHAIN_ID_INJECTIVE); + const msgs = await transferFromInjective( + walletAddress, + tokenBridgeAddress, + asset, + transferAmountParsed.toString(), + targetChain, + additionalPayload?.receivingContract || targetAddress, + feeParsed.toString(), + additionalPayload?.payload + ); + const tx = await broadcastInjectiveTx( + wallet, + walletAddress, + msgs, + "Wormhole - Initiate Transfer" + ); + onStart?.({ txId: tx.txHash }); + dispatch(setTransferTx({ id: tx.txHash, block: tx.height })); + enqueueSnackbar(null, { + content: Transaction confirmed, + }); + const sequence = parseSequenceFromLogInjective(tx); + if (!sequence) { + throw new Error("Sequence not found"); + } + const emitterAddress = await getEmitterAddressInjective(tokenBridgeAddress); + await fetchSignedVAA( + CHAIN_ID_INJECTIVE, + emitterAddress, + sequence, + enqueueSnackbar, + dispatch + ); + } catch (e) { + handleError(e, enqueueSnackbar, dispatch); + onError?.(e); + } +} + async function sei( dispatch: any, enqueueSnackbar: any, @@ -1022,6 +1091,7 @@ export function useHandleTransfer() { const { address: algoAccount, wallet: algoWallet } = useAlgorandWallet(); const { accountId: nearAccountId, wallet } = useNearContext(); const { account: aptosAddress, wallet: aptosWallet } = useAptosContext(); + const { wallet: injWallet, address: injAddress } = useInjectiveContext(); const suiWallet = useSuiWallet(); const seiWallet = useSeiWallet(); const seiAddress = seiWallet?.getAddress(); @@ -1294,6 +1364,29 @@ export function useHandleTransfer() { onError, onStart ); + } else if ( + sourceChain === CHAIN_ID_INJECTIVE && + injWallet && + injAddress && + !!sourceAsset && + decimals !== undefined && + !!targetAddress + ) { + injective( + dispatch, + enqueueSnackbar, + injWallet, + injAddress, + sourceAsset, + amount, + decimals, + targetChain, + targetAddress, + maybeAdditionalPayload, + relayerFee, + onError, + onStart + ); } else if ( sourceChain === CHAIN_ID_SUI && suiWallet?.isConnected() && @@ -1341,6 +1434,8 @@ export function useHandleTransfer() { nearAccountId, wallet, aptosAddress, + injWallet, + injAddress, suiWallet, dispatch, enqueueSnackbar, diff --git a/src/hooks/useInjectiveMetadata.ts b/src/hooks/useInjectiveMetadata.ts new file mode 100644 index 000000000..940302b3f --- /dev/null +++ b/src/hooks/useInjectiveMetadata.ts @@ -0,0 +1,88 @@ +import { parseSmartContractStateResponse } from "@certusone/wormhole-sdk"; +import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; +import { useLayoutEffect, useMemo, useState } from "react"; +import { DataWrapper } from "../store/helpers"; +import { getInjectiveWasmClient } from "../utils/injective"; + +export type InjectiveMetadata = { + symbol?: string; + logo?: string; + tokenName?: string; + decimals?: number; +}; + +const fetchSingleMetadata = async (address: string, client: ChainGrpcWasmApi) => + client + .fetchSmartContractState( + address, + Buffer.from(JSON.stringify({ token_info: {} })).toString("base64") + ) + .then((data) => { + const parsed = parseSmartContractStateResponse(data); + return { + symbol: parsed.symbol, + tokenName: parsed.name, + decimals: parsed.decimals, + } as InjectiveMetadata; + }); + +const fetchInjectiveMetadata = async (addresses: string[]) => { + const client = getInjectiveWasmClient(); + const promises: Promise[] = []; + addresses.forEach((address) => { + promises.push(fetchSingleMetadata(address, client)); + }); + const resultsArray = await Promise.all(promises); + const output = new Map(); + addresses.forEach((address, index) => { + output.set(address, resultsArray[index]); + }); + + return output; +}; + +const useInjectiveMetadata = ( + addresses: string[] +): DataWrapper> => { + const [isFetching, setIsFetching] = useState(false); + const [error, setError] = useState(""); + const [data, setData] = useState | null>(null); + + useLayoutEffect(() => { + let cancelled = false; + if (addresses.length) { + setIsFetching(true); + setError(""); + setData(null); + fetchInjectiveMetadata(addresses).then( + (results) => { + if (!cancelled) { + setData(results); + setIsFetching(false); + } + }, + () => { + if (!cancelled) { + setError("Could not retrieve contract metadata"); + setIsFetching(false); + } + } + ); + } + return () => { + cancelled = true; + }; + }, [addresses]); + + return useMemo( + () => ({ + data, + isFetching, + error, + receivedAt: null, + }), + [data, isFetching, error] + ); +}; + +export default useInjectiveMetadata; diff --git a/src/hooks/useInjectiveNativeBalances.ts b/src/hooks/useInjectiveNativeBalances.ts new file mode 100644 index 000000000..ea0b5386b --- /dev/null +++ b/src/hooks/useInjectiveNativeBalances.ts @@ -0,0 +1,52 @@ +import { MutableRefObject, useEffect, useMemo, useState } from "react"; +import { getInjectiveBankClient } from "../utils/injective"; + +export interface InjectiveNativeBalances { + [index: string]: string; +} + +export default function useInjectiveNativeBalances( + walletAddress?: string, + refreshRef?: MutableRefObject<() => void> +) { + const [isLoading, setIsLoading] = useState(true); + const [balances, setBalances] = useState( + {} + ); + const [refresh, setRefresh] = useState(false); + useEffect(() => { + if (refreshRef) { + refreshRef.current = () => { + setRefresh(true); + }; + } + }, [refreshRef]); + useEffect(() => { + setRefresh(false); + if (walletAddress) { + setIsLoading(true); + setBalances(undefined); + const client = getInjectiveBankClient(); + client + .fetchBalances(walletAddress) + .then(({ balances }) => { + const nativeBalances = balances.reduce((obj, { denom, amount }) => { + obj[denom] = amount; + return obj; + }, {} as InjectiveNativeBalances); + setIsLoading(false); + setBalances(nativeBalances); + }) + .catch((e) => { + console.error(e); + setIsLoading(false); + setBalances(undefined); + }); + } else { + setIsLoading(false); + setBalances(undefined); + } + }, [walletAddress, refresh]); + const value = useMemo(() => ({ isLoading, balances }), [isLoading, balances]); + return value; +} diff --git a/src/hooks/useIsWalletReady.ts b/src/hooks/useIsWalletReady.ts index fad0feb0b..ca5e99a82 100644 --- a/src/hooks/useIsWalletReady.ts +++ b/src/hooks/useIsWalletReady.ts @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_XPLA, @@ -18,6 +19,7 @@ import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import { APTOS_NETWORK, CLUSTER, getEvmChainId } from "../utils/consts"; import { useXplaWallet } from "../contexts/XplaWalletContext"; import { useAptosContext } from "../contexts/AptosWalletContext"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { useSuiWallet } from "../contexts/SuiWalletContext"; import { useSeiWallet } from "../contexts/SeiWalletContext"; @@ -67,6 +69,8 @@ function useIsWalletReady( const hasCorrectAptosNetwork = aptosNetwork?.name?.toLowerCase().includes(APTOS_NETWORK.toLowerCase()) || (CLUSTER === "devnet" && aptosNetwork?.chainId === "4"); + const { address: injAddress } = useInjectiveContext(); + const hasInjWallet = !!injAddress; const suiWallet = useSuiWallet(); const suiAddress = suiWallet?.getAddress(); const seiWallet = useSeiWallet(); @@ -108,6 +112,9 @@ function useIsWalletReady( ); } } + if (chainId === CHAIN_ID_INJECTIVE && hasInjWallet && injAddress) { + return createWalletStatus(true, undefined, injAddress); + } if (chainId === CHAIN_ID_SUI && suiAddress) { return createWalletStatus(true, undefined, suiAddress); } @@ -148,6 +155,8 @@ function useIsWalletReady( hasAptosWallet, aptosAddress, hasCorrectAptosNetwork, + hasInjWallet, + injAddress, suiAddress, hasSeiWallet, seiAddress, diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 34d15b28a..ac17eb6eb 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_TERRA2, @@ -20,6 +21,9 @@ import { Metadata } from "../utils/metaplex"; import useAlgoMetadata, { AlgoMetadata } from "./useAlgoMetadata"; import useAptosMetadata, { AptosMetadata } from "./useAptosMetadata"; import useEvmMetadata, { EvmMetadata } from "./useEvmMetadata"; +import useInjectiveMetadata, { + InjectiveMetadata, +} from "./useInjectiveMetadata"; import useMetaplexData from "./useMetaplexData"; import useNearMetadata from "./useNearMetadata"; import useSolanaTokenMap from "./useSolanaTokenMap"; @@ -237,6 +241,33 @@ const constructAptosMetadata = ( }; }; +const constructInjectiveMetadata = ( + addresses: string[], + metadataMap: DataWrapper> +) => { + const isFetching = metadataMap.isFetching; + const error = metadataMap.error; + const receivedAt = metadataMap.receivedAt; + const data = new Map(); + addresses.forEach((address) => { + const meta = metadataMap.data?.get(address); + const obj = { + symbol: meta?.symbol || undefined, + logo: undefined, + tokenName: meta?.tokenName || undefined, + decimals: meta?.decimals, + }; + data.set(address, obj); + }); + + return { + isFetching, + error, + receivedAt, + data, + }; +}; + const constructSuiMetadata = ( addresses: string[], metadataMap: DataWrapper> @@ -292,6 +323,9 @@ export default function useMetadata( const aptosAddresses = useMemo(() => { return chainId === CHAIN_ID_APTOS ? addresses : []; }, [chainId, addresses]); + const injAddresses = useMemo(() => { + return chainId === CHAIN_ID_INJECTIVE ? addresses : []; + }, [chainId, addresses]); const seiAddresses = useMemo(() => { return chainId === CHAIN_ID_SEI ? addresses : []; }, [chainId, addresses]); @@ -309,6 +343,7 @@ export default function useMetadata( const nearMetadata = useNearMetadata(nearAddresses); const xplaMetadata = useXplaMetadata(xplaAddresses); const aptosMetadata = useAptosMetadata(aptosAddresses); + const injMetadata = useInjectiveMetadata(injAddresses); const suiMetadata = useSuiMetadata(suiAddresses); const seiMetadata = useSeiMetadata(seiAddresses); @@ -333,6 +368,8 @@ export default function useMetadata( ? constructXplaMetadata(xplaAddresses, xplaMetadata) : chainId === CHAIN_ID_APTOS ? constructAptosMetadata(aptosAddresses, aptosMetadata) + : chainId === CHAIN_ID_INJECTIVE + ? constructInjectiveMetadata(injAddresses, injMetadata) : chainId === CHAIN_ID_SEI ? constructSeiMetadata(seiAddresses, seiMetadata) : chainId === CHAIN_ID_SUI @@ -356,6 +393,8 @@ export default function useMetadata( xplaMetadata, aptosAddresses, aptosMetadata, + injAddresses, + injMetadata, suiAddresses, suiMetadata, seiAddresses, diff --git a/src/hooks/useOriginalAsset.ts b/src/hooks/useOriginalAsset.ts index 52b08c6e2..dd93729b9 100644 --- a/src/hooks/useOriginalAsset.ts +++ b/src/hooks/useOriginalAsset.ts @@ -2,6 +2,7 @@ import { ChainId, CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_TERRA2, @@ -10,12 +11,14 @@ import { getOriginalAssetAptos, getOriginalAssetCosmWasm, getOriginalAssetEth, + getOriginalAssetInjective, getOriginalAssetSol, getTypeFromExternalAddress, hexToNativeAssetString, isEVMChain, isTerraChain, queryExternalId, + queryExternalIdInjective, uint8ArrayToHex, uint8ArrayToNative, CHAIN_ID_SUI, @@ -62,6 +65,7 @@ import { import useIsWalletReady from "./useIsWalletReady"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { getAptosClient } from "../utils/aptos"; +import { getInjectiveWasmClient } from "../utils/injective"; import { getSuiProvider } from "../utils/sui"; export type OriginalAssetInfo = { @@ -123,6 +127,11 @@ export async function getOriginalAssetToken( getTokenBridgeAddressForChain(CHAIN_ID_APTOS), foreignNativeStringAddress ); + } else if (foreignChain === CHAIN_ID_INJECTIVE) { + promise = await getOriginalAssetInjective( + foreignNativeStringAddress, + getInjectiveWasmClient() as any + ); } else if (foreignChain === CHAIN_ID_SUI) { promise = await getOriginalAssetSui( getSuiProvider(), @@ -330,6 +339,16 @@ function useOriginalAsset( getTokenBridgeAddressForChain(CHAIN_ID_APTOS), uint8ArrayToHex(result.assetAddress) ).then((tokenId) => setOriginAddress(tokenId || null)); + } else if (result.chainId === CHAIN_ID_INJECTIVE) { + const client = getInjectiveWasmClient(); + const tokenBridgeAddress = getTokenBridgeAddressForChain( + result.chainId + ); + queryExternalIdInjective( + client as any, + tokenBridgeAddress, + uint8ArrayToHex(result.assetAddress) + ).then((tokenId) => setOriginAddress(tokenId)); } else if (result.chainId === CHAIN_ID_SUI) { getForeignAssetSui( getSuiProvider(), diff --git a/src/hooks/useSyncTargetAddress.ts b/src/hooks/useSyncTargetAddress.ts index 09c1ebab5..6dabcba37 100644 --- a/src/hooks/useSyncTargetAddress.ts +++ b/src/hooks/useSyncTargetAddress.ts @@ -9,6 +9,7 @@ import { isEVMChain, isTerraChain, uint8ArrayToHex, + CHAIN_ID_INJECTIVE, CHAIN_ID_SUI, } from "@certusone/wormhole-sdk"; import { arrayify, zeroPad } from "@ethersproject/bytes"; @@ -41,6 +42,7 @@ import { getTransactionLastResult } from "near-api-js/lib/providers"; import BN from "bn.js"; import { useXplaWallet } from "../contexts/XplaWalletContext"; import { useAptosContext } from "../contexts/AptosWalletContext"; +import { useInjectiveContext } from "../contexts/InjectiveWalletContext"; import { useTerraWallet } from "../contexts/TerraWalletContext"; import { useSuiWallet } from "../contexts/SuiWalletContext"; import { useSeiWallet } from "../contexts/SeiWalletContext"; @@ -64,6 +66,7 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) { const { address: algoAccount } = useAlgorandWallet(); const { account: aptosAddress } = useAptosContext(); const { accountId: nearAccountId, wallet } = useNearContext(); + const { address: injAddress } = useInjectiveContext(); const isTBTC = useSelector(selectTransferIsTBTC); const suiWallet = useSuiWallet(); const suiAddress = suiWallet?.getAddress(); @@ -160,6 +163,12 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) { uint8ArrayToHex(decodeAddress(algoAccount).publicKey) ) ); + } else if (targetChain === CHAIN_ID_INJECTIVE && injAddress) { + dispatch( + setTargetAddressHex( + uint8ArrayToHex(zeroPad(cosmos.canonicalAddress(injAddress), 32)) + ) + ); } else if (targetChain === CHAIN_ID_NEAR && nearAccountId && wallet) { (async () => { try { @@ -240,6 +249,7 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) { wallet, xplaWallet, aptosAddress, + injAddress, suiAddress, isTBTC, seiAddress, diff --git a/src/index.tsx b/src/index.tsx index 79e3263cc..b82896699 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,7 @@ import { CHAIN_ID_ALGORAND, CHAIN_ID_APTOS, CHAIN_ID_ETH, + CHAIN_ID_INJECTIVE, CHAIN_ID_NEAR, CHAIN_ID_SOLANA, CHAIN_ID_SUI, @@ -23,6 +24,7 @@ import { getAlgorandWallets } from "./contexts/AlgorandWalletContext"; import { getWrappedWallets as getWrappedAptosWallets } from "./contexts/AptosWalletContext"; import { BetaContextProvider } from "./contexts/BetaContext"; import { getEvmWallets } from "./contexts/EthereumProviderContext"; +import { getInjectiveWallets } from "./contexts/InjectiveWalletContext"; import { getNearWallets } from "./contexts/NearWalletContext"; import { getWrappedWallets as getWrappedSolanaWallets } from "./contexts/SolanaWalletContext"; import { getSuiWallets } from "./contexts/SuiWalletContext"; @@ -42,6 +44,7 @@ const AGGREGATOR_WALLETS_BUILDER = async () => { [CHAIN_ID_ETH]: getEvmWallets(), [CHAIN_ID_SOLANA]: getWrappedSolanaWallets(), [CHAIN_ID_APTOS]: getWrappedAptosWallets(), + [CHAIN_ID_INJECTIVE]: getInjectiveWallets(), [CHAIN_ID_NEAR]: await getNearWallets(), [CHAIN_ID_TERRA2]: await getTerraWallets(), [CHAIN_ID_XPLA]: await getXplaWallets(), diff --git a/src/utils/coinGecko.ts b/src/utils/coinGecko.ts index d4e3bb30d..db2fec415 100644 --- a/src/utils/coinGecko.ts +++ b/src/utils/coinGecko.ts @@ -367,6 +367,8 @@ export const COIN_GECKO_IMAGE_URLS: CoinGeckoIdToImageUrl = { "https://assets.coingecko.com/coins/images/21976/small/i-DYP-Logo-1.png?1640570294", infinitup: "https://assets.coingecko.com/coins/images/18890/small/fc_qo2M7_400x400.jpg?1633749805", + "injective-protocol": + "https://assets.coingecko.com/coins/images/12882/small/Secondary_Symbol.png?1628233237", insure: "https://assets.coingecko.com/coins/images/10354/small/logo-grey-circle.png?1614910406", "interest-bearing-bitcoin": diff --git a/src/utils/consts.ts b/src/utils/consts.ts index 0b66eaff4..8df853c8a 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -11,6 +11,7 @@ import { CHAIN_ID_CELO, CHAIN_ID_ETH, CHAIN_ID_FANTOM, + CHAIN_ID_INJECTIVE, CHAIN_ID_KARURA, CHAIN_ID_KLAYTN, CHAIN_ID_MOONBEAM, @@ -76,8 +77,11 @@ import xplaIcon from "../icons/xpla.svg"; import evmosIcon from "../icons/evmos.svg"; import osmosIcon from "../icons/osmos.svg"; import kujiraIcon from "../icons/kujira.svg"; +import injectiveIcon from "../icons/injective.svg"; import { ConnectConfig, keyStores } from "near-api-js"; import { AptosNetwork } from "./aptos"; +import { getNetworkInfo, Network } from "@injectivelabs/networks"; +import { ChainId as InjectiveChainId } from "@injectivelabs/ts-types"; import { ChainConfiguration } from "@sei-js/react"; import { Connection } from "@mysten/sui.js"; import { chainToIcon } from "@wormhole-foundation/sdk-icons"; @@ -171,6 +175,11 @@ export const CHAINS: ChainInfo[] = name: "Fantom", logo: fantomIcon, }, + { + id: CHAIN_ID_INJECTIVE, + name: "Injective", + logo: injectiveIcon, + }, { id: CHAIN_ID_KARURA, name: "Karura", @@ -304,6 +313,11 @@ export const CHAINS: ChainInfo[] = name: "Fantom", logo: fantomIcon, }, + { + id: CHAIN_ID_INJECTIVE, + name: "Injective", + logo: injectiveIcon, + }, { id: CHAIN_ID_KARURA, name: "Karura", @@ -689,6 +703,8 @@ export const getDefaultNativeCurrencySymbol = (chainId: ChainId) => ? "APTOS" : chainId === CHAIN_ID_ARBITRUM ? "ETH" + : chainId === CHAIN_ID_INJECTIVE + ? "INJ" : chainId === CHAIN_ID_SUI ? "SUI" : ""; @@ -938,6 +954,41 @@ export const SEI_TRANSLATOR = export const SEI_TRANSLATER_TARGET = cosmos.canonicalAddress(SEI_TRANSLATOR); export const SEI_DECIMALS = 6; +export const getInjectiveNetworkName = () => { + if (CLUSTER === "mainnet") { + return Network.MainnetSentry; + } else if (CLUSTER === "testnet") { + return Network.TestnetK8s; + } + throw Error("Unsupported injective network"); +}; +export const getInjectiveNetwork = () => { + if (CLUSTER === "mainnet") { + return Network.MainnetSentry; + } else if (CLUSTER === "testnet") { + return Network.TestnetK8s; + } + throw Error("Unsupported injective network"); +}; + +export const getInjectiveNetworkInfo = () => { + if (CLUSTER === "mainnet") { + return getNetworkInfo(Network.MainnetSentry); + } else if (CLUSTER === "testnet") { + return getNetworkInfo(Network.TestnetK8s); + } + throw Error("Unsupported injective network"); +}; + +export const getInjectiveNetworkChainId = () => { + if (CLUSTER === "mainnet") { + return InjectiveChainId.Mainnet; + } else if (CLUSTER === "testnet") { + return InjectiveChainId.Testnet; + } + throw Error("Unsupported injective network"); +}; + export const SUI_CONNECTION = CLUSTER === "mainnet" ? new Connection({ fullnode: "https://fullnode.mainnet.sui.io:443" }) @@ -2045,6 +2096,9 @@ export const DISABLED_TOKEN_TRANSFERS: { [CHAIN_ID_BSC]: { "0xa2B726B1145A4773F68593CF171187d8EBe4d495": [], // INJ }, + [CHAIN_ID_INJECTIVE]: { + inj: [], // INJ + }, }; export const getIsTokenTransferDisabled = ( @@ -2088,6 +2142,15 @@ export const DISABLED_TOKEN_REASONS: { }, }, // INJ }, + [CHAIN_ID_INJECTIVE]: { + inj: { + text: "Transfers of INJ token can be made through the Injective Bridge.", + link: { + text: "Click here to go to Injective Bridge", + url: "https://hub.injective.network/bridge/", + }, + }, // INJ + }, }; export const getIsTokenTransferDisabledReasons = ( diff --git a/src/utils/injective.ts b/src/utils/injective.ts new file mode 100644 index 000000000..64e04d8fc --- /dev/null +++ b/src/utils/injective.ts @@ -0,0 +1,62 @@ +import { cosmos, isNativeDenomInjective } from "@certusone/wormhole-sdk"; +import { + ChainGrpcBankApi, + ChainGrpcWasmApi, + Msgs, + TxGrpcClient, +} from "@injectivelabs/sdk-ts"; +import { InjectiveWallet } from "@xlabs-libs/wallet-aggregator-injective"; +import { getInjectiveNetworkInfo } from "./consts"; + +export const NATIVE_INJECTIVE_DECIMALS = 18; + +export const INJECTIVE_NATIVE_DENOM = "inj"; + +export const getInjectiveWasmClient = () => + new ChainGrpcWasmApi(getInjectiveNetworkInfo().grpc); + +export const getInjectiveBankClient = () => + new ChainGrpcBankApi(getInjectiveNetworkInfo().grpc); + +export const getInjectiveTxClient = () => + new TxGrpcClient(getInjectiveNetworkInfo().grpc); + +export const isValidInjectiveAddress = (address: string) => { + if (isNativeDenomInjective(address)) { + return true; + } + try { + const startsWithInj = address && address.startsWith("inj"); + const isParsable = cosmos.canonicalAddress(address); + const isLengthOk = isParsable.length === 20; + return !!(startsWithInj && isParsable && isLengthOk); + } catch (error) { + return false; + } +}; + +export const formatNativeDenom = (denom: string) => + denom === INJECTIVE_NATIVE_DENOM ? "INJ" : ""; + +export const broadcastInjectiveTx = async ( + wallet: InjectiveWallet, + address: string, + msgs: Msgs | Msgs[], + memo: string = "" +) => { + const client = getInjectiveTxClient(); + const result = await wallet.sendTransaction({ + // @ts-ignore + msgs, + address, + memo, + }); + const tx = await client.fetchTxPoll(result.id); + if (!tx) { + throw new Error("Unable to fetch transaction"); + } + if (tx.code !== 0) { + throw new Error(`Transaction failed: ${tx.rawLog}`); + } + return tx; +};