bridge_ui: Chain Governor support (#1421)
This commit is contained in:
parent
484db04f79
commit
4f08f315f1
7
Tiltfile
7
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())
|
||||
|
|
|
@ -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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 />}>
|
||||
|
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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) =>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
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 e;
|
||||
throw new Error(results[0].reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue