From 4f08f315f1bdd2350854f84d32295ee5dd071084 Mon Sep 17 00:00:00 2001
From: kev1n-peters <96065607+kev1n-peters@users.noreply.github.com>
Date: Mon, 15 Aug 2022 09:18:01 -0500
Subject: [PATCH] bridge_ui: Chain Governor support (#1421)
---
Tiltfile | 7 +
bridge_ui/package-lock.json | 48 +++---
bridge_ui/package.json | 2 +-
bridge_ui/src/components/Recovery.tsx | 100 ++++++------
.../components/Transfer/PendingVAAWarning.tsx | 36 +++++
bridge_ui/src/components/Transfer/Send.tsx | 22 ++-
bridge_ui/src/components/Transfer/Source.tsx | 7 +-
.../Transfer/TransferLimitedWarning.tsx | 48 ++++++
bridge_ui/src/hooks/useHandleAttest.tsx | 8 +-
bridge_ui/src/hooks/useHandleNFTTransfer.tsx | 5 +-
bridge_ui/src/hooks/useHandleTransfer.tsx | 118 +++++++-------
bridge_ui/src/hooks/useIsTransferLimited.ts | 152 ++++++++++++++++++
bridge_ui/src/store/selectors.ts | 2 +
bridge_ui/src/store/transferSlice.ts | 7 +
bridge_ui/src/utils/consts.ts | 6 +
bridge_ui/src/utils/getSignedVAAWithRetry.ts | 54 ++++---
16 files changed, 458 insertions(+), 164 deletions(-)
create mode 100644 bridge_ui/src/components/Transfer/PendingVAAWarning.tsx
create mode 100644 bridge_ui/src/components/Transfer/TransferLimitedWarning.tsx
create mode 100644 bridge_ui/src/hooks/useIsTransferLimited.ts
diff --git a/Tiltfile b/Tiltfile
index d711d2bdf..9a3a42856 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -51,6 +51,7 @@ config.define_bool("ci_tests", False, "Enable tests runner component")
config.define_bool("bridge_ui_hot", False, "Enable hot loading bridge_ui")
config.define_bool("guardiand_debug", False, "Enable dlv endpoint for guardiand")
config.define_bool("node_metrics", False, "Enable Prometheus & Grafana for Guardian metrics")
+config.define_bool("guardiand_governor", False, "Enable chain governor in guardiand")
cfg = config.parse()
num_guardians = int(cfg.get("num", "1"))
@@ -71,6 +72,7 @@ e2e = cfg.get("e2e", ci)
ci_tests = cfg.get("ci_tests", ci)
guardiand_debug = cfg.get("guardiand_debug", False)
node_metrics = cfg.get("node_metrics", False)
+guardiand_governor = cfg.get("guardiand_governor", False)
bridge_ui_hot = not ci
@@ -224,6 +226,11 @@ def build_node_yaml():
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
]
+ if guardiand_governor:
+ container["command"] += [
+ "--chainGovernorEnabled"
+ ]
+
return encode_yaml_stream(node_yaml)
k8s_yaml_with_ns(build_node_yaml())
diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json
index a2f951091..d51ba634b 100644
--- a/bridge_ui/package-lock.json
+++ b/bridge_ui/package-lock.json
@@ -8,7 +8,7 @@
"name": "test_ui",
"version": "0.1.0",
"dependencies": {
- "@certusone/wormhole-sdk": "^0.5.0",
+ "@certusone/wormhole-sdk": "^0.6.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
@@ -87,12 +87,12 @@
},
"../sdk/js": {
"name": "@certusone/wormhole-sdk",
- "version": "0.5.0",
+ "version": "0.6.1",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
- "@certusone/wormhole-sdk-proto-web": "^0.0.1",
- "@certusone/wormhole-sdk-wasm": "file:../js-wasm",
+ "@certusone/wormhole-sdk-proto-web": "^0.0.3",
+ "@certusone/wormhole-sdk-wasm": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^3.1.3",
@@ -2131,11 +2131,11 @@
}
},
"node_modules/@certusone/wormhole-sdk": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.5.0.tgz",
- "integrity": "sha512-Z8Cj2yZ41if842jSSLzKLomwkq9PgXdjVq3r3VNzkSM3aZavU8vZqNT33LU9IabmW7hiWe1uI9j2Z1JZe7SIEg==",
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.6.1.tgz",
+ "integrity": "sha512-S4zU62gIipNbEqXGl1SaHBNX7003T9WZU3K0/mr8agTPpqatPMWZArkjH9VqSVRRIEIkDB8zx5sQk9vCtrkTHQ==",
"dependencies": {
- "@certusone/wormhole-sdk-proto-web": "^0.0.1",
+ "@certusone/wormhole-sdk-proto-web": "^0.0.3",
"@certusone/wormhole-sdk-wasm": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
@@ -2147,9 +2147,9 @@
}
},
"node_modules/@certusone/wormhole-sdk-proto-web": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.1.tgz",
- "integrity": "sha512-v6D+vCPqzTmrRuN0ZHpOdA1XnF3nmaD1wlJf025SXb7JFhVSmKyFXzLajkt50rk6SCkEvXtRlxNTJtnuCxg94Q==",
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.3.tgz",
+ "integrity": "sha512-O8gx8dLTcgF5jbmWjRiyZAn1LozslhWqDo6Q6QJfRiL6DWySV5TOXqgaEfQ4UGEM4uqM76HWZpwfEWUjaRhJ/A==",
"dependencies": {
"@improbable-eng/grpc-web": "^0.15.0",
"protobufjs": "^7.0.0",
@@ -2214,9 +2214,9 @@
}
},
"node_modules/@certusone/wormhole-sdk-wasm/node_modules/@types/node": {
- "version": "18.0.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz",
- "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ=="
+ "version": "18.7.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz",
+ "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ=="
},
"node_modules/@certusone/wormhole-sdk/node_modules/axios": {
"version": "0.24.0",
@@ -46816,11 +46816,11 @@
}
},
"@certusone/wormhole-sdk": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.5.0.tgz",
- "integrity": "sha512-Z8Cj2yZ41if842jSSLzKLomwkq9PgXdjVq3r3VNzkSM3aZavU8vZqNT33LU9IabmW7hiWe1uI9j2Z1JZe7SIEg==",
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.6.1.tgz",
+ "integrity": "sha512-S4zU62gIipNbEqXGl1SaHBNX7003T9WZU3K0/mr8agTPpqatPMWZArkjH9VqSVRRIEIkDB8zx5sQk9vCtrkTHQ==",
"requires": {
- "@certusone/wormhole-sdk-proto-web": "^0.0.1",
+ "@certusone/wormhole-sdk-proto-web": "^0.0.3",
"@certusone/wormhole-sdk-wasm": "^0.0.1",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
@@ -46847,9 +46847,9 @@
}
},
"@certusone/wormhole-sdk-proto-web": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.1.tgz",
- "integrity": "sha512-v6D+vCPqzTmrRuN0ZHpOdA1XnF3nmaD1wlJf025SXb7JFhVSmKyFXzLajkt50rk6SCkEvXtRlxNTJtnuCxg94Q==",
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.3.tgz",
+ "integrity": "sha512-O8gx8dLTcgF5jbmWjRiyZAn1LozslhWqDo6Q6QJfRiL6DWySV5TOXqgaEfQ4UGEM4uqM76HWZpwfEWUjaRhJ/A==",
"requires": {
"@improbable-eng/grpc-web": "^0.15.0",
"protobufjs": "^7.0.0",
@@ -46909,9 +46909,9 @@
},
"dependencies": {
"@types/node": {
- "version": "18.0.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz",
- "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ=="
+ "version": "18.7.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz",
+ "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ=="
}
}
},
diff --git a/bridge_ui/package.json b/bridge_ui/package.json
index 45af2c258..22a05117d 100644
--- a/bridge_ui/package.json
+++ b/bridge_ui/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@certusone/wormhole-sdk": "^0.5.0",
+ "@certusone/wormhole-sdk": "^0.6.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
diff --git a/bridge_ui/src/components/Recovery.tsx b/bridge_ui/src/components/Recovery.tsx
index 9a21929e7..37eb96020 100644
--- a/bridge_ui/src/components/Recovery.tsx
+++ b/bridge_ui/src/components/Recovery.tsx
@@ -79,6 +79,7 @@ import ButtonWithLoader from "./ButtonWithLoader";
import ChainSelect from "./ChainSelect";
import KeyAndBalance from "./KeyAndBalance";
import RelaySelector from "./RelaySelector";
+import PendingVAAWarning from "./Transfer/PendingVAAWarning";
const useStyles = makeStyles((theme) => ({
mainCard: {
@@ -97,6 +98,32 @@ const useStyles = makeStyles((theme) => ({
},
}));
+async function fetchSignedVAA(
+ chainId: ChainId,
+ emitterAddress: string,
+ sequence: string
+) {
+ const { vaaBytes, isPending } = await getSignedVAAWithRetry(
+ chainId,
+ emitterAddress,
+ sequence,
+ WORMHOLE_RPC_HOSTS.length
+ );
+ return {
+ vaa: vaaBytes ? uint8ArrayToHex(vaaBytes) : undefined,
+ isPending,
+ error: null,
+ };
+}
+
+function handleError(e: any, enqueueSnackbar: any) {
+ console.error(e);
+ enqueueSnackbar(null, {
+ content: {parseError(e)},
+ });
+ return { vaa: null, isPending: false, error: parseError(e) };
+}
+
async function algo(tx: string, enqueueSnackbar: any) {
try {
const algodClient = new algosdk.Algodv2(
@@ -126,19 +153,9 @@ async function algo(tx: string, enqueueSnackbar: any) {
throw new Error("Sequence not found");
}
const emitterAddress = getEmitterAddressAlgorand(ALGORAND_TOKEN_BRIDGE_ID);
- const { vaaBytes } = await getSignedVAAWithRetry(
- CHAIN_ID_ALGORAND,
- emitterAddress,
- sequence,
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
+ return fetchSignedVAA(CHAIN_ID_ALGORAND, emitterAddress, sequence);
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- return { vaa: null, error: parseError(e) };
+ return handleError(e, enqueueSnackbar);
}
}
@@ -160,19 +177,9 @@ async function evm(
? getNFTBridgeAddressForChain(chainId)
: getTokenBridgeAddressForChain(chainId)
);
- const { vaaBytes } = await getSignedVAAWithRetry(
- chainId,
- emitterAddress,
- sequence.toString(),
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
+ return fetchSignedVAA(chainId, emitterAddress, sequence);
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- return { vaa: null, error: parseError(e) };
+ return handleError(e, enqueueSnackbar);
}
}
@@ -187,19 +194,9 @@ async function solana(tx: string, enqueueSnackbar: any, nft: boolean) {
const emitterAddress = await getEmitterAddressSolana(
nft ? SOL_NFT_BRIDGE_ADDRESS : SOL_TOKEN_BRIDGE_ADDRESS
);
- const { vaaBytes } = await getSignedVAAWithRetry(
- CHAIN_ID_SOLANA,
- emitterAddress,
- sequence.toString(),
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
+ return fetchSignedVAA(CHAIN_ID_SOLANA, emitterAddress, sequence);
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- return { vaa: null, error: parseError(e) };
+ return handleError(e, enqueueSnackbar);
}
}
@@ -214,19 +211,9 @@ async function terra(tx: string, enqueueSnackbar: any, chainId: TerraChainId) {
const emitterAddress = await getEmitterAddressTerra(
getTokenBridgeAddressForChain(chainId)
);
- const { vaaBytes } = await getSignedVAAWithRetry(
- chainId,
- emitterAddress,
- sequence,
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
+ return fetchSignedVAA(chainId, emitterAddress, sequence);
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- return { vaa: null, error: parseError(e) };
+ return handleError(e, enqueueSnackbar);
}
}
@@ -380,6 +367,7 @@ export default function Recovery() {
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
const [recoverySignedVAA, setRecoverySignedVAA] = useState("");
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState(null);
+ const [isVAAPending, setIsVAAPending] = useState(false);
const [terra2TokenId, setTerra2TokenId] = useState("");
const { isReady, statusMessage } = useIsWalletReady(recoverySourceChain);
const walletConnectError =
@@ -449,7 +437,7 @@ export default function Recovery() {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
- const { vaa, error } = await evm(
+ const { vaa, isPending, error } = await evm(
provider,
recoverySourceTx,
enqueueSnackbar,
@@ -464,13 +452,14 @@ export default function Recovery() {
if (error) {
setRecoverySourceTxError(error);
}
+ setIsVAAPending(isPending);
}
})();
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
- const { vaa, error } = await solana(
+ const { vaa, isPending, error } = await solana(
recoverySourceTx,
enqueueSnackbar,
isNFT
@@ -483,6 +472,7 @@ export default function Recovery() {
if (error) {
setRecoverySourceTxError(error);
}
+ setIsVAAPending(isPending);
}
})();
} else if (isTerraChain(recoverySourceChain)) {
@@ -490,7 +480,7 @@ export default function Recovery() {
setRecoverySourceTxIsLoading(true);
setTerra2TokenId("");
(async () => {
- const { vaa, error } = await terra(
+ const { vaa, isPending, error } = await terra(
recoverySourceTx,
enqueueSnackbar,
recoverySourceChain
@@ -503,13 +493,17 @@ export default function Recovery() {
if (error) {
setRecoverySourceTxError(error);
}
+ setIsVAAPending(isPending);
}
})();
} else if (recoverySourceChain === CHAIN_ID_ALGORAND) {
setRecoverySourceTxError("");
setRecoverySourceTxIsLoading(true);
(async () => {
- const { vaa, error } = await algo(recoverySourceTx, enqueueSnackbar);
+ const { vaa, isPending, error } = await algo(
+ recoverySourceTx,
+ enqueueSnackbar
+ );
if (!cancelled) {
setRecoverySourceTxIsLoading(false);
if (vaa) {
@@ -518,6 +512,7 @@ export default function Recovery() {
if (error) {
setRecoverySourceTxError(error);
}
+ setIsVAAPending(isPending);
}
})();
}
@@ -701,6 +696,7 @@ export default function Recovery() {
>
Recover
+ {isVAAPending && }
}>
diff --git a/bridge_ui/src/components/Transfer/PendingVAAWarning.tsx b/bridge_ui/src/components/Transfer/PendingVAAWarning.tsx
new file mode 100644
index 000000000..95a2da4fc
--- /dev/null
+++ b/bridge_ui/src/components/Transfer/PendingVAAWarning.tsx
@@ -0,0 +1,36 @@
+import { Link, makeStyles } from "@material-ui/core";
+import { Alert } from "@material-ui/lab";
+import { useSelector } from "react-redux";
+import { selectTransferSourceChain } from "../../store/selectors";
+import { CHAINS_BY_ID } from "../../utils/consts";
+
+const useStyles = makeStyles((theme) => ({
+ alert: {
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+ },
+}));
+
+const PendingVAAWarning = () => {
+ const classes = useStyles();
+ const sourceChain = useSelector(selectTransferSourceChain);
+ const chainName = CHAINS_BY_ID[sourceChain]?.name || "unknown";
+ const message = `The daily notional value limit for transfers on ${chainName} has been exceeded. As
+ a result, the VAA for this transfer is pending. If you have any questions,
+ please open a support ticket on `;
+ return (
+
+ {message}
+
+ https://discord.gg/wormholecrypto
+
+ {"."}
+
+ );
+};
+
+export default PendingVAAWarning;
diff --git a/bridge_ui/src/components/Transfer/Send.tsx b/bridge_ui/src/components/Transfer/Send.tsx
index 78b080200..288f0fb16 100644
--- a/bridge_ui/src/components/Transfer/Send.tsx
+++ b/bridge_ui/src/components/Transfer/Send.tsx
@@ -8,7 +8,7 @@ import { Alert } from "@material-ui/lab";
import { ethers } from "ethers";
import { formatUnits, parseUnits } from "ethers/lib/utils";
import { useCallback, useMemo, useState } from "react";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import useAllowance from "../../hooks/useAllowance";
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
import useIsWalletReady from "../../hooks/useIsWalletReady";
@@ -16,6 +16,7 @@ import {
selectSourceWalletAddress,
selectTransferAmount,
selectTransferIsSendComplete,
+ selectTransferIsVAAPending,
selectTransferRelayerFee,
selectTransferSourceAsset,
selectTransferSourceChain,
@@ -23,6 +24,7 @@ import {
selectTransferTargetError,
selectTransferTransferTx,
} from "../../store/selectors";
+import { reset } from "../../store/transferSlice";
import { CHAINS_BY_ID, CLUSTER } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
@@ -31,10 +33,12 @@ import SolanaTPSWarning from "../SolanaTPSWarning";
import StepDescription from "../StepDescription";
import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
import TransactionProgress from "../TransactionProgress";
+import PendingVAAWarning from "./PendingVAAWarning";
import SendConfirmationDialog from "./SendConfirmationDialog";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
function Send() {
+ const dispatch = useDispatch();
const { handleClick, disabled, showLoader } = useHandleTransfer();
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const handleTransferClick = useCallback(() => {
@@ -47,6 +51,9 @@ function Send() {
const handleConfirmClose = useCallback(() => {
setIsConfirmOpen(false);
}, []);
+ const handleResetClick = useCallback(() => {
+ dispatch(reset());
+ }, [dispatch]);
const sourceChain = useSelector(selectTransferSourceChain);
const sourceAsset = useSelector(selectTransferSourceAsset);
@@ -79,6 +86,7 @@ function Send() {
parseUnits("1", sourceDecimals).toBigInt();
const transferTx = useSelector(selectTransferTransferTx);
const isSendComplete = useSelector(selectTransferIsSendComplete);
+ const isVAAPending = useSelector(selectTransferIsVAAPending);
const error = useSelector(selectTransferTargetError);
const [allowanceError, setAllowanceError] = useState("");
@@ -195,7 +203,7 @@ function Send() {
Transfer
@@ -212,8 +220,16 @@ function Send() {
+ {isVAAPending ? (
+ <>
+
+
+ Transfer More Tokens!
+
+ >
+ ) : null}
>
);
}
diff --git a/bridge_ui/src/components/Transfer/Source.tsx b/bridge_ui/src/components/Transfer/Source.tsx
index 7e8b9e6eb..08995bf87 100644
--- a/bridge_ui/src/components/Transfer/Source.tsx
+++ b/bridge_ui/src/components/Transfer/Source.tsx
@@ -46,6 +46,8 @@ import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
import SourceAssetWarning from "./SourceAssetWarning";
import ChainWarningMessage from "../ChainWarningMessage";
+import useIsTransferLimited from "../../hooks/useIsTransferLimited";
+import TransferLimitedWarning from "./TransferLimitedWarning";
const useStyles = makeStyles((theme) => ({
chainSelectWrapper: {
@@ -111,6 +113,7 @@ function Source() {
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields);
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
+ const isTransferLimited = useIsTransferLimited();
const handleMigrationClick = useCallback(() => {
if (sourceChain === CHAIN_ID_SOLANA) {
history.push(
@@ -248,11 +251,13 @@ function Source() {
) : null}
+
({
+ alert: {
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+ },
+}));
+
+const TransferLimitedWarning = ({
+ isTransferLimited,
+}: {
+ isTransferLimited: IsTransferLimitedResult;
+}) => {
+ const classes = useStyles();
+ if (
+ isTransferLimited.isLimited &&
+ isTransferLimited.reason &&
+ isTransferLimited.limits
+ ) {
+ const chainName =
+ CHAINS_BY_ID[isTransferLimited.limits.chainId]?.name || "unknown";
+ const message =
+ isTransferLimited.reason === "EXCEEDS_MAX_NOTIONAL"
+ ? `This transfer's estimated notional value would exceed the notional value limit for transfers on ${chainName} (${USD_FORMATTER.format(
+ isTransferLimited.limits.chainNotionalLimit
+ )}).`
+ : isTransferLimited.reason === "EXCEEDS_REMAINING_NOTIONAL"
+ ? `This transfer's estimated notional value may exceed the remaining notional value available for transfers on ${chainName} (${USD_FORMATTER.format(
+ isTransferLimited.limits.chainRemainingAvailableNotional
+ )}).`
+ : "";
+ return (
+
+ {message}
+
+ );
+ }
+ return null;
+};
+
+export default TransferLimitedWarning;
diff --git a/bridge_ui/src/hooks/useHandleAttest.tsx b/bridge_ui/src/hooks/useHandleAttest.tsx
index 4aa3aa4ae..4b0fc5b64 100644
--- a/bridge_ui/src/hooks/useHandleAttest.tsx
+++ b/bridge_ui/src/hooks/useHandleAttest.tsx
@@ -11,6 +11,7 @@ import {
getEmitterAddressEth,
getEmitterAddressSolana,
getEmitterAddressTerra,
+ getSignedVAAWithRetry,
isEVMChain,
isTerraChain,
parseSequenceFromLogAlgorand,
@@ -58,8 +59,8 @@ import {
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
+ WORMHOLE_RPC_HOSTS,
} from "../utils/consts";
-import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import { postWithFees, waitForTerraExecution } from "../utils/terra";
@@ -72,7 +73,6 @@ async function algo(
) {
dispatch(setIsSending(true));
try {
- console.log("ALGO", sourceAsset);
const algodClient = new algosdk.Algodv2(
ALGORAND_HOST.algodToken,
ALGORAND_HOST.algodServer,
@@ -102,6 +102,7 @@ async function algo(
content: Fetching VAA,
});
const { vaaBytes } = await getSignedVAAWithRetry(
+ WORMHOLE_RPC_HOSTS,
CHAIN_ID_ALGORAND,
emitterAddress,
sequence
@@ -156,6 +157,7 @@ async function evm(
content: Fetching VAA,
});
const { vaaBytes } = await getSignedVAAWithRetry(
+ WORMHOLE_RPC_HOSTS,
chainId,
emitterAddress,
sequence
@@ -208,6 +210,7 @@ async function solana(
content: Fetching VAA,
});
const { vaaBytes } = await getSignedVAAWithRetry(
+ WORMHOLE_RPC_HOSTS,
CHAIN_ID_SOLANA,
emitterAddress,
sequence
@@ -262,6 +265,7 @@ async function terra(
content: Fetching VAA,
});
const { vaaBytes } = await getSignedVAAWithRetry(
+ WORMHOLE_RPC_HOSTS,
chainId,
emitterAddress,
sequence
diff --git a/bridge_ui/src/hooks/useHandleNFTTransfer.tsx b/bridge_ui/src/hooks/useHandleNFTTransfer.tsx
index b4fafe315..e9a079448 100644
--- a/bridge_ui/src/hooks/useHandleNFTTransfer.tsx
+++ b/bridge_ui/src/hooks/useHandleNFTTransfer.tsx
@@ -4,6 +4,7 @@ import {
CHAIN_ID_SOLANA,
getEmitterAddressEth,
getEmitterAddressSolana,
+ getSignedVAAWithRetry,
hexToUint8Array,
isEVMChain,
parseSequenceFromLogEth,
@@ -47,8 +48,8 @@ import {
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_NFT_BRIDGE_ADDRESS,
+ WORMHOLE_RPC_HOSTS,
} from "../utils/consts";
-import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import parseError from "../utils/parseError";
import { signSendAndConfirm } from "../utils/solana";
import useNFTTargetAddressHex from "./useNFTTargetAddress";
@@ -96,6 +97,7 @@ async function evm(
content: Fetching VAA,
});
const { vaaBytes } = await getSignedVAAWithRetry(
+ WORMHOLE_RPC_HOSTS,
chainId,
emitterAddress,
sequence.toString()
@@ -162,6 +164,7 @@ async function solana(
content: Fetching VAA,
});
const { vaaBytes } = await getSignedVAAWithRetry(
+ WORMHOLE_RPC_HOSTS,
CHAIN_ID_SOLANA,
emitterAddress,
sequence
diff --git a/bridge_ui/src/hooks/useHandleTransfer.tsx b/bridge_ui/src/hooks/useHandleTransfer.tsx
index 54fa3dec8..33904e5c6 100644
--- a/bridge_ui/src/hooks/useHandleTransfer.tsx
+++ b/bridge_ui/src/hooks/useHandleTransfer.tsx
@@ -54,6 +54,7 @@ import {
selectTransferTargetChain,
} from "../store/selectors";
import {
+ setIsVAAPending,
setIsSending,
setSignedVAAHex,
setTransferTx,
@@ -75,6 +76,46 @@ import { signSendAndConfirm } from "../utils/solana";
import { postWithFees, waitForTerraExecution } from "../utils/terra";
import useTransferTargetAddressHex from "./useTransferTargetAddress";
+async function fetchSignedVAA(
+ chainId: ChainId,
+ emitterAddress: string,
+ sequence: string,
+ enqueueSnackbar: any,
+ dispatch: any
+) {
+ enqueueSnackbar(null, {
+ content: Fetching VAA,
+ });
+ const { vaaBytes, isPending } = await getSignedVAAWithRetry(
+ chainId,
+ emitterAddress,
+ sequence
+ );
+ if (vaaBytes !== undefined) {
+ dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
+ dispatch(setIsVAAPending(false));
+ enqueueSnackbar(null, {
+ content: Fetched Signed VAA,
+ });
+ } else if (isPending) {
+ dispatch(setIsVAAPending(isPending));
+ enqueueSnackbar(null, {
+ content: VAA is Pending,
+ });
+ } else {
+ throw new Error("Error retrieving VAA info");
+ }
+}
+
+function handleError(e: any, enqueueSnackbar: any, dispatch: any) {
+ console.error(e);
+ enqueueSnackbar(null, {
+ content: {parseError(e)},
+ });
+ dispatch(setIsSending(false));
+ dispatch(setIsVAAPending(false));
+}
+
async function algo(
dispatch: any,
enqueueSnackbar: any,
@@ -120,24 +161,15 @@ async function algo(
content: Transaction confirmed,
});
const emitterAddress = getEmitterAddressAlgorand(ALGORAND_TOKEN_BRIDGE_ID);
- enqueueSnackbar(null, {
- content: Fetching VAA,
- });
- const { vaaBytes } = await getSignedVAAWithRetry(
+ await fetchSignedVAA(
chainId,
emitterAddress,
- sequence
+ sequence,
+ enqueueSnackbar,
+ dispatch
);
- dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
- enqueueSnackbar(null, {
- content: Fetched Signed VAA,
- });
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- dispatch(setIsSending(false));
+ handleError(e, enqueueSnackbar, dispatch);
}
}
@@ -205,24 +237,15 @@ async function evm(
const emitterAddress = getEmitterAddressEth(
getTokenBridgeAddressForChain(chainId)
);
- enqueueSnackbar(null, {
- content: Fetching VAA,
- });
- const { vaaBytes } = await getSignedVAAWithRetry(
+ await fetchSignedVAA(
chainId,
emitterAddress,
- sequence.toString()
+ sequence,
+ enqueueSnackbar,
+ dispatch
);
- dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
- enqueueSnackbar(null, {
- content: Fetched Signed VAA,
- });
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- dispatch(setIsSending(false));
+ handleError(e, enqueueSnackbar, dispatch);
}
}
@@ -291,25 +314,15 @@ async function solana(
const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS
);
- enqueueSnackbar(null, {
- content: Fetching VAA,
- });
- const { vaaBytes } = await getSignedVAAWithRetry(
+ await fetchSignedVAA(
CHAIN_ID_SOLANA,
emitterAddress,
- sequence
+ sequence,
+ enqueueSnackbar,
+ dispatch
);
-
- dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
- enqueueSnackbar(null, {
- content: Fetched Signed VAA,
- });
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- dispatch(setIsSending(false));
+ handleError(e, enqueueSnackbar, dispatch);
}
}
@@ -360,24 +373,15 @@ async function terra(
throw new Error("Sequence not found");
}
const emitterAddress = await getEmitterAddressTerra(tokenBridgeAddress);
- enqueueSnackbar(null, {
- content: Fetching VAA,
- });
- const { vaaBytes } = await getSignedVAAWithRetry(
+ await fetchSignedVAA(
chainId,
emitterAddress,
- sequence
+ sequence,
+ enqueueSnackbar,
+ dispatch
);
- enqueueSnackbar(null, {
- content: Fetched Signed VAA,
- });
- dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: {parseError(e)},
- });
- dispatch(setIsSending(false));
+ handleError(e, enqueueSnackbar, dispatch);
}
}
diff --git a/bridge_ui/src/hooks/useIsTransferLimited.ts b/bridge_ui/src/hooks/useIsTransferLimited.ts
new file mode 100644
index 000000000..48c16576e
--- /dev/null
+++ b/bridge_ui/src/hooks/useIsTransferLimited.ts
@@ -0,0 +1,152 @@
+import axios from "axios";
+import { useEffect, useMemo, useRef, useState } from "react";
+import { useSelector } from "react-redux";
+import {
+ selectTransferAmount,
+ selectTransferOriginAsset,
+ selectTransferOriginChain,
+ selectTransferSourceChain,
+} from "../store/selectors";
+import { WORMHOLE_RPC_HOSTS } from "../utils/consts";
+import { ChainId } from "@certusone/wormhole-sdk";
+
+const REMAINING_NOTIONAL_TOLERANCE = 0.95;
+interface TokenListEntry {
+ originAddress: string;
+ originChainId: number;
+ price: number;
+}
+
+interface TokenList {
+ entries: TokenListEntry[];
+}
+
+interface AvailableNotionalByChainEntry {
+ chainId: number;
+ remainingAvailableNotional: number;
+ notionalLimit: number;
+}
+
+interface AvailableNotionalByChain {
+ entries: AvailableNotionalByChainEntry[];
+}
+
+export interface ChainLimits {
+ chainId: ChainId;
+ chainNotionalLimit: number;
+ chainRemainingAvailableNotional: number;
+ tokenPrice: number;
+}
+
+export interface IsTransferLimitedResult {
+ isLimited: boolean;
+ reason?: "EXCEEDS_REMAINING_NOTIONAL" | "EXCEEDS_MAX_NOTIONAL";
+ limits?: ChainLimits;
+}
+
+const useIsTransferLimited = (): IsTransferLimitedResult => {
+ const [tokenList, setTokenList] = useState(null);
+ const [availableNotionalByChain, setAvailableNotionalByChain] =
+ useState(null);
+
+ const sourceChain = useSelector(selectTransferSourceChain);
+ const originChain = useSelector(selectTransferOriginChain);
+ const originAsset = useSelector(selectTransferOriginAsset);
+ const amount = useSelector(selectTransferAmount);
+ const amountParsed = useMemo(() => {
+ return amount ? parseFloat(amount) : undefined;
+ }, [amount]);
+
+ const effectTriggered = useRef(false);
+
+ useEffect(() => {
+ if (!effectTriggered.current && amountParsed) {
+ let cancelled = false;
+ (async () => {
+ for (const rpcHost of WORMHOLE_RPC_HOSTS) {
+ try {
+ const baseUrl = `${rpcHost}/v1/governor`;
+ const [tokenListResponse, availableNotionalByChainResponse] =
+ await Promise.all([
+ axios.get(`${baseUrl}/token_list`),
+ axios.get(
+ `${baseUrl}/available_notional_by_chain`
+ ),
+ ]);
+ if (!cancelled) {
+ setTokenList(tokenListResponse.data);
+ setAvailableNotionalByChain(
+ availableNotionalByChainResponse.data
+ );
+ break;
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ if (cancelled) {
+ break;
+ }
+ }
+ return () => {
+ cancelled = true;
+ };
+ })();
+ effectTriggered.current = true;
+ }
+ }, [amountParsed]);
+
+ const result = useMemo(() => {
+ if (
+ originAsset &&
+ originChain &&
+ amountParsed &&
+ tokenList &&
+ availableNotionalByChain
+ ) {
+ const token = tokenList.entries.find(
+ (entry) =>
+ entry.originChainId === originChain &&
+ entry.originAddress === "0x" + originAsset
+ );
+ if (token) {
+ const chain = availableNotionalByChain.entries.find(
+ (entry) => entry.chainId === sourceChain
+ );
+ if (chain) {
+ const transferNotional = token.price * amountParsed;
+ const isLimitedReason =
+ transferNotional > chain.notionalLimit
+ ? "EXCEEDS_MAX_NOTIONAL"
+ : transferNotional >
+ chain.remainingAvailableNotional * REMAINING_NOTIONAL_TOLERANCE
+ ? "EXCEEDS_REMAINING_NOTIONAL"
+ : undefined;
+ return {
+ isLimited: !!isLimitedReason,
+ reason: isLimitedReason,
+ limits: {
+ chainId: sourceChain,
+ chainNotionalLimit: chain.notionalLimit,
+ chainRemainingAvailableNotional: chain.remainingAvailableNotional,
+ tokenPrice: token.price,
+ },
+ };
+ }
+ }
+ }
+ return {
+ isLimited: false,
+ };
+ }, [
+ sourceChain,
+ originChain,
+ originAsset,
+ amountParsed,
+ tokenList,
+ availableNotionalByChain,
+ ]);
+
+ return result;
+};
+
+export default useIsTransferLimited;
diff --git a/bridge_ui/src/store/selectors.ts b/bridge_ui/src/store/selectors.ts
index 291d52947..70006d8d2 100644
--- a/bridge_ui/src/store/selectors.ts
+++ b/bridge_ui/src/store/selectors.ts
@@ -201,6 +201,8 @@ export const selectTransferTransferTx = (state: RootState) =>
state.transfer.transferTx;
export const selectTransferSignedVAAHex = (state: RootState) =>
state.transfer.signedVAAHex;
+export const selectTransferIsVAAPending = (state: RootState) =>
+ state.transfer.isVAAPending;
export const selectTransferIsSending = (state: RootState) =>
state.transfer.isSending;
export const selectTransferIsRedeeming = (state: RootState) =>
diff --git a/bridge_ui/src/store/transferSlice.ts b/bridge_ui/src/store/transferSlice.ts
index 9926d97c6..e83606d4d 100644
--- a/bridge_ui/src/store/transferSlice.ts
+++ b/bridge_ui/src/store/transferSlice.ts
@@ -54,6 +54,7 @@ export interface TransferState {
transferTx: Transaction | undefined;
signedVAAHex: string | undefined;
isSending: boolean;
+ isVAAPending: boolean;
isRedeeming: boolean;
redeemTx: Transaction | undefined;
isApproving: boolean;
@@ -81,6 +82,7 @@ const initialState: TransferState = {
transferTx: undefined,
signedVAAHex: undefined,
isSending: false,
+ isVAAPending: false,
isRedeeming: false,
redeemTx: undefined,
isApproving: false,
@@ -214,11 +216,15 @@ export const transferSlice = createSlice({
setSignedVAAHex: (state, action: PayloadAction) => {
state.signedVAAHex = action.payload;
state.isSending = false;
+ state.isVAAPending = false;
state.activeStep = 3;
},
setIsSending: (state, action: PayloadAction) => {
state.isSending = action.payload;
},
+ setIsVAAPending: (state, action: PayloadAction) => {
+ state.isVAAPending = action.payload;
+ },
setIsRedeeming: (state, action: PayloadAction) => {
state.isRedeeming = action.payload;
},
@@ -325,6 +331,7 @@ export const {
setTransferTx,
setSignedVAAHex,
setIsSending,
+ setIsVAAPending,
setIsRedeeming,
setRedeemTx,
setIsApproving,
diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts
index 321e4cca7..cd2fb3b3c 100644
--- a/bridge_ui/src/utils/consts.ts
+++ b/bridge_ui/src/utils/consts.ts
@@ -1593,3 +1593,9 @@ export const getIsTokenTransferDisabled = (
? disabledTransfers.length === 0 || disabledTransfers.includes(targetChain)
: false;
};
+
+export const USD_NUMBER_FORMATTER = new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+});
diff --git a/bridge_ui/src/utils/getSignedVAAWithRetry.ts b/bridge_ui/src/utils/getSignedVAAWithRetry.ts
index dfb1360bd..0709b076f 100644
--- a/bridge_ui/src/utils/getSignedVAAWithRetry.ts
+++ b/bridge_ui/src/utils/getSignedVAAWithRetry.ts
@@ -1,34 +1,42 @@
-import { ChainId, getSignedVAA } from "@certusone/wormhole-sdk";
+import {
+ ChainId,
+ ChainName,
+ getGovernorIsVAAEnqueued,
+ getSignedVAA,
+} from "@certusone/wormhole-sdk";
import { WORMHOLE_RPC_HOSTS } from "./consts";
-export let CURRENT_WORMHOLE_RPC_HOST = -1;
+export interface GetSignedVAAWithRetryResult {
+ vaaBytes: Uint8Array | undefined;
+ isPending: boolean;
+}
-export const getNextRpcHost = () =>
- ++CURRENT_WORMHOLE_RPC_HOST % WORMHOLE_RPC_HOSTS.length;
-
-export async function getSignedVAAWithRetry(
- emitterChain: ChainId,
+export const getSignedVAAWithRetry = async (
+ emitterChain: ChainId | ChainName,
emitterAddress: string,
sequence: string,
retryAttempts?: number
-) {
- let result;
+): Promise => {
+ let currentWormholeRpcHost = -1;
+ const getNextRpcHost = () =>
+ ++currentWormholeRpcHost % WORMHOLE_RPC_HOSTS.length;
let attempts = 0;
- while (!result) {
+ while (true) {
attempts++;
await new Promise((resolve) => setTimeout(resolve, 1000));
- try {
- result = await getSignedVAA(
- WORMHOLE_RPC_HOSTS[getNextRpcHost()],
- emitterChain,
- emitterAddress,
- sequence
- );
- } catch (e) {
- if (retryAttempts !== undefined && attempts > retryAttempts) {
- throw e;
- }
+ const rpcHost = WORMHOLE_RPC_HOSTS[getNextRpcHost()];
+ const results = await Promise.allSettled([
+ getSignedVAA(rpcHost, emitterChain, emitterAddress, sequence),
+ getGovernorIsVAAEnqueued(rpcHost, emitterChain, emitterAddress, sequence),
+ ]);
+ if (results[0].status === "fulfilled") {
+ return { vaaBytes: results[0].value.vaaBytes, isPending: false };
+ }
+ if (results[1].status === "fulfilled" && results[1].value.isEnqueued) {
+ return { vaaBytes: undefined, isPending: true };
+ }
+ if (retryAttempts !== undefined && attempts > retryAttempts) {
+ throw new Error(results[0].reason);
}
}
- return result;
-}
+};