bridge_ui: Chain Governor support (#1421)

This commit is contained in:
kev1n-peters 2022-08-15 09:18:01 -05:00 committed by GitHub
parent 484db04f79
commit 4f08f315f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 458 additions and 164 deletions

View File

@ -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())

View File

@ -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=="
}
}
},

View File

@ -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",

View File

@ -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: <Alert severity="error">{parseError(e)}</Alert>,
});
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: <Alert severity="error">{parseError(e)}</Alert>,
});
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: <Alert severity="error">{parseError(e)}</Alert>,
});
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: <Alert severity="error">{parseError(e)}</Alert>,
});
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: <Alert severity="error">{parseError(e)}</Alert>,
});
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<any>(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
</ButtonWithLoader>
{isVAAPending && <PendingVAAWarning />}
<div className={classes.advancedContainer}>
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>

View File

@ -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 (
<Alert variant="outlined" severity="warning" className={classes.alert}>
{message}
<Link
href="https://discord.gg/wormholecrypto"
target="_blank"
rel="noopener noreferrer"
>
https://discord.gg/wormholecrypto
</Link>
{"."}
</Alert>
);
};
export default PendingVAAWarning;

View File

@ -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() {
<ButtonWithLoader
disabled={isDisabled}
onClick={handleTransferClick}
showLoader={showLoader}
showLoader={showLoader && !isVAAPending}
error={errorMessage}
>
Transfer
@ -212,8 +220,16 @@ function Send() {
<TransactionProgress
chainId={sourceChain}
tx={transferTx}
isSendComplete={isSendComplete}
isSendComplete={isSendComplete || isVAAPending}
/>
{isVAAPending ? (
<>
<PendingVAAWarning />
<ButtonWithLoader onClick={handleResetClick}>
Transfer More Tokens!
</ButtonWithLoader>
</>
) : null}
</>
);
}

View File

@ -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}
<ChainWarningMessage chainId={sourceChain} />
<ChainWarningMessage chainId={targetChain} />
<TransferLimitedWarning isTransferLimited={isTransferLimited} />
<ButtonWithLoader
disabled={
!isSourceComplete ||
isSourceTransferDisabled ||
isTargetTransferDisabled
isTargetTransferDisabled ||
isTransferLimited.reason === "EXCEEDS_MAX_NOTIONAL"
}
onClick={handleNextClick}
showLoader={false}

View File

@ -0,0 +1,48 @@
import { makeStyles } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { IsTransferLimitedResult } from "../../hooks/useIsTransferLimited";
import {
CHAINS_BY_ID,
USD_NUMBER_FORMATTER as USD_FORMATTER,
} from "../../utils/consts";
const useStyles = makeStyles((theme) => ({
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 (
<Alert variant="outlined" severity="warning" className={classes.alert}>
{message}
</Alert>
);
}
return null;
};
export default TransferLimitedWarning;

View File

@ -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: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_ALGORAND,
emitterAddress,
sequence
@ -156,6 +157,7 @@ async function evm(
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
chainId,
emitterAddress,
sequence
@ -208,6 +210,7 @@ async function solana(
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_SOLANA,
emitterAddress,
sequence
@ -262,6 +265,7 @@ async function terra(
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
chainId,
emitterAddress,
sequence

View File

@ -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: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
chainId,
emitterAddress,
sequence.toString()
@ -162,6 +164,7 @@ async function solana(
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_SOLANA,
emitterAddress,
sequence

View File

@ -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: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes, isPending } = await getSignedVAAWithRetry(
chainId,
emitterAddress,
sequence
);
if (vaaBytes !== undefined) {
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
dispatch(setIsVAAPending(false));
enqueueSnackbar(null, {
content: <Alert severity="success">Fetched Signed VAA</Alert>,
});
} else if (isPending) {
dispatch(setIsVAAPending(isPending));
enqueueSnackbar(null, {
content: <Alert severity="warning">VAA is Pending</Alert>,
});
} else {
throw new Error("Error retrieving VAA info");
}
}
function handleError(e: any, enqueueSnackbar: any, dispatch: any) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
dispatch(setIsSending(false));
dispatch(setIsVAAPending(false));
}
async function algo(
dispatch: any,
enqueueSnackbar: any,
@ -120,24 +161,15 @@ async function algo(
content: <Alert severity="success">Transaction confirmed</Alert>,
});
const emitterAddress = getEmitterAddressAlgorand(ALGORAND_TOKEN_BRIDGE_ID);
enqueueSnackbar(null, {
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
await fetchSignedVAA(
chainId,
emitterAddress,
sequence
sequence,
enqueueSnackbar,
dispatch
);
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));
handleError(e, enqueueSnackbar, dispatch);
}
}
@ -205,24 +237,15 @@ async function evm(
const emitterAddress = getEmitterAddressEth(
getTokenBridgeAddressForChain(chainId)
);
enqueueSnackbar(null, {
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
await fetchSignedVAA(
chainId,
emitterAddress,
sequence.toString()
sequence,
enqueueSnackbar,
dispatch
);
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));
handleError(e, enqueueSnackbar, dispatch);
}
}
@ -291,25 +314,15 @@ async function solana(
const emitterAddress = await getEmitterAddressSolana(
SOL_TOKEN_BRIDGE_ADDRESS
);
enqueueSnackbar(null, {
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
await fetchSignedVAA(
CHAIN_ID_SOLANA,
emitterAddress,
sequence
sequence,
enqueueSnackbar,
dispatch
);
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));
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: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
await fetchSignedVAA(
chainId,
emitterAddress,
sequence
sequence,
enqueueSnackbar,
dispatch
);
enqueueSnackbar(null, {
content: <Alert severity="success">Fetched Signed VAA</Alert>,
});
dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
} catch (e) {
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
dispatch(setIsSending(false));
handleError(e, enqueueSnackbar, dispatch);
}
}

View File

@ -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<TokenList | null>(null);
const [availableNotionalByChain, setAvailableNotionalByChain] =
useState<AvailableNotionalByChain | null>(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<TokenList>(`${baseUrl}/token_list`),
axios.get<AvailableNotionalByChain>(
`${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<IsTransferLimitedResult>(() => {
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;

View File

@ -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) =>

View File

@ -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<string>) => {
state.signedVAAHex = action.payload;
state.isSending = false;
state.isVAAPending = false;
state.activeStep = 3;
},
setIsSending: (state, action: PayloadAction<boolean>) => {
state.isSending = action.payload;
},
setIsVAAPending: (state, action: PayloadAction<boolean>) => {
state.isVAAPending = action.payload;
},
setIsRedeeming: (state, action: PayloadAction<boolean>) => {
state.isRedeeming = action.payload;
},
@ -325,6 +331,7 @@ export const {
setTransferTx,
setSignedVAAHex,
setIsSending,
setIsVAAPending,
setIsRedeeming,
setRedeemTx,
setIsApproving,

View File

@ -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,
});

View File

@ -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<GetSignedVAAWithRetryResult> => {
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;
}
};