Merge pull request #6 from kev1n-peters/near-ui

Near UI support
This commit is contained in:
apollo-othermike 2022-09-10 14:07:43 -04:00 committed by GitHub
commit e5cff9c0cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2466 additions and 137 deletions

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,15 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@near-wallet-selector/core": "^7.0.1",
"@near-wallet-selector/default-wallets": "^7.0.1",
"@near-wallet-selector/math-wallet": "^7.0.1",
"@near-wallet-selector/meteor-wallet": "^7.0.1",
"@near-wallet-selector/modal-ui": "^7.0.1",
"@near-wallet-selector/my-near-wallet": "^7.0.1",
"@near-wallet-selector/near-wallet": "^7.0.1",
"@near-wallet-selector/nightly": "^7.0.1",
"@near-wallet-selector/sender": "^7.0.1",
"@project-serum/serum": "^0.13.60",
"@randlabs/myalgo-connect": "^1.1.3",
"@reduxjs/toolkit": "^1.6.1",
@ -30,6 +39,7 @@
"ethers": "^5.6.8",
"js-base64": "^3.6.1",
"luxon": "^2.3.1",
"near-api-js": "^0.44.2",
"notistack": "^1.0.10",
"numeral": "^2.0.6",
"react": "^17.0.2",
@ -40,6 +50,7 @@
"react-table": "^7.7.0",
"recharts": "^2.1.9",
"redux": "^3.7.2",
"rxjs": "^7.5.6",
"use-debounce": "^7.0.0"
},
"scripts": {

View File

@ -1,12 +1,14 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
isEVMChain,
isTerraChain,
} from "@certusone/wormhole-sdk";
import AlgorandWalletKey from "./AlgorandWalletKey";
import EthereumSignerKey from "./EthereumSignerKey";
import NearWalletKey from "./NearWalletKey";
import SolanaWalletKey from "./SolanaWalletKey";
import TerraWalletKey from "./TerraWalletKey";
@ -23,6 +25,9 @@ function KeyAndBalance({ chainId }: { chainId: ChainId }) {
if (chainId === CHAIN_ID_ALGORAND) {
return <AlgorandWalletKey />;
}
if (chainId === CHAIN_ID_NEAR) {
return <NearWalletKey />;
}
return null;
}

View File

@ -0,0 +1,17 @@
import { useNearContext } from "../contexts/NearWalletContext";
import ToggleConnectedButton from "./ToggleConnectedButton";
const NearWalletKey = () => {
const { connect, disconnect, accountId: activeAccount } = useNearContext();
return (
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={!!activeAccount}
pk={activeAccount || ""}
/>
);
};
export default NearWalletKey;

View File

@ -3,6 +3,7 @@ import {
CHAIN_ID_ACALA,
CHAIN_ID_ALGORAND,
CHAIN_ID_KARURA,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
getEmitterAddressAlgorand,
@ -45,11 +46,13 @@ import { LCDClient } from "@terra-money/terra.js";
import algosdk from "algosdk";
import axios from "axios";
import { ethers } from "ethers";
import { base58 } from "ethers/lib/utils";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useAcalaRelayerInfo } from "../hooks/useAcalaRelayerInfo";
import useIsWalletReady from "../hooks/useIsWalletReady";
import useRelayersAvailable, { Relayer } from "../hooks/useRelayersAvailable";
@ -71,8 +74,14 @@ import {
SOL_TOKEN_BRIDGE_ADDRESS,
getTerraConfig,
WORMHOLE_RPC_HOSTS,
NEAR_TOKEN_BRIDGE_ACCOUNT,
} from "../utils/consts";
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import {
getEmitterAddressNear,
makeNearAccount,
parseSequenceFromLogNear,
} from "../utils/near";
import parseError from "../utils/parseError";
import { queryExternalId } from "../utils/terra";
import ButtonWithLoader from "./ButtonWithLoader";
@ -183,6 +192,24 @@ async function evm(
}
}
async function near(tx: string, enqueueSnackbar: any, nearAccountId: string) {
try {
const account = await makeNearAccount(nearAccountId);
const receipt = await account.connection.provider.txStatusReceipts(
base58.decode(tx),
nearAccountId
);
const sequence = parseSequenceFromLogNear(receipt);
if (!sequence) {
throw new Error("Sequence not found");
}
const emitterAddress = getEmitterAddressNear(NEAR_TOKEN_BRIDGE_ACCOUNT);
return await fetchSignedVAA(CHAIN_ID_NEAR, emitterAddress, sequence);
} catch (e) {
return handleError(e, enqueueSnackbar);
}
}
async function solana(tx: string, enqueueSnackbar: any, nft: boolean) {
try {
const connection = new Connection(SOLANA_HOST, "confirmed");
@ -369,6 +396,7 @@ export default function Recovery() {
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
const [isVAAPending, setIsVAAPending] = useState(false);
const [terra2TokenId, setTerra2TokenId] = useState("");
const { accountId: nearAccountId } = useNearContext();
const { isReady, statusMessage } = useIsWalletReady(recoverySourceChain);
const walletConnectError =
isEVMChain(recoverySourceChain) && !isReady ? statusMessage : "";
@ -515,6 +543,26 @@ export default function Recovery() {
setIsVAAPending(isPending);
}
})();
} else if (recoverySourceChain === CHAIN_ID_NEAR && nearAccountId) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
const { vaa, isPending, error } = await near(
recoverySourceTx,
enqueueSnackbar,
nearAccountId
);
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
setRecoverySignedVAA(vaa);
}
if (error) {
setRecoverySourceTxError(error);
}
setIsVAAPending(isPending);
}
})();
}
return () => {
cancelled = true;
@ -527,6 +575,7 @@ export default function Recovery() {
enqueueSnackbar,
isNFT,
isReady,
nearAccountId,
]);
const handleTypeChange = useCallback((event) => {
setRecoverySourceChain((prevChain) =>

View File

@ -18,6 +18,7 @@ import {
isTerraChain,
CHAIN_ID_TERRA2,
} from "@certusone/wormhole-sdk";
import { CHAIN_ID_NEAR } from "@certusone/wormhole-sdk/lib/esm";
import { Button, makeStyles, Typography } from "@material-ui/core";
import { Transaction } from "../store/transferSlice";
import { CLUSTER, getExplorerName } from "../utils/consts";
@ -124,6 +125,10 @@ export default function ShowTx({
? `https://${CLUSTER === "testnet" ? "testnet." : ""}algoexplorer.io/tx/${
tx?.id
}`
: chainId === CHAIN_ID_NEAR
? `https://explorer.${
CLUSTER === "testnet" ? "testnet." : ""
}near.org/transactions/${tx?.id}`
: undefined;
const explorerName = getExplorerName(chainId);

View File

@ -19,6 +19,7 @@ import {
isTerraChain,
CHAIN_ID_TERRA2,
TerraChainId,
CHAIN_ID_NEAR,
} from "@certusone/wormhole-sdk";
import { Button, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { FileCopy, OpenInNew } from "@material-ui/icons";
@ -188,6 +189,10 @@ export default function SmartAddress({
? `https://${CLUSTER === "testnet" ? "testnet." : ""}algoexplorer.io/${
isAsset ? "asset" : "address"
}/${useableAddress}`
: chainId === CHAIN_ID_NEAR
? `https://explorer.${
CLUSTER === "testnet" ? "testnet." : ""
}near.org/accounts/${useableAddress}`
: undefined;
const explorerName = getExplorerName(chainId);

View File

@ -1,7 +1,9 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
isEVMChain,
nativeToHexString,
} from "@certusone/wormhole-sdk";
@ -148,10 +150,21 @@ function SecondaryAssetInformation({
<RegisterNowButtonCore
originChain={originAssetInfo?.originChain || undefined}
originAsset={
nativeToHexString(
originAssetInfo?.originAddress || undefined,
originAssetInfo?.originChain || CHAIN_ID_SOLANA // this should exist
) || undefined
// use pre-image for these
originAssetInfo?.originChain === CHAIN_ID_TERRA2 ||
originAssetInfo?.originChain === CHAIN_ID_NEAR
? originAssetInfo?.originAddress || undefined
: nativeToHexString(
originAssetInfo?.originAddress || undefined,
originAssetInfo?.originChain || CHAIN_ID_SOLANA // this should exist
) || undefined
}
forceAsset={
// use pre-image for these
originAssetInfo?.originChain === CHAIN_ID_TERRA2 ||
originAssetInfo?.originChain === CHAIN_ID_NEAR
? originAssetInfo?.originAddress || undefined
: undefined
}
targetChain={chainId}
/>

View File

@ -0,0 +1,108 @@
import { CHAIN_ID_NEAR } from "@certusone/wormhole-sdk";
import { formatUnits } from "ethers/lib/utils";
import { useCallback } from "react";
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { fetchSingleMetadata } from "../../hooks/useNearMetadata";
import { DataWrapper } from "../../store/helpers";
import { NFTParsedTokenAccount } from "../../store/nftSlice";
import { ParsedTokenAccount } from "../../store/transferSlice";
import { makeNearAccount } from "../../utils/near";
import TokenPicker, { BasicAccountRender } from "./TokenPicker";
type NearTokenPickerProps = {
value: ParsedTokenAccount | null;
onChange: (newValue: ParsedTokenAccount | null) => void;
tokenAccounts: DataWrapper<ParsedTokenAccount[]> | undefined;
disabled: boolean;
resetAccounts: (() => void) | undefined;
};
const returnsFalse = () => false;
export default function NearTokenPicker(props: NearTokenPickerProps) {
const { value, onChange, disabled, tokenAccounts, resetAccounts } = props;
const { walletAddress } = useIsWalletReady(CHAIN_ID_NEAR);
const resetAccountWrapper = useCallback(() => {
resetAccounts && resetAccounts();
}, [resetAccounts]);
const isLoading = tokenAccounts?.isFetching || false;
const onChangeWrapper = useCallback(
async (account: NFTParsedTokenAccount | null) => {
if (account === null) {
onChange(null);
return Promise.resolve();
}
onChange(account);
return Promise.resolve();
},
[onChange]
);
const lookupNearAddress = useCallback(
(lookupAsset: string) => {
if (!walletAddress) {
return Promise.reject("Wallet not connected");
}
return makeNearAccount(walletAddress)
.then((account) => {
return fetchSingleMetadata(lookupAsset, account)
.then((metadata) => {
return account
.viewFunction(lookupAsset, "ft_balance_of", {
account_id: walletAddress,
})
.then((amount) => {
return createParsedTokenAccount(
walletAddress,
lookupAsset,
amount,
metadata.decimals,
parseFloat(formatUnits(amount, metadata.decimals)),
formatUnits(amount, metadata.decimals).toString(),
metadata.symbol,
metadata.tokenName,
undefined,
false
);
})
.catch(() => Promise.reject());
})
.catch(() => Promise.reject());
})
.catch(() => Promise.reject());
},
[walletAddress]
);
const isSearchableAddress = useCallback(
(address: string) => address.length > 0,
[]
);
const RenderComp = useCallback(
({ account }: { account: NFTParsedTokenAccount }) => {
return BasicAccountRender(account, returnsFalse, false);
},
[]
);
return (
<TokenPicker
value={value}
options={tokenAccounts?.data || []}
RenderOption={RenderComp}
onChange={onChangeWrapper}
isValidAddress={isSearchableAddress}
getAddress={lookupNearAddress}
disabled={disabled}
resetAccounts={resetAccountWrapper}
error={""}
showLoader={isLoading}
nft={false}
chainId={CHAIN_ID_NEAR}
/>
);
}

View File

@ -1,6 +1,7 @@
//import Autocomplete from '@material-ui/lab/Autocomplete';
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
isEVMChain,
isTerraChain,
@ -27,6 +28,7 @@ import {
} from "../../store/transferSlice";
import AlgoTokenPicker from "./AlgoTokenPicker";
import EvmTokenPicker from "./EvmTokenPicker";
import NearTokenPicker from "./NearTokenPicker";
import RefreshButtonWrapper from "./RefreshButtonWrapper";
import SolanaTokenPicker from "./SolanaTokenPicker";
import TerraTokenPicker from "./TerraTokenPicker";
@ -125,6 +127,14 @@ export const TokenSelector = (props: TokenSelectorProps) => {
resetAccounts={maps?.resetAccounts}
tokenAccounts={maps?.tokenAccounts}
/>
) : lookupChain === CHAIN_ID_NEAR ? (
<NearTokenPicker
value={sourceParsedTokenAccount || null}
disabled={disabled}
onChange={handleOnChange}
resetAccounts={maps?.resetAccounts}
tokenAccounts={maps?.tokenAccounts}
/>
) : (
<TextField
variant="outlined"

View File

@ -17,6 +17,7 @@ import {
} from "../../store/selectors";
import {
ChainId,
CHAIN_ID_NEAR,
CHAIN_ID_TERRA2,
hexToNativeAssetString,
} from "@certusone/wormhole-sdk";
@ -25,10 +26,12 @@ export function RegisterNowButtonCore({
originChain,
originAsset,
targetChain,
forceAsset,
}: {
originChain: ChainId | undefined;
originAsset: string | undefined;
targetChain: ChainId;
forceAsset?: string;
}) {
const dispatch = useDispatch();
const history = useHistory();
@ -38,8 +41,8 @@ export function RegisterNowButtonCore({
const canSwitch = originChain && originAsset && !signedVAAHex;
const handleClick = useCallback(() => {
const nativeAsset = originChain
? originChain === CHAIN_ID_TERRA2
? sourceAsset // use the preimage address for terra2
? originChain === CHAIN_ID_TERRA2 || originChain === CHAIN_ID_NEAR
? sourceAsset || forceAsset // use the preimage address for terra2
: hexToNativeAssetString(originAsset, originChain)
: undefined;
if (originChain && originAsset && nativeAsset && canSwitch) {
@ -57,6 +60,7 @@ export function RegisterNowButtonCore({
targetChain,
history,
sourceAsset,
forceAsset,
]);
if (!canSwitch) return null;
return (

View File

@ -3,9 +3,11 @@ import {
hexToNativeString,
isEVMChain,
} from "@certusone/wormhole-sdk";
import { CHAIN_ID_NEAR } from "@certusone/wormhole-sdk/lib/esm";
import { makeStyles, Typography } from "@material-ui/core";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNearContext } from "../../contexts/NearWalletContext";
import useGetTargetParsedTokenAccounts from "../../hooks/useGetTargetParsedTokenAccounts";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
@ -24,6 +26,7 @@ import {
} from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { CHAINS, CLUSTER } from "../../utils/consts";
import { getEmitterAddressNear } from "../../utils/near";
import ButtonWithLoader from "../ButtonWithLoader";
import ChainSelect from "../ChainSelect";
import FeeMethodSelector from "../FeeMethodSelector";
@ -48,6 +51,8 @@ const useStyles = makeStyles((theme) => ({
}));
export const useTargetInfo = () => {
const { accountId: nearAccountId } = useNearContext();
const targetChain = useSelector(selectTransferTargetChain);
const targetAddressHex = useSelector(selectTransferTargetAddressHex);
const targetAsset = useSelector(selectTransferTargetAsset);
@ -58,7 +63,14 @@ export const useTargetInfo = () => {
const symbol = targetParsedTokenAccount?.symbol;
const logo = targetParsedTokenAccount?.logo;
const readableTargetAddress =
hexToNativeString(targetAddressHex, targetChain) || "";
targetChain === CHAIN_ID_NEAR
? // Near uses a hashed address, which isn't very readable - check that the hash matches and show them their account id
nearAccountId &&
// this just happens to be the same hashing mechanism as emitters
getEmitterAddressNear(nearAccountId) === targetAddressHex
? nearAccountId
: targetAddressHex || ""
: hexToNativeString(targetAddressHex, targetChain) || "";
return useMemo(
() => ({
targetChain,

View File

@ -0,0 +1,200 @@
import {
AccountState,
Network,
setupWalletSelector,
Wallet,
WalletSelector,
WalletSelectorState,
} from "@near-wallet-selector/core";
import { setupDefaultWallets } from "@near-wallet-selector/default-wallets";
import { setupMathWallet } from "@near-wallet-selector/math-wallet";
import { setupMeteorWallet } from "@near-wallet-selector/meteor-wallet";
import {
setupModal,
WalletSelectorModal,
} from "@near-wallet-selector/modal-ui";
import "@near-wallet-selector/modal-ui/styles.css";
import { setupMyNearWallet } from "@near-wallet-selector/my-near-wallet";
import { setupNearWallet } from "@near-wallet-selector/near-wallet";
import { setupNightly } from "@near-wallet-selector/nightly";
import { setupSender } from "@near-wallet-selector/sender";
import { KeyPair, WalletConnection } from "near-api-js";
import React, {
ReactChildren,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { distinctUntilChanged, map, Subscription } from "rxjs";
import { CLUSTER, NEAR_TOKEN_BRIDGE_ACCOUNT } from "../utils/consts";
// monkeypatch to allow for full permissions
// https://github.com/near/near-api-js/blob/96785cb3db14be593b6e6d013b6870ba56a212a8/packages/near-api-js/src/wallet-account.ts#L177
const LOGIN_WALLET_URL_SUFFIX = "/login/";
const PENDING_ACCESS_KEY_PREFIX = "pending_key"; // browser storage key for a pending access key (i.e. key has been generated but we are not sure it was added yet)
WalletConnection.prototype.requestSignIn = async function requestSignIn({
contractId,
methodNames,
successUrl,
failureUrl,
}: any) {
const currentUrl = new URL(window.location.href);
const newUrl = new URL(this._walletBaseUrl + LOGIN_WALLET_URL_SUFFIX);
newUrl.searchParams.set("success_url", successUrl || currentUrl.href);
newUrl.searchParams.set("failure_url", failureUrl || currentUrl.href);
if (contractId) {
/* Throws exception if contract account does not exist */
const contractAccount = await this._near.account(contractId);
await contractAccount.state();
// THIS IS THE EDIT
// newUrl.searchParams.set("contract_id", contractId);
const accessKey = KeyPair.fromRandom("ed25519");
newUrl.searchParams.set("public_key", accessKey.getPublicKey().toString());
await this._keyStore.setKey(
this._networkId,
PENDING_ACCESS_KEY_PREFIX + accessKey.getPublicKey(),
accessKey
);
}
if (methodNames) {
methodNames.forEach((methodName: any) => {
newUrl.searchParams.append("methodNames", methodName);
});
}
window.location.assign(newUrl.toString());
};
declare global {
interface Window {
selector: WalletSelector;
modal: WalletSelectorModal;
}
}
interface INearContext {
connect(): void;
disconnect(): void;
accounts: AccountState[];
accountId: string | null;
wallet: Wallet | null;
}
const NearContext = React.createContext<INearContext>({
connect: () => {},
disconnect: () => {},
accounts: [],
accountId: null,
wallet: null,
});
const NearDevnet: Network = {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
helperUrl: "",
explorerUrl: "",
indexerUrl: "",
};
export const NearContextProvider = ({
children,
}: {
children: ReactChildren;
}) => {
const [selector, setSelector] = useState<WalletSelector | null>(null);
const [modal, setModal] = useState<WalletSelectorModal | null>(null);
const [accounts, setAccounts] = useState<AccountState[]>([]);
const [accountId, setAccountId] = useState<string | null>(null);
const [wallet, setWallet] = useState<Wallet | null>(null);
useEffect(() => {
let cancelled = false;
let subscription: Subscription;
(async () => {
const selector = await setupWalletSelector({
network:
CLUSTER === "mainnet"
? "mainnet"
: "testnet"
? "testnet"
: NearDevnet,
modules: [
...(await setupDefaultWallets()),
setupNearWallet(),
setupMyNearWallet(),
setupSender(),
setupMathWallet(),
setupNightly(),
setupMeteorWallet(),
],
debug: true,
});
const modal = setupModal(selector, {
contractId: NEAR_TOKEN_BRIDGE_ACCOUNT || "",
});
const accounts = selector.store.getState().accounts;
subscription = selector.store.observable
.pipe(
map((state: WalletSelectorState) => state.accounts),
distinctUntilChanged()
)
.subscribe((nextAccounts: AccountState[]) => {
if (!cancelled) {
setAccounts(nextAccounts);
}
});
if (!cancelled) {
setSelector(selector);
setModal(modal);
setAccounts(accounts);
}
})();
return () => {
subscription?.unsubscribe();
cancelled = true;
};
}, []);
useEffect(() => {
const accountId =
accounts.find((account) => account.active)?.accountId || null;
setAccountId(accountId);
(async () => {
setWallet((await selector?.wallet()) || null);
})();
}, [selector, accounts]);
const connect = useCallback(() => {
modal?.show();
}, [modal]);
const disconnect = useCallback(() => {
modal?.hide();
selector
?.wallet()
.then((wallet) =>
wallet.signOut().catch((error) => console.error(error))
);
}, [selector, modal]);
const value = useMemo(
() => ({
connect,
disconnect,
accounts,
accountId,
wallet,
}),
[connect, disconnect, accounts, accountId, wallet]
);
return <NearContext.Provider value={value}>{children}</NearContext.Provider>;
};
export function useNearContext() {
return useContext(NearContext);
}

View File

@ -1,6 +1,7 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
getOriginalAssetAlgorand,
getOriginalAssetCosmWasm,
@ -21,6 +22,7 @@ import { Algodv2 } from "algosdk";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { setSourceWormholeWrappedInfo as setNFTSourceWormholeWrappedInfo } from "../store/nftSlice";
import {
selectNFTIsRecovery,
@ -38,10 +40,13 @@ import {
getNFTBridgeAddressForChain,
getTerraConfig,
getTokenBridgeAddressForChain,
NATIVE_NEAR_PLACEHOLDER,
NEAR_TOKEN_BRIDGE_ACCOUNT,
SOLANA_HOST,
SOL_NFT_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import { getOriginalAssetNear, makeNearAccount } from "../utils/near";
export interface StateSafeWormholeWrappedInfo {
isWrapped: boolean;
@ -76,6 +81,7 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
? setNFTSourceWormholeWrappedInfo
: setTransferSourceWormholeWrappedInfo;
const { provider } = useEthereumProvider();
const { accountId: nearAccountId } = useNearContext();
const isRecovery = useSelector(
nft ? selectNFTIsRecovery : selectTransferIsRecovery
);
@ -158,6 +164,25 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
}
} catch (e) {}
}
if (
sourceChain === CHAIN_ID_NEAR &&
nearAccountId &&
sourceAsset !== undefined
) {
try {
const account = await makeNearAccount(nearAccountId);
const wrappedInfo = makeStateSafe(
await getOriginalAssetNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
sourceAsset === NATIVE_NEAR_PLACEHOLDER ? "" : sourceAsset
)
);
if (!cancelled) {
dispatch(setSourceWormholeWrappedInfo(wrappedInfo));
}
} catch (e) {}
}
})();
return () => {
cancelled = true;
@ -171,6 +196,7 @@ function useCheckIfWormholeWrapped(nft?: boolean) {
nft,
setSourceWormholeWrappedInfo,
tokenId,
nearAccountId,
]);
}

View File

@ -1,7 +1,9 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
getForeignAssetAlgorand,
getForeignAssetEth,
getForeignAssetSolana,
@ -25,9 +27,19 @@ import {
SOLANA_HOST,
SOL_TOKEN_BRIDGE_ADDRESS,
getTerraConfig,
NEAR_TOKEN_BRIDGE_ACCOUNT,
NATIVE_NEAR_PLACEHOLDER,
NATIVE_NEAR_WH_ADDRESS,
} from "../utils/consts";
import useIsWalletReady from "./useIsWalletReady";
import { Algodv2 } from "algosdk";
import {
getEmitterAddressNear,
getForeignAssetNear,
makeNearAccount,
} from "../utils/near";
import { useNearContext } from "../contexts/NearWalletContext";
import { buildTokenId } from "@certusone/wormhole-sdk/lib/esm/cosmwasm/address";
export type ForeignAssetInfo = {
doesExist: boolean;
@ -43,6 +55,7 @@ function useFetchForeignAsset(
const { isReady } = useIsWalletReady(foreignChain, false);
const correctEvmNetwork = getEvmChainId(foreignChain);
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
const { accountId: nearAccountId } = useNearContext();
const [assetAddress, setAssetAddress] = useState<string | null>(null);
const [doesExist, setDoesExist] = useState<boolean | null>(null);
@ -50,6 +63,15 @@ function useFetchForeignAsset(
const [isLoading, setIsLoading] = useState(false);
const originAssetHex = useMemo(() => {
try {
if (originChain === CHAIN_ID_TERRA2) {
return buildTokenId(originAsset);
}
if (originChain === CHAIN_ID_NEAR) {
if (originAsset === NATIVE_NEAR_PLACEHOLDER) {
return NATIVE_NEAR_WH_ADDRESS;
}
return getEmitterAddressNear(originAsset);
}
return nativeToHexString(originAsset, originChain);
} catch (e) {
return null;
@ -148,6 +170,19 @@ function useFetchForeignAsset(
originAssetHex
);
}
: foreignChain === CHAIN_ID_NEAR && nearAccountId
? () => {
return makeNearAccount(nearAccountId)
.then((account) =>
getForeignAssetNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
originChain,
originAssetHex
)
)
.catch(() => Promise.reject("Failed to make Near account"));
}
: () => Promise.resolve(null);
getterFunc()
@ -193,6 +228,7 @@ function useFetchForeignAsset(
provider,
setArgs,
argsEqual,
nearAccountId,
]);
const compoundError = useMemo(() => {

View File

@ -1,6 +1,7 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
getForeignAssetAlgorand,
@ -25,6 +26,7 @@ import { ethers } from "ethers";
import { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import {
errorDataWrapper,
fetchDataWrapper,
@ -53,7 +55,15 @@ import {
SOL_NFT_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
getTerraConfig,
NEAR_TOKEN_BRIDGE_ACCOUNT,
NATIVE_NEAR_WH_ADDRESS,
NATIVE_NEAR_PLACEHOLDER,
} from "../utils/consts";
import {
getForeignAssetNear,
lookupHash,
makeNearAccount,
} from "../utils/near";
import { queryExternalId } from "../utils/terra";
function useFetchTargetAsset(nft?: boolean) {
@ -78,6 +88,7 @@ function useFetchTargetAsset(nft?: boolean) {
const { provider, chainId: evmChainId } = useEthereumProvider();
const correctEvmNetwork = getEvmChainId(targetChain);
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
const { accountId: nearAccountId } = useNearContext();
const [lastSuccessfulArgs, setLastSuccessfulArgs] = useState<{
isSourceAssetWormholeWrapped: boolean | undefined;
originChain: ChainId | undefined;
@ -134,6 +145,34 @@ function useFetchTargetAsset(nft?: boolean) {
)
);
}
} else if (originChain === CHAIN_ID_NEAR && nearAccountId) {
if (originAsset === NATIVE_NEAR_WH_ADDRESS) {
dispatch(
setTargetAsset(
receiveDataWrapper({
doesExist: true,
address: NATIVE_NEAR_PLACEHOLDER,
})
)
);
} else {
const account = await makeNearAccount(nearAccountId);
const tokenAccount = await lookupHash(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
originAsset || ""
);
if (!cancelled) {
dispatch(
setTargetAsset(
receiveDataWrapper({
doesExist: true,
address: tokenAccount[1] || null,
})
)
);
}
}
} else {
if (!cancelled) {
dispatch(
@ -302,6 +341,45 @@ function useFetchTargetAsset(nft?: boolean) {
}
}
}
if (
targetChain === CHAIN_ID_NEAR &&
originChain &&
originAsset &&
nearAccountId
) {
dispatch(setTargetAsset(fetchDataWrapper()));
try {
const account = await makeNearAccount(nearAccountId);
const asset = await getForeignAssetNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
originChain,
originAsset
);
if (!cancelled) {
dispatch(
setTargetAsset(
receiveDataWrapper({
doesExist: !!asset,
address: asset === null ? asset : asset.toString(),
})
)
);
setArgs();
}
} catch (e) {
console.error(e);
if (!cancelled) {
dispatch(
setTargetAsset(
errorDataWrapper(
"Unable to determine existence of wrapped asset"
)
)
);
}
}
}
})();
return () => {
cancelled = true;
@ -319,6 +397,7 @@ function useFetchTargetAsset(nft?: boolean) {
hasCorrectEvmNetwork,
argsMatchLastSuccess,
setArgs,
nearAccountId,
]);
}

View File

@ -1,5 +1,6 @@
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
getIsTransferCompletedAlgorand,
getIsTransferCompletedEth,
@ -14,6 +15,7 @@ import algosdk from "algosdk";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import {
selectTransferIsRecovery,
selectTransferTargetAddressHex,
@ -27,7 +29,9 @@ import {
SOLANA_HOST,
getTerraGasPricesUrl,
getTerraConfig,
NEAR_TOKEN_BRIDGE_ACCOUNT,
} from "../utils/consts";
import { getIsTransferCompletedNear, makeNearAccount } from "../utils/near";
import useIsWalletReady from "./useIsWalletReady";
import useTransferSignedVAA from "./useTransferSignedVAA";
@ -50,6 +54,7 @@ export default function useGetIsTransferCompleted(
const { isReady } = useIsWalletReady(targetChain, false);
const { provider, chainId: evmChainId } = useEthereumProvider();
const { accountId: nearAccountId } = useNearContext();
const signedVAA = useTransferSignedVAA();
const hasCorrectEvmNetwork = evmChainId === getEvmChainId(targetChain);
@ -160,6 +165,24 @@ export default function useGetIsTransferCompleted(
setIsLoading(false);
}
})();
} else if (targetChain === CHAIN_ID_NEAR && nearAccountId) {
setIsLoading(true);
(async () => {
try {
const account = await makeNearAccount(nearAccountId);
transferCompleted = await getIsTransferCompletedNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
signedVAA
);
} catch (error) {
console.error(error);
}
if (!cancelled) {
setIsTransferCompleted(transferCompleted);
setIsLoading(false);
}
})();
}
}
return () => {
@ -174,6 +197,7 @@ export default function useGetIsTransferCompleted(
isReady,
provider,
pollState,
nearAccountId,
]);
return { isTransferCompletedLoading: isLoading, isTransferCompleted };

View File

@ -11,6 +11,7 @@ import {
CHAIN_ID_FANTOM,
CHAIN_ID_KARURA,
CHAIN_ID_KLAYTN,
CHAIN_ID_NEAR,
CHAIN_ID_NEON,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
@ -40,6 +41,7 @@ import {
Provider,
useEthereumProvider,
} from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import acalaIcon from "../icons/acala.svg";
import auroraIcon from "../icons/aurora.svg";
@ -85,11 +87,16 @@ import {
ACA_DECIMALS,
ALGORAND_HOST,
ALGO_DECIMALS,
COVALENT_GET_TOKENS_URL,
BLOCKSCOUT_GET_TOKENS_URL,
CELO_ADDRESS,
CELO_DECIMALS,
COVALENT_GET_TOKENS_URL,
getDefaultNativeCurrencyAddressEvm,
KAR_ADDRESS,
KAR_DECIMALS,
logoOverrides,
NATIVE_NEAR_DECIMALS,
NATIVE_NEAR_PLACEHOLDER,
ROPSTEN_WETH_ADDRESS,
ROPSTEN_WETH_DECIMALS,
SOLANA_HOST,
@ -97,8 +104,6 @@ import {
WAVAX_DECIMALS,
WBNB_ADDRESS,
WBNB_DECIMALS,
CELO_ADDRESS,
CELO_DECIMALS,
WETH_ADDRESS,
WETH_AURORA_ADDRESS,
WETH_AURORA_DECIMALS,
@ -110,11 +115,11 @@ import {
WMATIC_ADDRESS,
WMATIC_DECIMALS,
WNEON_ADDRESS,
WNEON_DECIMALS,
WNEON_DECIMALS,
WROSE_ADDRESS,
WROSE_DECIMALS,
getDefaultNativeCurrencyAddressEvm,
} from "../utils/consts";
import { makeNearAccount } from "../utils/near";
import {
ExtractedMintInfo,
extractMintInfo,
@ -811,6 +816,44 @@ const getAlgorandParsedTokenAccounts = async (
}
};
const getNearParsedTokenAccounts = async (
walletAddress: string,
dispatch: Dispatch,
nft: boolean
) => {
dispatch(
nft ? fetchSourceParsedTokenAccountsNFT() : fetchSourceParsedTokenAccounts()
);
try {
if (nft) {
dispatch(receiveSourceParsedTokenAccountsNFT([]));
return;
}
const account = await makeNearAccount(walletAddress);
const balance = await account.getAccountBalance();
const nativeNear = createParsedTokenAccount(
walletAddress, //publicKey
NATIVE_NEAR_PLACEHOLDER, //the app doesn't like when this isn't truthy
balance.available, //amount
NATIVE_NEAR_DECIMALS,
parseFloat(formatUnits(balance.available, NATIVE_NEAR_DECIMALS)),
formatUnits(balance.available, NATIVE_NEAR_DECIMALS).toString(),
"NEAR",
"Near",
undefined, //TODO logo
true
);
dispatch(receiveSourceParsedTokenAccounts([nativeNear]));
} catch (e) {
console.error(e);
dispatch(
nft
? errorSourceParsedTokenAccountsNFT("Failed to load NFT metadata")
: errorSourceParsedTokenAccounts("Failed to load token metadata.")
);
}
};
/**
* Fetches the balance of an asset for the connected wallet
* This should handle every type of chain in the future, but only reads the Transfer state.
@ -831,6 +874,7 @@ function useGetAvailableTokens(nft: boolean = false) {
const solPK = solanaWallet?.publicKey;
const { provider, signerAddress } = useEthereumProvider();
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId } = useNearContext();
const [covalent, setCovalent] = useState<any>(undefined);
const [covalentLoading, setCovalentLoading] = useState(false);
@ -862,6 +906,8 @@ function useGetAvailableTokens(nft: boolean = false) {
? solPK?.toString()
: lookupChain === CHAIN_ID_ALGORAND
? algoAccounts[0]?.address
: lookupChain === CHAIN_ID_NEAR
? nearAccountId || undefined
: undefined;
const resetSourceAccounts = useCallback(() => {
@ -1376,7 +1422,7 @@ function useGetAvailableTokens(nft: boolean = false) {
cancelled = true;
};
}, [lookupChain, provider, signerAddress, nft, ethNativeAccount]);
useEffect(() => {
let cancelled = false;
if (
@ -1513,6 +1559,18 @@ function useGetAvailableTokens(nft: boolean = false) {
return () => {};
}, [dispatch, lookupChain, currentSourceWalletAddress, tokenAccounts, nft]);
//Near accounts load
useEffect(() => {
if (lookupChain === CHAIN_ID_NEAR && currentSourceWalletAddress) {
if (
!(tokenAccounts.data || tokenAccounts.isFetching || tokenAccounts.error)
) {
getNearParsedTokenAccounts(currentSourceWalletAddress, dispatch, nft);
}
}
return () => {};
}, [dispatch, lookupChain, currentSourceWalletAddress, tokenAccounts, nft]);
const ethAccounts = useMemo(() => {
const output = { ...tokenAccounts };
@ -1555,6 +1613,11 @@ function useGetAvailableTokens(nft: boolean = false) {
resetAccounts: resetSourceAccounts,
}
: lookupChain === CHAIN_ID_ALGORAND
? {
tokenAccounts,
resetAccounts: resetSourceAccounts,
}
: lookupChain === CHAIN_ID_NEAR
? {
tokenAccounts,
resetAccounts: resetSourceAccounts,

View File

@ -1,5 +1,6 @@
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
isEVMChain,
isNativeDenom,
@ -25,11 +26,16 @@ import {
getEvmChainId,
SOLANA_HOST,
getTerraConfig,
NATIVE_NEAR_PLACEHOLDER,
NATIVE_NEAR_DECIMALS,
} from "../utils/consts";
import { NATIVE_TERRA_DECIMALS } from "../utils/terra";
import { createParsedTokenAccount } from "./useGetSourceParsedTokenAccounts";
import useMetadata from "./useMetadata";
import { Algodv2 } from "algosdk";
import { useNearContext } from "../contexts/NearWalletContext";
import { makeNearAccount } from "../utils/near";
import { fetchSingleMetadata } from "./useNearMetadata";
function useGetTargetParsedTokenAccounts() {
const dispatch = useDispatch();
@ -58,6 +64,7 @@ function useGetTargetParsedTokenAccounts() {
} = useEthereumProvider();
const hasCorrectEvmNetwork = evmChainId === getEvmChainId(targetChain);
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId } = useNearContext();
const hasResolvedMetadata = metadata.data || metadata.error;
useEffect(() => {
// targetParsedTokenAccount is cleared on setTargetAsset, but we need to clear it on wallet changes too
@ -270,6 +277,93 @@ function useGetTargetParsedTokenAccounts() {
}
}
}
if (targetChain === CHAIN_ID_NEAR && nearAccountId) {
try {
makeNearAccount(nearAccountId)
.then((account) => {
if (targetAsset === NATIVE_NEAR_PLACEHOLDER) {
account
.getAccountBalance()
.then((balance) => {
if (!cancelled) {
dispatch(
setTargetParsedTokenAccount(
createParsedTokenAccount(
nearAccountId, //publicKey
NATIVE_NEAR_PLACEHOLDER, //the app doesn't like when this isn't truthy
balance.available, //amount
NATIVE_NEAR_DECIMALS,
parseFloat(
formatUnits(balance.available, NATIVE_NEAR_DECIMALS)
),
formatUnits(
balance.available,
NATIVE_NEAR_DECIMALS
).toString(),
"NEAR",
"Near",
undefined, //TODO logo
true
)
)
);
}
})
.catch(() => {
if (!cancelled) {
// TODO: error state
}
});
} else {
fetchSingleMetadata(targetAsset, account)
.then(({ decimals }) => {
account
.viewFunction(targetAsset, "ft_balance_of", {
account_id: nearAccountId,
})
.then((balance) => {
if (!cancelled) {
dispatch(
setTargetParsedTokenAccount(
createParsedTokenAccount(
nearAccountId,
targetAsset,
balance.toString(),
decimals,
Number(formatUnits(balance, decimals)),
formatUnits(balance, decimals),
symbol,
tokenName,
logo
)
)
);
}
})
.catch(() => {
if (!cancelled) {
// TODO: error state
}
});
})
.catch(() => {
if (!cancelled) {
// TODO: error state
}
});
}
})
.catch(() => {
if (!cancelled) {
// TODO: error state
}
});
} catch (e) {
if (!cancelled) {
// TODO: error state
}
}
}
return () => {
cancelled = true;
};
@ -289,6 +383,7 @@ function useGetTargetParsedTokenAccounts() {
logo,
algoAccounts,
decimals,
nearAccountId,
]);
}

View File

@ -6,6 +6,7 @@ import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_KLAYTN,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
getEmitterAddressAlgorand,
getEmitterAddressEth,
@ -22,6 +23,7 @@ import {
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import { Alert } from "@material-ui/lab";
import { Wallet } from "@near-wallet-selector/core";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection, PublicKey } from "@solana/web3.js";
import {
@ -35,6 +37,7 @@ import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useAlgorandContext } from "../contexts/AlgorandWalletContext";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
setAttestTx,
@ -56,11 +59,23 @@ import {
ALGORAND_TOKEN_BRIDGE_ID,
getBridgeAddressForChain,
getTokenBridgeAddressForChain,
NATIVE_NEAR_PLACEHOLDER,
NEAR_CORE_BRIDGE_ACCOUNT,
NEAR_TOKEN_BRIDGE_ACCOUNT,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
WORMHOLE_RPC_HOSTS,
} from "../utils/consts";
import {
attestNearFromNear,
attestTokenFromNear,
// attestTokenFromNear,
getEmitterAddressNear,
makeNearAccount,
parseSequenceFromLogNear,
signAndSendTransactions,
} from "../utils/near";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import { postWithFees, waitForTerraExecution } from "../utils/terra";
@ -175,6 +190,63 @@ async function evm(
}
}
async function near(
dispatch: any,
enqueueSnackbar: any,
senderAddr: string,
sourceAsset: string,
wallet: Wallet
) {
dispatch(setIsSending(true));
try {
const account = await makeNearAccount(senderAddr);
const msgs =
sourceAsset === NATIVE_NEAR_PLACEHOLDER
? await attestNearFromNear(
account,
NEAR_CORE_BRIDGE_ACCOUNT,
NEAR_TOKEN_BRIDGE_ACCOUNT
)
: await attestTokenFromNear(
account,
NEAR_CORE_BRIDGE_ACCOUNT,
NEAR_TOKEN_BRIDGE_ACCOUNT,
sourceAsset
);
const receipt = await signAndSendTransactions(account, wallet, msgs);
const sequence = parseSequenceFromLogNear(receipt);
dispatch(
setAttestTx({
id: receipt.transaction_outcome.id,
block: 0,
})
);
enqueueSnackbar(null, {
content: <Alert severity="success">Transaction confirmed</Alert>,
});
const emitterAddress = getEmitterAddressNear(NEAR_TOKEN_BRIDGE_ACCOUNT);
enqueueSnackbar(null, {
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_NEAR,
emitterAddress,
sequence
);
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
enqueueSnackbar(null, {
content: <Alert severity="success">Fetched Signed VAA</Alert>,
});
} catch (e) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
dispatch(setIsSending(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
@ -297,6 +369,7 @@ export function useHandleAttest() {
const terraWallet = useConnectedWallet();
const terraFeeDenom = useSelector(selectTerraFeeDenom);
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId, wallet } = useNearContext();
const disabled = !isTargetComplete || isSending || isSendComplete;
const handleAttestClick = useCallback(() => {
if (isEVMChain(sourceChain) && !!signer) {
@ -314,6 +387,8 @@ export function useHandleAttest() {
);
} else if (sourceChain === CHAIN_ID_ALGORAND && algoAccounts[0]) {
algo(dispatch, enqueueSnackbar, algoAccounts[0].address, sourceAsset);
} else if (sourceChain === CHAIN_ID_NEAR && nearAccountId && wallet) {
near(dispatch, enqueueSnackbar, nearAccountId, sourceAsset, wallet);
} else {
}
}, [
@ -327,6 +402,8 @@ export function useHandleAttest() {
sourceAsset,
terraFeeDenom,
algoAccounts,
nearAccountId,
wallet,
]);
return useMemo(
() => ({

View File

@ -4,6 +4,7 @@ import {
CHAIN_ID_ALGORAND,
CHAIN_ID_KARURA,
CHAIN_ID_KLAYTN,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
createWrappedOnAlgorand,
createWrappedOnEth,
@ -17,6 +18,7 @@ import {
updateWrappedOnTerra,
} from "@certusone/wormhole-sdk";
import { Alert } from "@material-ui/lab";
import { Wallet } from "@near-wallet-selector/core";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import {
@ -30,6 +32,7 @@ import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useAlgorandContext } from "../contexts/AlgorandWalletContext";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { setCreateTx, setIsCreating } from "../store/attestSlice";
import {
@ -46,11 +49,17 @@ import {
getTokenBridgeAddressForChain,
KARURA_HOST,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
NEAR_TOKEN_BRIDGE_ACCOUNT,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import { getKaruraGasParams } from "../utils/karura";
import {
createWrappedOnNear,
makeNearAccount,
signAndSendTransactions,
} from "../utils/near";
import parseError from "../utils/parseError";
import { postVaaWithRetry } from "../utils/postVaa";
import { signSendAndConfirm } from "../utils/solana";
@ -142,6 +151,39 @@ async function evm(
}
}
async function near(
dispatch: any,
enqueueSnackbar: any,
senderAddr: string,
signedVAA: Uint8Array,
wallet: Wallet
) {
dispatch(setIsCreating(true));
try {
const account = await makeNearAccount(senderAddr);
const msgs = await createWrappedOnNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
signedVAA
);
const receipt = await signAndSendTransactions(account, wallet, msgs);
dispatch(
setCreateTx({
id: receipt.transaction_outcome.id,
block: 0,
})
);
enqueueSnackbar(null, {
content: <Alert severity="success">Transaction confirmed</Alert>,
});
} catch (e) {
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
dispatch(setIsCreating(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
@ -249,6 +291,7 @@ export function useHandleCreateWrapped(shouldUpdate: boolean) {
const terraWallet = useConnectedWallet();
const terraFeeDenom = useSelector(selectTerraFeeDenom);
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId, wallet } = useNearContext();
const handleCreateClick = useCallback(() => {
if (isEVMChain(targetChain) && !!signer && !!signedVAA) {
evm(
@ -289,6 +332,13 @@ export function useHandleCreateWrapped(shouldUpdate: boolean) {
!!signedVAA
) {
algo(dispatch, enqueueSnackbar, algoAccounts[0]?.address, signedVAA);
} else if (
targetChain === CHAIN_ID_NEAR &&
nearAccountId &&
wallet &&
!!signedVAA
) {
near(dispatch, enqueueSnackbar, nearAccountId, signedVAA, wallet);
} else {
// enqueueSnackbar(
// "Creating wrapped tokens on this chain is not yet supported",
@ -309,6 +359,8 @@ export function useHandleCreateWrapped(shouldUpdate: boolean) {
shouldUpdate,
terraFeeDenom,
algoAccounts,
nearAccountId,
wallet,
]);
return useMemo(
() => ({

View File

@ -2,6 +2,7 @@ import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_KLAYTN,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
isEVMChain,
isTerraChain,
@ -15,6 +16,7 @@ import {
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import { Alert } from "@material-ui/lab";
import { Wallet } from "@near-wallet-selector/core";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import {
@ -29,6 +31,7 @@ import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useAlgorandContext } from "../contexts/AlgorandWalletContext";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
selectTerraFeeDenom,
@ -44,10 +47,16 @@ import {
ALGORAND_TOKEN_BRIDGE_ID,
getTokenBridgeAddressForChain,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
NEAR_TOKEN_BRIDGE_ACCOUNT,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import {
makeNearAccount,
redeemOnNear,
signAndSendTransactions,
} from "../utils/near";
import parseError from "../utils/parseError";
import { postVaaWithRetry } from "../utils/postVaa";
import { signSendAndConfirm } from "../utils/solana";
@ -135,6 +144,39 @@ async function evm(
}
}
async function near(
dispatch: any,
enqueueSnackbar: any,
senderAddr: string,
signedVAA: Uint8Array,
wallet: Wallet
) {
dispatch(setIsRedeeming(true));
try {
const account = await makeNearAccount(senderAddr);
const msgs = await redeemOnNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
signedVAA
);
const receipt = await signAndSendTransactions(account, wallet, msgs);
dispatch(
setRedeemTx({
id: receipt.transaction_outcome.id,
block: 0,
})
);
enqueueSnackbar(null, {
content: <Alert severity="success">Transaction confirmed</Alert>,
});
} catch (e) {
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
dispatch(setIsRedeeming(false));
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
@ -233,6 +275,7 @@ export function useHandleRedeem() {
const terraWallet = useConnectedWallet();
const terraFeeDenom = useSelector(selectTerraFeeDenom);
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId, wallet } = useNearContext();
const signedVAA = useTransferSignedVAA();
const isRedeeming = useSelector(selectTransferIsRedeeming);
const handleRedeemClick = useCallback(() => {
@ -267,6 +310,13 @@ export function useHandleRedeem() {
!!signedVAA
) {
algo(dispatch, enqueueSnackbar, algoAccounts[0]?.address, signedVAA);
} else if (
targetChain === CHAIN_ID_NEAR &&
nearAccountId &&
wallet &&
!!signedVAA
) {
near(dispatch, enqueueSnackbar, nearAccountId, signedVAA, wallet);
} else {
}
}, [
@ -280,6 +330,8 @@ export function useHandleRedeem() {
terraWallet,
terraFeeDenom,
algoAccounts,
nearAccountId,
wallet,
]);
const handleRedeemNativeClick = useCallback(() => {

View File

@ -23,7 +23,9 @@ import {
transferNativeSol,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import { CHAIN_ID_NEAR } from "@certusone/wormhole-sdk/lib/esm";
import { Alert } from "@material-ui/lab";
import { Wallet } from "@near-wallet-selector/core";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection } from "@solana/web3.js";
import {
@ -38,6 +40,7 @@ import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useAlgorandContext } from "../contexts/AlgorandWalletContext";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
selectTerraFeeDenom,
@ -54,8 +57,8 @@ import {
selectTransferTargetChain,
} from "../store/selectors";
import {
setIsVAAPending,
setIsSending,
setIsVAAPending,
setSignedVAAHex,
setTransferTx,
} from "../store/transferSlice";
@ -66,11 +69,22 @@ import {
ALGORAND_TOKEN_BRIDGE_ID,
getBridgeAddressForChain,
getTokenBridgeAddressForChain,
NATIVE_NEAR_PLACEHOLDER,
NEAR_CORE_BRIDGE_ACCOUNT,
NEAR_TOKEN_BRIDGE_ACCOUNT,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import {
getEmitterAddressNear,
makeNearAccount,
parseSequenceFromLogNear,
signAndSendTransactions,
transferNearFromNear,
transferTokenFromNear,
} from "../utils/near";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import { postWithFees, waitForTerraExecution } from "../utils/terra";
@ -249,6 +263,70 @@ async function evm(
}
}
async function near(
dispatch: any,
enqueueSnackbar: any,
wallet: Wallet,
senderAddr: string,
tokenAddress: string,
decimals: number,
amount: string,
recipientChain: ChainId,
recipientAddress: Uint8Array,
chainId: ChainId,
relayerFee?: string
) {
dispatch(setIsSending(true));
try {
const baseAmountParsed = parseUnits(amount, decimals);
const feeParsed = parseUnits(relayerFee || "0", decimals);
const transferAmountParsed = baseAmountParsed.add(feeParsed);
const account = await makeNearAccount(senderAddr);
const msgs =
tokenAddress === NATIVE_NEAR_PLACEHOLDER
? await transferNearFromNear(
account,
NEAR_CORE_BRIDGE_ACCOUNT,
NEAR_TOKEN_BRIDGE_ACCOUNT,
transferAmountParsed.toBigInt(),
recipientAddress,
recipientChain,
feeParsed.toBigInt()
)
: await transferTokenFromNear(
account,
NEAR_CORE_BRIDGE_ACCOUNT,
NEAR_TOKEN_BRIDGE_ACCOUNT,
tokenAddress,
transferAmountParsed.toBigInt(),
recipientAddress,
recipientChain,
feeParsed.toBigInt()
);
const receipt = await signAndSendTransactions(account, wallet, msgs);
const sequence = parseSequenceFromLogNear(receipt);
dispatch(
setTransferTx({
id: receipt.transaction_outcome.id,
block: 0,
})
);
enqueueSnackbar(null, {
content: <Alert severity="success">Transaction confirmed</Alert>,
});
const emitterAddress = getEmitterAddressNear(NEAR_TOKEN_BRIDGE_ACCOUNT);
await fetchSignedVAA(
chainId,
emitterAddress,
sequence,
enqueueSnackbar,
dispatch
);
} catch (e) {
handleError(e, enqueueSnackbar, dispatch);
}
}
async function solana(
dispatch: any,
enqueueSnackbar: any,
@ -404,6 +482,7 @@ export function useHandleTransfer() {
const terraWallet = useConnectedWallet();
const terraFeeDenom = useSelector(selectTerraFeeDenom);
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId, wallet } = useNearContext();
const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
@ -501,6 +580,27 @@ export function useHandleTransfer() {
sourceChain,
relayerFee
);
} else if (
sourceChain === CHAIN_ID_NEAR &&
nearAccountId &&
wallet &&
!!sourceAsset &&
decimals !== undefined &&
!!targetAddress
) {
near(
dispatch,
enqueueSnackbar,
wallet,
nearAccountId,
sourceAsset,
decimals,
amount,
targetChain,
targetAddress,
sourceChain,
relayerFee
);
} else {
}
}, [
@ -523,6 +623,8 @@ export function useHandleTransfer() {
isNative,
terraFeeDenom,
algoAccounts,
nearAccountId,
wallet,
]);
return useMemo(
() => ({

View File

@ -1,6 +1,7 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
isEVMChain,
isTerraChain,
@ -13,6 +14,7 @@ import {
ConnectType,
useEthereumProvider,
} from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { CLUSTER, getEvmChainId } from "../utils/consts";
import {
@ -58,6 +60,7 @@ function useIsWalletReady(
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
const { accounts: algorandAccounts } = useAlgorandContext();
const algoPK = algorandAccounts[0]?.address;
const { accountId: nearPK } = useNearContext();
const forceNetworkSwitch = useCallback(async () => {
if (provider && correctEvmNetwork) {
@ -118,6 +121,9 @@ function useIsWalletReady(
if (chainId === CHAIN_ID_ALGORAND && algoPK) {
return createWalletStatus(true, undefined, forceNetworkSwitch, algoPK);
}
if (chainId === CHAIN_ID_NEAR && nearPK) {
return createWalletStatus(true, undefined, forceNetworkSwitch, nearPK);
}
if (isEVMChain(chainId) && hasEthInfo && signerAddress) {
if (hasCorrectEvmNetwork) {
return createWalletStatus(
@ -158,6 +164,7 @@ function useIsWalletReady(
signerAddress,
terraWallet,
algoPK,
nearPK,
]);
}

View File

@ -1,6 +1,7 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
isEVMChain,
@ -15,6 +16,7 @@ import { Metadata } from "../utils/metaplex";
import useAlgoMetadata, { AlgoMetadata } from "./useAlgoMetadata";
import useEvmMetadata, { EvmMetadata } from "./useEvmMetadata";
import useMetaplexData from "./useMetaplexData";
import useNearMetadata from "./useNearMetadata";
import useSolanaTokenMap from "./useSolanaTokenMap";
import useTerraMetadata, { TerraMetadata } from "./useTerraMetadata";
import useTerraTokenMap, { TerraTokenMap } from "./useTerraTokenMap";
@ -165,6 +167,9 @@ export default function useMetadata(
const algoAddresses = useMemo(() => {
return chainId === CHAIN_ID_ALGORAND ? addresses : [];
}, [chainId, addresses]);
const nearAddresses = useMemo(() => {
return chainId === CHAIN_ID_NEAR ? addresses : [];
}, [chainId, addresses]);
const metaplexData = useMetaplexData(solanaAddresses);
const terraMetadata = useTerraMetadata(
@ -173,6 +178,7 @@ export default function useMetadata(
);
const ethMetadata = useEvmMetadata(ethereumAddresses, chainId);
const algoMetadata = useAlgoMetadata(algoAddresses);
const nearMetadata = useNearMetadata(nearAddresses);
const output: DataWrapper<Map<string, GenericMetadata>> = useMemo(
() =>
@ -189,6 +195,8 @@ export default function useMetadata(
)
: chainId === CHAIN_ID_ALGORAND
? constructAlgoMetadata(algoAddresses, algoMetadata)
: chainId === CHAIN_ID_NEAR
? constructAlgoMetadata(nearAddresses, nearMetadata)
: getEmptyDataWrapper(),
[
chainId,
@ -202,6 +210,8 @@ export default function useMetadata(
terraTokenMap,
algoAddresses,
algoMetadata,
nearAddresses,
nearMetadata,
]
);

View File

@ -0,0 +1,83 @@
import { Account } from "near-api-js";
import { useEffect, useMemo, useState } from "react";
import { useNearContext } from "../contexts/NearWalletContext";
import { DataWrapper } from "../store/helpers";
import { makeNearAccount } from "../utils/near";
import { AlgoMetadata } from "./useAlgoMetadata";
export const fetchSingleMetadata = async (
address: string,
account: Account
): Promise<AlgoMetadata> => {
const assetInfo = await account.viewFunction(address, "ft_metadata");
return {
tokenName: assetInfo.name,
symbol: assetInfo.symbol,
decimals: assetInfo.decimals,
};
};
const fetchNearMetadata = async (
addresses: string[],
nearAccountId: string
) => {
const account = await makeNearAccount(nearAccountId);
const promises: Promise<AlgoMetadata>[] = [];
addresses.forEach((address) => {
promises.push(fetchSingleMetadata(address, account));
});
const resultsArray = await Promise.all(promises);
const output = new Map<string, AlgoMetadata>();
addresses.forEach((address, index) => {
output.set(address, resultsArray[index]);
});
return output;
};
function useNearMetadata(
addresses: string[]
): DataWrapper<Map<string, AlgoMetadata>> {
const { accountId: nearAccountId } = useNearContext();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState("");
const [data, setData] = useState<Map<string, AlgoMetadata> | null>(null);
useEffect(() => {
let cancelled = false;
if (addresses.length && nearAccountId) {
setIsFetching(true);
setError("");
setData(null);
fetchNearMetadata(addresses, nearAccountId).then(
(results) => {
if (!cancelled) {
setData(results);
setIsFetching(false);
}
},
() => {
if (!cancelled) {
setError("Could not retrieve contract metadata");
setIsFetching(false);
}
}
);
}
return () => {
cancelled = true;
};
}, [addresses, nearAccountId]);
return useMemo(
() => ({
data,
isFetching,
error,
receivedAt: null,
}),
[data, isFetching, error]
);
}
export default useNearMetadata;

View File

@ -1,6 +1,7 @@
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA2,
getOriginalAssetAlgorand,
@ -28,6 +29,7 @@ import {
Provider,
useEthereumProvider,
} from "../contexts/EthereumProviderContext";
import { useNearContext } from "../contexts/NearWalletContext";
import { DataWrapper } from "../store/helpers";
import {
ALGORAND_HOST,
@ -35,11 +37,19 @@ import {
getNFTBridgeAddressForChain,
getTerraConfig,
getTokenBridgeAddressForChain,
NATIVE_NEAR_PLACEHOLDER,
NATIVE_NEAR_WH_ADDRESS,
NEAR_TOKEN_BRIDGE_ACCOUNT,
SOLANA_HOST,
SOLANA_SYSTEM_PROGRAM_ADDRESS,
SOL_NFT_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "../utils/consts";
import {
getOriginalAssetNear,
lookupHash,
makeNearAccount,
} from "../utils/near";
import { queryExternalId } from "../utils/terra";
import useIsWalletReady from "./useIsWalletReady";
@ -52,7 +62,8 @@ export type OriginalAssetInfo = {
export async function getOriginalAssetToken(
foreignChain: ChainId,
foreignNativeStringAddress: string,
provider?: Web3Provider
provider?: Web3Provider,
nearAccountId?: string | null
) {
let promise = null;
try {
@ -88,6 +99,13 @@ export async function getOriginalAssetToken(
ALGORAND_TOKEN_BRIDGE_ID,
BigInt(foreignNativeStringAddress)
);
} else if (foreignChain === CHAIN_ID_NEAR && nearAccountId) {
const account = await makeNearAccount(nearAccountId);
promise = await getOriginalAssetNear(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
foreignNativeStringAddress
);
}
} catch (e) {
promise = Promise.reject("Invalid foreign arguments.");
@ -137,7 +155,8 @@ export async function getOriginalAsset(
foreignNativeStringAddress: string,
nft: boolean,
tokenId?: string,
provider?: Provider
provider?: Provider,
nearAccountId?: string | null
): Promise<WormholeWrappedNFTInfo> {
const result = nft
? await getOriginalAssetNFT(
@ -149,7 +168,8 @@ export async function getOriginalAsset(
: await getOriginalAssetToken(
foreignChain,
foreignNativeStringAddress,
provider
provider,
nearAccountId
);
if (
@ -178,6 +198,7 @@ function useOriginalAsset(
tokenId?: string
): DataWrapper<OriginalAssetInfo> {
const { provider } = useEthereumProvider();
const { accountId: nearAccountId } = useNearContext();
const { isReady } = useIsWalletReady(foreignChain, false);
const [originAddress, setOriginAddress] = useState<string | null>(null);
const [originTokenId, setOriginTokenId] = useState<string | null>(null);
@ -222,10 +243,26 @@ function useOriginalAsset(
if (argumentError) {
return;
}
// short circuit for near native
if (
foreignChain === CHAIN_ID_NEAR &&
foreignAddress === NATIVE_NEAR_PLACEHOLDER
) {
setOriginChain(CHAIN_ID_NEAR);
setOriginAddress(NATIVE_NEAR_PLACEHOLDER);
return;
}
let cancelled = false;
setIsLoading(true);
getOriginalAsset(foreignChain, foreignAddress, nft, tokenId, provider)
getOriginalAsset(
foreignChain,
foreignAddress,
nft,
tokenId,
provider,
nearAccountId
)
.then((result) => {
if (!cancelled) {
setIsLoading(false);
@ -234,6 +271,24 @@ function useOriginalAsset(
queryExternalId(uint8ArrayToHex(result.assetAddress)).then(
(tokenId) => setOriginAddress(tokenId || null)
);
} else if (result.chainId === CHAIN_ID_NEAR) {
if (
uint8ArrayToHex(result.assetAddress) === NATIVE_NEAR_WH_ADDRESS
) {
setOriginAddress(NATIVE_NEAR_PLACEHOLDER);
} else if (nearAccountId) {
makeNearAccount(nearAccountId).then((account) => {
lookupHash(
account,
NEAR_TOKEN_BRIDGE_ACCOUNT,
uint8ArrayToHex(result.assetAddress)
).then((tokenAccount) => {
if (!cancelled) {
setOriginAddress(tokenAccount[1] || null);
}
});
});
}
} else {
setOriginAddress(
hexToNativeAssetString(
@ -261,6 +316,7 @@ function useOriginalAsset(
argumentError,
tokenId,
argsEqual,
nearAccountId,
]);
const output: DataWrapper<OriginalAssetInfo> = useMemo(

View File

@ -1,6 +1,7 @@
import {
canonicalAddress,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
isEVMChain,
isTerraChain,
@ -29,6 +30,11 @@ import {
} from "../store/selectors";
import { setTargetAddressHex as setTransferTargetAddressHex } from "../store/transferSlice";
import { decodeAddress } from "algosdk";
import { useNearContext } from "../contexts/NearWalletContext";
import { makeNearAccount, signAndSendTransactions } from "../utils/near";
import { NEAR_TOKEN_BRIDGE_ACCOUNT } from "../utils/consts";
import { getTransactionLastResult } from "near-api-js/lib/providers";
import BN from "bn.js";
function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
const dispatch = useDispatch();
@ -47,6 +53,7 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
const terraWallet = useConnectedWallet();
const { accounts: algoAccounts } = useAlgorandContext();
const { accountId: nearAccountId, wallet } = useNearContext();
const setTargetAddressHex = nft
? setNFTTargetAddressHex
: setTransferTargetAddressHex;
@ -116,6 +123,55 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
uint8ArrayToHex(decodeAddress(algoAccounts[0].address).publicKey)
)
);
} else if (targetChain === CHAIN_ID_NEAR && nearAccountId && wallet) {
(async () => {
try {
const account = await makeNearAccount(nearAccountId);
// So, near can have account names up to 64 bytes but wormhole can only have 32...
// as a result, we have to hash our account names to sha256's.. What we are doing
// here is doing a RPC call (does not require any interaction with the wallet and is free)
// that both tells us our account hash AND if we are already registered...
let account_hash = await account.viewFunction(
NEAR_TOKEN_BRIDGE_ACCOUNT,
"hash_account",
{
account: nearAccountId,
}
);
if (!cancelled) {
let myAddress = account_hash[1];
console.log("account hash for", nearAccountId, account_hash);
if (!account_hash[0]) {
console.log("Registering the receiving account");
let myAddress2 = getTransactionLastResult(
await signAndSendTransactions(account, wallet, [
{
contractId: NEAR_TOKEN_BRIDGE_ACCOUNT,
methodName: "register_account",
args: { account: nearAccountId },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
},
])
);
console.log("account hash returned: " + myAddress2);
} else {
console.log("account already registered");
}
if (!cancelled) {
dispatch(setTargetAddressHex(myAddress));
}
}
} catch (e) {
console.log(e);
if (!cancelled) {
dispatch(setTargetAddressHex(undefined));
}
}
})();
} else {
dispatch(setTargetAddressHex(undefined));
}
@ -135,6 +191,8 @@ function useSyncTargetAddress(shouldFire: boolean, nft?: boolean) {
nft,
setTargetAddressHex,
algoAccounts,
nearAccountId,
wallet,
]);
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 288"><g id="Layer_1" data-name="Layer 1"><path d="M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z"/></g></svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@ -9,6 +9,7 @@ import BackgroundImage from "./components/BackgroundImage";
import { AlgorandContextProvider } from "./contexts/AlgorandWalletContext";
import { BetaContextProvider } from "./contexts/BetaContext";
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
import { NearContextProvider } from "./contexts/NearWalletContext";
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext.tsx";
import { TerraWalletProvider } from "./contexts/TerraWalletContext.tsx";
import ErrorBoundary from "./ErrorBoundary";
@ -27,10 +28,12 @@ ReactDOM.render(
<EthereumProviderProvider>
<TerraWalletProvider>
<AlgorandContextProvider>
<HashRouter>
<BackgroundImage />
<App />
</HashRouter>
<NearContextProvider>
<HashRouter>
<BackgroundImage />
<App />
</HashRouter>
</NearContextProvider>
</AlgorandContextProvider>
</TerraWalletProvider>
</EthereumProviderProvider>

View File

@ -11,6 +11,7 @@ import {
CHAIN_ID_FANTOM,
CHAIN_ID_KARURA,
CHAIN_ID_KLAYTN,
CHAIN_ID_NEAR,
CHAIN_ID_NEON,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
@ -41,6 +42,8 @@ import polygonIcon from "../icons/polygon.svg";
import solanaIcon from "../icons/solana.svg";
import terraIcon from "../icons/terra.svg";
import terra2Icon from "../icons/terra2.svg";
import nearIcon from "../icons/near.svg";
import { ConnectConfig, keyStores } from "near-api-js";
export type Cluster = "devnet" | "testnet" | "mainnet";
export const CLUSTER: Cluster =
@ -107,6 +110,11 @@ export const CHAINS: ChainInfo[] =
name: "Klaytn",
logo: klaytnIcon,
},
{
id: CHAIN_ID_NEAR,
name: "Near",
logo: nearIcon,
},
{
id: CHAIN_ID_OASIS,
name: "Oasis",
@ -190,6 +198,11 @@ export const CHAINS: ChainInfo[] =
name: "Klaytn",
logo: klaytnIcon,
},
{
id: CHAIN_ID_NEAR,
name: "Near",
logo: nearIcon,
},
{
id: CHAIN_ID_NEON,
name: "Neon",
@ -237,6 +250,11 @@ export const CHAINS: ChainInfo[] =
name: "Ethereum",
logo: ethIcon,
},
{
id: CHAIN_ID_NEAR,
name: "Near",
logo: nearIcon,
},
{
id: CHAIN_ID_SOLANA,
name: "Solana",
@ -846,6 +864,20 @@ export const ALGORAND_TOKEN_BRIDGE_ID = BigInt(
export const ALGORAND_WAIT_FOR_CONFIRMATIONS =
CLUSTER === "mainnet" ? 4 : CLUSTER === "testnet" ? 4 : 1;
export const NEAR_CORE_BRIDGE_ACCOUNT =
CLUSTER === "mainnet"
? "contract.wormhole_crypto.near"
: CLUSTER === "testnet"
? "wormhole.wormhole.testnet"
: "wormhole.test.near";
export const NEAR_TOKEN_BRIDGE_ACCOUNT =
CLUSTER === "mainnet"
? "contract.portalbridge.near"
: CLUSTER === "testnet"
? "token.wormhole.testnet"
: "token.test.near";
export const getBridgeAddressForChain = (chainId: ChainId) =>
chainId === CHAIN_ID_SOLANA
? SOL_BRIDGE_ADDRESS
@ -1409,6 +1441,40 @@ export const getTerraFCDBaseUrl = (chainId: TerraChainId) =>
export const getTerraGasPricesUrl = (chainId: TerraChainId) =>
`${getTerraFCDBaseUrl(chainId)}/v1/txs/gas_prices`;
export const nearKeyStore = new keyStores.BrowserLocalStorageKeyStore();
export const getNearConnectionConfig = (): ConnectConfig =>
CLUSTER === "mainnet"
? {
networkId: "mainnet",
keyStore: nearKeyStore,
nodeUrl: "https://rpc.mainnet.near.org",
walletUrl: "https://wallet.mainnet.near.org",
helperUrl: "https://helper.mainnet.near.org",
headers: {},
}
: CLUSTER === "testnet"
? {
networkId: "testnet",
keyStore: nearKeyStore,
nodeUrl: "https://rpc.testnet.near.org",
walletUrl: "https://wallet.testnet.near.org",
helperUrl: "https://helper.testnet.near.org",
headers: {},
}
: {
networkId: "sandbox",
keyStore: nearKeyStore,
nodeUrl: "http://localhost:3030",
helperUrl: "",
headers: {},
};
export const NATIVE_NEAR_DECIMALS = 24;
export const NATIVE_NEAR_PLACEHOLDER = "near";
export const NATIVE_NEAR_WH_ADDRESS =
"0000000000000000000000000000000000000000000000000000000000000000";
export const TOTAL_TRANSACTIONS_WORMHOLE = `https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-totals?groupBy=address`;
export const RECENT_TRANSACTIONS_WORMHOLE = `https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-recent?groupBy=address&numRows=2`;

476
bridge_ui/src/utils/near.ts Normal file
View File

@ -0,0 +1,476 @@
import {
ChainId,
ChainName,
CHAIN_ID_NEAR,
coalesceChainId,
hexToUint8Array,
uint8ArrayToHex,
WormholeWrappedInfo,
} from "@certusone/wormhole-sdk";
import { _parseVAAAlgorand } from "@certusone/wormhole-sdk/lib/esm/algorand";
import { Wallet } from "@near-wallet-selector/core/lib/wallet";
import BN from "bn.js";
import { arrayify, sha256, zeroPad } from "ethers/lib/utils";
import { Account, connect } from "near-api-js";
import { FunctionCallOptions } from "near-api-js/lib/account";
import { FinalExecutionOutcome } from "near-api-js/lib/providers";
import { getNearConnectionConfig } from "./consts";
export const makeNearAccount = async (senderAddr: string) =>
await (await connect(getNearConnectionConfig())).account(senderAddr);
export const signAndSendTransactions = async (
account: Account,
wallet: Wallet,
messages: FunctionCallOptions[]
): Promise<FinalExecutionOutcome> => {
// the browser wallet's signAndSendTransactions call navigates away from the page which is incompatible with the current app design
if (wallet.type === "browser" && account) {
let lastReceipt: FinalExecutionOutcome | null = null;
for (const message of messages) {
lastReceipt = await account.functionCall(message);
}
if (!lastReceipt) {
throw new Error("An error occurred while fetching the transaction info");
}
return lastReceipt;
}
const receipts = await wallet.signAndSendTransactions({
transactions: messages.map((options) => ({
signerId: wallet.id,
receiverId: options.contractId,
actions: [
{
type: "FunctionCall",
params: {
methodName: options.methodName,
args: options.args,
gas: options.gas?.toString() || "0",
deposit: options.attachedDeposit?.toString() || "0",
},
},
],
})),
});
if (!receipts || receipts.length === 0) {
throw new Error("An error occurred while fetching the transaction info");
}
return receipts[receipts.length - 1];
};
export function getIsWrappedAssetNear(
tokenBridge: string,
asset: string
): boolean {
return asset.endsWith("." + tokenBridge);
}
export async function lookupHash(
account: Account,
tokenBridge: string,
hash: string
): Promise<[boolean, string]> {
return await account.viewFunction(tokenBridge, "hash_lookup", {
hash,
});
}
export async function getOriginalAssetNear(
client: Account,
tokenAccount: string,
assetAccount: string
): Promise<WormholeWrappedInfo> {
let retVal: WormholeWrappedInfo = {
isWrapped: false,
chainId: CHAIN_ID_NEAR,
assetAddress: new Uint8Array(),
};
retVal.isWrapped = await getIsWrappedAssetNear(tokenAccount, assetAccount);
if (!retVal.isWrapped) {
retVal.assetAddress = assetAccount
? arrayify(sha256(Buffer.from(assetAccount)))
: zeroPad(arrayify("0x"), 32);
return retVal;
}
let buf = await client.viewFunction(tokenAccount, "get_original_asset", {
token: assetAccount,
});
retVal.chainId = buf[1];
retVal.assetAddress = hexToUint8Array(buf[0]);
return retVal;
}
export function getEmitterAddressNear(programAddress: string): string {
return uint8ArrayToHex(arrayify(sha256(Buffer.from(programAddress, "utf8"))));
}
export function parseSequenceFromLogNear(
result: FinalExecutionOutcome
): string {
let sequence = "";
for (const o of result.receipts_outcome) {
for (const l of o.outcome.logs) {
if (l.startsWith("EVENT_JSON:")) {
const body = JSON.parse(l.slice(11));
if (body.standard === "wormhole" && body.event === "publish") {
return body.seq;
}
}
}
}
return sequence;
}
export async function getForeignAssetNear(
client: Account,
tokenAccount: string,
chain: ChainId | ChainName,
contract: string
): Promise<string | null> {
const chainId = coalesceChainId(chain);
let ret = await client.viewFunction(tokenAccount, "get_foreign_asset", {
chain: chainId,
address: contract,
});
if (ret === "") return null;
else return ret;
}
export async function getIsTransferCompletedNear(
client: Account,
tokenAccount: string,
signedVAA: Uint8Array
): Promise<boolean> {
// Could we just pass in the vaa already as hex?
let vaa = Buffer.from(signedVAA).toString("hex");
return (
await client.viewFunction(tokenAccount, "is_transfer_completed", {
vaa: vaa,
})
)[1];
}
export async function transferTokenFromNear(
client: Account,
coreBridge: string,
tokenBridge: string,
assetId: string,
qty: bigint,
receiver: Uint8Array,
chain: ChainId | ChainName,
fee: bigint,
payload: string = ""
): Promise<FunctionCallOptions[]> {
let isWrapped = getIsWrappedAssetNear(tokenBridge, assetId);
let message_fee = await client.viewFunction(coreBridge, "message_fee", {});
if (isWrapped) {
return [
{
contractId: tokenBridge,
methodName: "send_transfer_wormhole_token",
args: {
token: assetId,
amount: qty.toString(10),
receiver: uint8ArrayToHex(receiver),
chain: chain,
fee: fee.toString(10),
payload: payload,
message_fee: message_fee,
},
attachedDeposit: new BN(message_fee + 1),
gas: new BN("100000000000000"),
},
];
} else {
const msgs = [];
let bal = await client.viewFunction(assetId, "storage_balance_of", {
account_id: tokenBridge,
});
if (bal === null) {
// Looks like we have to stake some storage for this asset
// for the token bridge...
msgs.push({
contractId: assetId,
methodName: "storage_deposit",
args: { account_id: tokenBridge, registration_only: true },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
});
}
if (message_fee > 0) {
let bank = await client.viewFunction(tokenBridge, "bank_balance", {
acct: client.accountId,
});
if (!bank[0]) {
msgs.push({
contractId: tokenBridge,
methodName: "register_bank",
args: {},
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
});
}
if (bank[1] < message_fee) {
msgs.push({
contractId: tokenBridge,
methodName: "fill_bank",
args: {},
gas: new BN("100000000000000"),
attachedDeposit: new BN(message_fee),
});
}
}
msgs.push({
contractId: assetId,
methodName: "ft_transfer_call",
args: {
receiver_id: tokenBridge,
amount: qty.toString(10),
msg: JSON.stringify({
receiver: uint8ArrayToHex(receiver),
chain: chain,
fee: fee.toString(10),
payload: payload,
message_fee: message_fee,
}),
},
attachedDeposit: new BN(1),
gas: new BN("100000000000000"),
});
return msgs;
}
}
export async function transferNearFromNear(
client: Account,
coreBridge: string,
tokenBridge: string,
qty: bigint,
receiver: Uint8Array,
chain: ChainId | ChainName,
fee: bigint,
payload: string = ""
): Promise<FunctionCallOptions[]> {
let message_fee = await client.viewFunction(coreBridge, "message_fee", {});
return [
{
contractId: tokenBridge,
methodName: "send_transfer_near",
args: {
receiver: uint8ArrayToHex(receiver),
chain: chain,
fee: fee.toString(10),
payload: payload,
message_fee: message_fee,
},
attachedDeposit: new BN(qty.toString(10)).add(new BN(message_fee)),
gas: new BN("100000000000000"),
},
];
}
export async function redeemOnNear(
client: Account,
tokenBridge: string,
vaa: Uint8Array
): Promise<FunctionCallOptions[]> {
const msgs = [];
let p = _parseVAAAlgorand(vaa);
if (p.ToChain !== CHAIN_ID_NEAR) {
throw new Error("Not destined for NEAR");
}
let user = await client.viewFunction(tokenBridge, "hash_lookup", {
hash: uint8ArrayToHex(p.ToAddress as Uint8Array),
});
if (!user[0]) {
throw new Error(
"Unregistered receiver (receiving account is not registered)"
);
}
user = user[1];
let token = await getForeignAssetNear(
client,
tokenBridge,
p.FromChain as ChainId,
p.Contract as string
);
if (token === "") {
throw new Error("Unregistered token (this been attested yet?)");
}
if (
(p.Contract as string) !==
"0000000000000000000000000000000000000000000000000000000000000000"
) {
let bal = await client.viewFunction(token as string, "storage_balance_of", {
account_id: user,
});
if (bal === null) {
console.log("Registering ", user, " for ", token);
msgs.push({
contractId: token as string,
methodName: "storage_deposit",
args: { account_id: user, registration_only: true },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
});
}
if (
p.Fee !== undefined &&
Buffer.compare(
p.Fee,
Buffer.from(
"0000000000000000000000000000000000000000000000000000000000000000",
"hex"
)
) !== 0
) {
let bal = await client.viewFunction(
token as string,
"storage_balance_of",
{
account_id: client.accountId,
}
);
if (bal === null) {
console.log("Registering ", client.accountId, " for ", token);
msgs.push({
contractId: token as string,
methodName: "storage_deposit",
args: { account_id: client.accountId, registration_only: true },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
});
}
}
}
msgs.push({
contractId: tokenBridge,
methodName: "submit_vaa",
args: {
vaa: uint8ArrayToHex(vaa),
},
attachedDeposit: new BN("100000000000000000000000"),
gas: new BN("150000000000000"),
});
msgs.push({
contractId: tokenBridge,
methodName: "submit_vaa",
args: {
vaa: uint8ArrayToHex(vaa),
},
attachedDeposit: new BN("100000000000000000000000"),
gas: new BN("150000000000000"),
});
return msgs;
}
export async function createWrappedOnNear(
client: Account,
tokenBridge: string,
attestVAA: Uint8Array
): Promise<FunctionCallOptions[]> {
const msgs = [];
let vaa = Buffer.from(attestVAA).toString("hex");
let res = await client.viewFunction(tokenBridge, "deposit_estimates", {});
msgs.push({
contractId: tokenBridge,
methodName: "submit_vaa",
args: { vaa: vaa },
attachedDeposit: new BN(res[1]),
gas: new BN("150000000000000"),
});
msgs.push({
contractId: tokenBridge,
methodName: "submit_vaa",
args: { vaa: vaa },
attachedDeposit: new BN(res[1]),
gas: new BN("150000000000000"),
});
return msgs;
}
export async function attestTokenFromNear(
client: Account,
coreBridge: string,
tokenBridge: string,
asset: string
): Promise<FunctionCallOptions[]> {
const msgs = [];
let message_fee = await client.viewFunction(coreBridge, "message_fee", {});
// Non-signing event
if (!getIsWrappedAssetNear(tokenBridge, asset)) {
// Non-signing event that hits the RPC
let res = await client.viewFunction(tokenBridge, "hash_account", {
account: asset,
});
// if res[0] == false, the account has not been
// registered... The first user to attest a non-wormhole token
// is gonna have to pay for the space
if (!res[0]) {
// Signing event
msgs.push({
contractId: tokenBridge,
methodName: "register_account",
args: { account: asset },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
});
}
}
msgs.push({
contractId: tokenBridge,
methodName: "attest_token",
args: { token: asset, message_fee: message_fee },
attachedDeposit: new BN("3000000000000000000000").add(new BN(message_fee)), // 0.003 NEAR
gas: new BN("100000000000000"),
});
return msgs;
}
export async function attestNearFromNear(
client: Account,
coreBridge: string,
tokenBridge: string
): Promise<FunctionCallOptions[]> {
let message_fee =
(await client.viewFunction(coreBridge, "message_fee", {})) + 1;
return [
{
contractId: tokenBridge,
methodName: "attest_near",
args: { message_fee: message_fee },
attachedDeposit: new BN(message_fee),
gas: new BN("100000000000000"),
},
];
}