Minor fixes and bidirectional transfers
Change-Id: I7cbb02fe79b799c1ce350cee9c5b73ea17483385
This commit is contained in:
parent
1da690aa42
commit
8b3e0f00e0
|
@ -20,6 +20,7 @@
|
|||
"@typechain/ethers-v5": "^7.0.1",
|
||||
"bridge": "file:rust_modules\\core",
|
||||
"ethers": "^5.4.1",
|
||||
"prettier": "^2.3.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.4",
|
||||
|
@ -29467,7 +29468,6 @@
|
|||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
|
||||
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
|
@ -35735,7 +35735,6 @@
|
|||
"resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
|
||||
"integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.0.0-beta.39",
|
||||
"@babel/traverse": "^7.0.0-beta.39",
|
||||
|
@ -35749,7 +35748,6 @@
|
|||
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz",
|
||||
"integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"babylon": "bin/babylon.js"
|
||||
},
|
||||
|
@ -36585,7 +36583,6 @@
|
|||
"resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz",
|
||||
"integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
|
@ -36595,7 +36592,6 @@
|
|||
"resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz",
|
||||
"integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0-beta.36",
|
||||
"long": "^3.2.0",
|
||||
|
@ -36615,7 +36611,6 @@
|
|||
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
|
||||
"integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
|
@ -46630,6 +46625,7 @@
|
|||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bitcore-lib": "^8.25.10",
|
||||
"unorm": "^1.4.1"
|
||||
}
|
||||
},
|
||||
|
@ -62340,8 +62336,7 @@
|
|||
"prettier": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
|
||||
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
|
||||
},
|
||||
"pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
|
@ -67424,7 +67419,6 @@
|
|||
"resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
|
||||
"integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@babel/core": "^7.0.0-beta.39",
|
||||
"@babel/traverse": "^7.0.0-beta.39",
|
||||
|
@ -67437,8 +67431,7 @@
|
|||
"version": "7.0.0-beta.47",
|
||||
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz",
|
||||
"integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -67448,7 +67441,8 @@
|
|||
"integrity": "sha512-R4s75XH+o8qM+WaRrAU9S2rbAMDzob18/S3V8R9ZoFpZkPWLAohWWlzWAp1ybeTkOuuku/X1zJtxiV0pBYxZww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^1.1.0"
|
||||
"loader-utils": "^1.1.0",
|
||||
"wasm-dce": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
|
@ -68170,15 +68164,13 @@
|
|||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz",
|
||||
"integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"webassembly-interpreter": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz",
|
||||
"integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0-beta.36",
|
||||
"long": "^3.2.0",
|
||||
|
@ -68189,8 +68181,7 @@
|
|||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
|
||||
"integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
"@truffle/hdwallet-provider": "^1.4.1",
|
||||
"copy-dir": "^1.3.0",
|
||||
"truffle": "^5.4.1",
|
||||
"wasm-loader": "^1.3.0"
|
||||
"wasm-loader": "^1.3.0",
|
||||
"prettier": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +1,83 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
import useAttestSignedVAA from "../../hooks/useAttestSignedVAA";
|
||||
import { setIsCreating } from "../../store/attestSlice";
|
||||
import {
|
||||
selectAttestIsCreating,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
import createWrappedOn, {
|
||||
createWrappedOnSolana,
|
||||
} from "../../utils/createWrappedOn";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
function Create() {
|
||||
const dispatch = useDispatch();
|
||||
const classes = useStyles();
|
||||
const targetChain = useSelector(selectAttestTargetChain);
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const signedVAA = useAttestSignedVAA();
|
||||
const isCreating = useSelector(selectAttestIsCreating);
|
||||
const handleCreateClick = useCallback(() => {
|
||||
if (
|
||||
targetChain === CHAIN_ID_SOLANA &&
|
||||
createWrappedOn[targetChain] === createWrappedOnSolana &&
|
||||
signedVAA
|
||||
) {
|
||||
dispatch(setIsCreating(true));
|
||||
createWrappedOnSolana(wallet, solPK?.toString(), signedVAA);
|
||||
}
|
||||
}, [dispatch, targetChain, wallet, solPK, signedVAA]);
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
disabled={isCreating}
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
{isCreating ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Create;
|
||||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
import useAttestSignedVAA from "../../hooks/useAttestSignedVAA";
|
||||
import { setIsCreating } from "../../store/attestSlice";
|
||||
import {
|
||||
selectAttestIsCreating,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
import createWrappedOn, {
|
||||
createWrappedOnEth,
|
||||
createWrappedOnSolana,
|
||||
} from "../../utils/createWrappedOn";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
function Create() {
|
||||
const dispatch = useDispatch();
|
||||
const classes = useStyles();
|
||||
const targetChain = useSelector(selectAttestTargetChain);
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const signedVAA = useAttestSignedVAA();
|
||||
const isCreating = useSelector(selectAttestIsCreating);
|
||||
const { provider, signer } = useEthereumProvider();
|
||||
const handleCreateClick = useCallback(() => {
|
||||
if (
|
||||
targetChain === CHAIN_ID_SOLANA &&
|
||||
createWrappedOn[targetChain] === createWrappedOnSolana &&
|
||||
signedVAA
|
||||
) {
|
||||
dispatch(setIsCreating(true));
|
||||
createWrappedOnSolana(wallet, solPK?.toString(), signedVAA);
|
||||
}
|
||||
if (
|
||||
targetChain === CHAIN_ID_ETH &&
|
||||
createWrappedOn[targetChain] === createWrappedOnEth &&
|
||||
signedVAA
|
||||
) {
|
||||
(async () => {
|
||||
dispatch(setIsCreating(true));
|
||||
createWrappedOnEth(provider, signer, signedVAA);
|
||||
})();
|
||||
}
|
||||
}, [dispatch, targetChain, wallet, solPK, signedVAA]);
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
disabled={isCreating}
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
{isCreating ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Create;
|
||||
|
|
|
@ -1,127 +1,127 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
import useWrappedAsset from "../../hooks/useWrappedAsset";
|
||||
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
|
||||
import {
|
||||
selectAttestIsSendComplete,
|
||||
selectAttestIsSending,
|
||||
selectAttestIsTargetComplete,
|
||||
selectAttestSourceAsset,
|
||||
selectAttestSourceChain,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { uint8ArrayToHex } from "../../utils/array";
|
||||
import attestFrom, {
|
||||
attestFromEth,
|
||||
attestFromSolana,
|
||||
} from "../../utils/attestFrom";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: move attest to its own workflow
|
||||
|
||||
function Send() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectAttestSourceChain);
|
||||
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||
const targetChain = useSelector(selectAttestTargetChain);
|
||||
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
|
||||
const isSending = useSelector(selectAttestIsSending);
|
||||
const isSendComplete = useSelector(selectAttestIsSendComplete);
|
||||
const { provider, signer } = useEthereumProvider();
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const {
|
||||
isLoading: isCheckingWrapped,
|
||||
// isWrapped,
|
||||
wrappedAsset,
|
||||
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
|
||||
// TODO: check this and send to separate flow
|
||||
const isWrapped = true;
|
||||
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
|
||||
// TODO: dynamically get "to" wallet
|
||||
const handleAttestClick = useCallback(() => {
|
||||
// TODO: more generic way of calling these
|
||||
if (attestFrom[sourceChain]) {
|
||||
if (
|
||||
sourceChain === CHAIN_ID_ETH &&
|
||||
attestFrom[sourceChain] === attestFromEth
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await attestFromEth(provider, signer, sourceAsset);
|
||||
console.log("bytes in attest", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (
|
||||
sourceChain === CHAIN_ID_SOLANA &&
|
||||
attestFrom[sourceChain] === attestFromSolana
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await attestFromSolana(
|
||||
wallet,
|
||||
solPK?.toString(),
|
||||
sourceAsset
|
||||
);
|
||||
console.log("bytes in attest", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [dispatch, sourceChain, provider, signer, wallet, solPK, sourceAsset]);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
onClick={handleAttestClick}
|
||||
disabled={!isTargetComplete || isSending || isSendComplete}
|
||||
>
|
||||
Attest
|
||||
</Button>
|
||||
{isSending ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Send;
|
||||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
import useWrappedAsset from "../../hooks/useWrappedAsset";
|
||||
import { setIsSending, setSignedVAAHex } from "../../store/attestSlice";
|
||||
import {
|
||||
selectAttestIsSendComplete,
|
||||
selectAttestIsSending,
|
||||
selectAttestIsTargetComplete,
|
||||
selectAttestSourceAsset,
|
||||
selectAttestSourceChain,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { uint8ArrayToHex } from "../../utils/array";
|
||||
import attestFrom, {
|
||||
attestFromEth,
|
||||
attestFromSolana,
|
||||
} from "../../utils/attestFrom";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: move attest to its own workflow
|
||||
|
||||
function Send() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectAttestSourceChain);
|
||||
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||
const targetChain = useSelector(selectAttestTargetChain);
|
||||
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
|
||||
const isSending = useSelector(selectAttestIsSending);
|
||||
const isSendComplete = useSelector(selectAttestIsSendComplete);
|
||||
const { provider, signer } = useEthereumProvider();
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const {
|
||||
isLoading: isCheckingWrapped,
|
||||
// isWrapped,
|
||||
wrappedAsset,
|
||||
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
|
||||
// TODO: check this and send to separate flow
|
||||
const isWrapped = true;
|
||||
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
|
||||
// TODO: dynamically get "to" wallet
|
||||
const handleAttestClick = useCallback(() => {
|
||||
// TODO: more generic way of calling these
|
||||
if (attestFrom[sourceChain]) {
|
||||
if (
|
||||
sourceChain === CHAIN_ID_ETH &&
|
||||
attestFrom[sourceChain] === attestFromEth
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await attestFromEth(provider, signer, sourceAsset);
|
||||
console.log("bytes in attest", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (
|
||||
sourceChain === CHAIN_ID_SOLANA &&
|
||||
attestFrom[sourceChain] === attestFromSolana
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await attestFromSolana(
|
||||
wallet,
|
||||
solPK?.toString(),
|
||||
sourceAsset
|
||||
);
|
||||
console.log("bytes in attest", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [dispatch, sourceChain, provider, signer, wallet, solPK, sourceAsset]);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
onClick={handleAttestClick}
|
||||
disabled={!isTargetComplete || isSending || isSendComplete}
|
||||
>
|
||||
Attest
|
||||
</Button>
|
||||
{isSending ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Send;
|
||||
|
|
|
@ -1,85 +1,85 @@
|
|||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectAttestIsSourceComplete,
|
||||
selectAttestShouldLockFields,
|
||||
selectAttestSourceAsset,
|
||||
selectAttestSourceChain,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
incrementStep,
|
||||
setSourceAsset,
|
||||
setSourceChain,
|
||||
} from "../../store/attestSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferField: {
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Source() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectAttestSourceChain);
|
||||
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
|
||||
const shouldLockFields = useSelector(selectAttestShouldLockFields);
|
||||
const handleSourceChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleAssetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceAsset(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={sourceChain}
|
||||
onChange={handleSourceChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{CHAINS.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<KeyAndBalance chainId={sourceChain} />
|
||||
<TextField
|
||||
placeholder="Asset"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={sourceAsset}
|
||||
onChange={handleAssetChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Source;
|
||||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectAttestIsSourceComplete,
|
||||
selectAttestShouldLockFields,
|
||||
selectAttestSourceAsset,
|
||||
selectAttestSourceChain,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
incrementStep,
|
||||
setSourceAsset,
|
||||
setSourceChain,
|
||||
} from "../../store/attestSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferField: {
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Source() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectAttestSourceChain);
|
||||
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||
const isSourceComplete = useSelector(selectAttestIsSourceComplete);
|
||||
const shouldLockFields = useSelector(selectAttestShouldLockFields);
|
||||
const handleSourceChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleAssetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceAsset(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={sourceChain}
|
||||
onChange={handleSourceChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{CHAINS.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<KeyAndBalance chainId={sourceChain} />
|
||||
<TextField
|
||||
placeholder="Asset"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={sourceAsset}
|
||||
onChange={handleAssetChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Source;
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
import { Button, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectAttestIsTargetComplete,
|
||||
selectAttestShouldLockFields,
|
||||
selectAttestSourceChain,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/attestSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Target() {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectAttestSourceChain);
|
||||
const chains = useMemo(
|
||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||
[sourceChain]
|
||||
);
|
||||
const targetChain = useSelector(selectAttestTargetChain);
|
||||
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
|
||||
const shouldLockFields = useSelector(selectAttestShouldLockFields);
|
||||
const handleTargetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setTargetChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={targetChain}
|
||||
onChange={handleTargetChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{chains.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{/* TODO: determine "to" token address */}
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<Button
|
||||
disabled={!isTargetComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Target;
|
||||
import { Button, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectAttestIsTargetComplete,
|
||||
selectAttestShouldLockFields,
|
||||
selectAttestSourceChain,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/attestSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Target() {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectAttestSourceChain);
|
||||
const chains = useMemo(
|
||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||
[sourceChain]
|
||||
);
|
||||
const targetChain = useSelector(selectAttestTargetChain);
|
||||
const isTargetComplete = useSelector(selectAttestIsTargetComplete);
|
||||
const shouldLockFields = useSelector(selectAttestShouldLockFields);
|
||||
const handleTargetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setTargetChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={targetChain}
|
||||
onChange={handleTargetChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{chains.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{/* TODO: determine "to" token address */}
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<Button
|
||||
disabled={!isTargetComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Target;
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
import {
|
||||
Container,
|
||||
Step,
|
||||
StepButton,
|
||||
StepContent,
|
||||
Stepper,
|
||||
} from "@material-ui/core";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
||||
import {
|
||||
selectAttestActiveStep,
|
||||
selectAttestSignedVAAHex,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/attestSlice";
|
||||
import Create from "./Create";
|
||||
import Send from "./Send";
|
||||
import Source from "./Source";
|
||||
import Target from "./Target";
|
||||
|
||||
// TODO: ensure that both wallets are connected to the same known network
|
||||
|
||||
function Attest() {
|
||||
useGetBalanceEffect();
|
||||
const dispatch = useDispatch();
|
||||
const activeStep = useSelector(selectAttestActiveStep);
|
||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Stepper activeStep={activeStep} orientation="vertical">
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
||||
Select a source
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Source />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(1))}>
|
||||
Select a target
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Target />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(2))}>
|
||||
Send attestation
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Send />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton
|
||||
onClick={() => dispatch(setStep(3))}
|
||||
disabled={!signedVAAHex}
|
||||
>
|
||||
Create wrapper
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Create />
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Attest;
|
||||
import {
|
||||
Container,
|
||||
Step,
|
||||
StepButton,
|
||||
StepContent,
|
||||
Stepper,
|
||||
} from "@material-ui/core";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
||||
import {
|
||||
selectAttestActiveStep,
|
||||
selectAttestSignedVAAHex,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/attestSlice";
|
||||
import Create from "./Create";
|
||||
import Send from "./Send";
|
||||
import Source from "./Source";
|
||||
import Target from "./Target";
|
||||
|
||||
// TODO: ensure that both wallets are connected to the same known network
|
||||
|
||||
function Attest() {
|
||||
useGetBalanceEffect();
|
||||
const dispatch = useDispatch();
|
||||
const activeStep = useSelector(selectAttestActiveStep);
|
||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Stepper activeStep={activeStep} orientation="vertical">
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
||||
Select a source
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Source />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(1))}>
|
||||
Select a target
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Target />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(2))}>
|
||||
Send attestation
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Send />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton
|
||||
onClick={() => dispatch(setStep(3))}
|
||||
disabled={!signedVAAHex}
|
||||
>
|
||||
Create wrapper
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Create />
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Attest;
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import ToggleConnectedButton from "./ToggleConnectedButton";
|
||||
|
||||
const EthereumSignerKey = () => {
|
||||
const { connect, disconnect, signerAddress, providerError } =
|
||||
useEthereumProvider();
|
||||
return (
|
||||
<>
|
||||
<ToggleConnectedButton
|
||||
connect={connect}
|
||||
disconnect={disconnect}
|
||||
connected={!!signerAddress}
|
||||
pk={signerAddress || ""}
|
||||
/>
|
||||
{providerError ? (
|
||||
<Typography variant="body2" color="error">
|
||||
{providerError}
|
||||
</Typography>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EthereumSignerKey;
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import ToggleConnectedButton from "./ToggleConnectedButton";
|
||||
|
||||
const EthereumSignerKey = () => {
|
||||
const { connect, disconnect, signerAddress, providerError } =
|
||||
useEthereumProvider();
|
||||
return (
|
||||
<>
|
||||
<ToggleConnectedButton
|
||||
connect={connect}
|
||||
disconnect={disconnect}
|
||||
connected={!!signerAddress}
|
||||
pk={signerAddress || ""}
|
||||
/>
|
||||
{providerError ? (
|
||||
<Typography variant="body2" color="error">
|
||||
{providerError}
|
||||
</Typography>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EthereumSignerKey;
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||
import EthereumSignerKey from "./EthereumSignerKey";
|
||||
import SolanaWalletKey from "./SolanaWalletKey";
|
||||
|
||||
function KeyAndBalance({
|
||||
chainId,
|
||||
balance,
|
||||
}: {
|
||||
chainId: ChainId;
|
||||
balance?: string;
|
||||
}) {
|
||||
if (chainId === CHAIN_ID_ETH) {
|
||||
return (
|
||||
<>
|
||||
<EthereumSignerKey />
|
||||
<Typography>{balance}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (chainId === CHAIN_ID_SOLANA) {
|
||||
return (
|
||||
<>
|
||||
<SolanaWalletKey />
|
||||
<Typography>{balance}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default KeyAndBalance;
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||
import EthereumSignerKey from "./EthereumSignerKey";
|
||||
import SolanaWalletKey from "./SolanaWalletKey";
|
||||
|
||||
function KeyAndBalance({
|
||||
chainId,
|
||||
balance,
|
||||
}: {
|
||||
chainId: ChainId;
|
||||
balance?: string;
|
||||
}) {
|
||||
if (chainId === CHAIN_ID_ETH) {
|
||||
return (
|
||||
<>
|
||||
<EthereumSignerKey />
|
||||
<Typography>{balance}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (chainId === CHAIN_ID_SOLANA) {
|
||||
return (
|
||||
<>
|
||||
<SolanaWalletKey />
|
||||
<Typography>{balance}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default KeyAndBalance;
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import ToggleConnectedButton from "./ToggleConnectedButton";
|
||||
|
||||
const SolanaWalletKey = () => {
|
||||
const { connect, disconnect, connected, wallet } = useSolanaWallet();
|
||||
const pk = wallet?.publicKey?.toString() || "";
|
||||
return (
|
||||
<ToggleConnectedButton
|
||||
connect={connect}
|
||||
disconnect={disconnect}
|
||||
connected={connected}
|
||||
pk={pk}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SolanaWalletKey;
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import ToggleConnectedButton from "./ToggleConnectedButton";
|
||||
|
||||
const SolanaWalletKey = () => {
|
||||
const { connect, disconnect, connected, wallet } = useSolanaWallet();
|
||||
const pk = wallet?.publicKey?.toString() || "";
|
||||
return (
|
||||
<ToggleConnectedButton
|
||||
connect={connect}
|
||||
disconnect={disconnect}
|
||||
connected={connected}
|
||||
pk={pk}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SolanaWalletKey;
|
||||
|
|
|
@ -1,51 +1,51 @@
|
|||
import { Button, makeStyles, Tooltip } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
button: {
|
||||
display: "block",
|
||||
margin: `${theme.spacing(1)}px auto`,
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
},
|
||||
}));
|
||||
|
||||
const ToggleConnectedButton = ({
|
||||
connect,
|
||||
disconnect,
|
||||
connected,
|
||||
pk,
|
||||
}: {
|
||||
connect(): any;
|
||||
disconnect(): any;
|
||||
connected: boolean;
|
||||
pk: string;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const is0x = pk.startsWith("0x");
|
||||
return connected ? (
|
||||
<Tooltip title={pk}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={disconnect}
|
||||
className={classes.button}
|
||||
>
|
||||
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
|
||||
{pk.substr(pk.length - (is0x ? 4 : 3))}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={connect}
|
||||
className={classes.button}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleConnectedButton;
|
||||
import { Button, makeStyles, Tooltip } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
button: {
|
||||
display: "block",
|
||||
margin: `${theme.spacing(1)}px auto`,
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
},
|
||||
}));
|
||||
|
||||
const ToggleConnectedButton = ({
|
||||
connect,
|
||||
disconnect,
|
||||
connected,
|
||||
pk,
|
||||
}: {
|
||||
connect(): any;
|
||||
disconnect(): any;
|
||||
connected: boolean;
|
||||
pk: string;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const is0x = pk.startsWith("0x");
|
||||
return connected ? (
|
||||
<Tooltip title={pk}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={disconnect}
|
||||
className={classes.button}
|
||||
>
|
||||
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
|
||||
{pk.substr(pk.length - (is0x ? 4 : 3))}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={connect}
|
||||
className={classes.button}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleConnectedButton;
|
||||
|
|
|
@ -1,67 +1,78 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
|
||||
import {
|
||||
selectTransferIsRedeeming,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { setIsRedeeming } from "../../store/transferSlice";
|
||||
import { CHAIN_ID_ETH } from "../../utils/consts";
|
||||
import redeemOn, { redeemOnEth } from "../../utils/redeemOn";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
function Redeem() {
|
||||
const dispatch = useDispatch();
|
||||
const classes = useStyles();
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const { provider, signer } = useEthereumProvider();
|
||||
const signedVAA = useTransferSignedVAA();
|
||||
const isRedeeming = useSelector(selectTransferIsRedeeming);
|
||||
const handleRedeemClick = useCallback(() => {
|
||||
if (
|
||||
targetChain === CHAIN_ID_ETH &&
|
||||
redeemOn[targetChain] === redeemOnEth &&
|
||||
signedVAA
|
||||
) {
|
||||
dispatch(setIsRedeeming(true));
|
||||
redeemOnEth(provider, signer, signedVAA);
|
||||
}
|
||||
}, [dispatch, targetChain, provider, signer, signedVAA]);
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
disabled={isRedeeming}
|
||||
onClick={handleRedeemClick}
|
||||
>
|
||||
Redeem
|
||||
</Button>
|
||||
{isRedeeming ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Redeem;
|
||||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import useTransferSignedVAA from "../../hooks/useTransferSignedVAA";
|
||||
import {
|
||||
selectTransferIsRedeeming,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { setIsRedeeming } from "../../store/transferSlice";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
import redeemOn, { redeemOnEth, redeemOnSolana } from "../../utils/redeemOn";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
function Redeem() {
|
||||
const dispatch = useDispatch();
|
||||
const classes = useStyles();
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const { provider, signer } = useEthereumProvider();
|
||||
const signedVAA = useTransferSignedVAA();
|
||||
const isRedeeming = useSelector(selectTransferIsRedeeming);
|
||||
const handleRedeemClick = useCallback(() => {
|
||||
if (
|
||||
targetChain === CHAIN_ID_ETH &&
|
||||
redeemOn[targetChain] === redeemOnEth &&
|
||||
signedVAA
|
||||
) {
|
||||
dispatch(setIsRedeeming(true));
|
||||
redeemOnEth(provider, signer, signedVAA);
|
||||
}
|
||||
if (
|
||||
targetChain === CHAIN_ID_SOLANA &&
|
||||
redeemOn[targetChain] === redeemOnSolana &&
|
||||
signedVAA
|
||||
) {
|
||||
dispatch(setIsRedeeming(true));
|
||||
redeemOnSolana(wallet, solPK?.toString(), signedVAA);
|
||||
}
|
||||
}, [dispatch, targetChain, provider, signer, signedVAA]);
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
disabled={isRedeeming}
|
||||
onClick={handleRedeemClick}
|
||||
>
|
||||
Redeem
|
||||
</Button>
|
||||
{isRedeeming ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Redeem;
|
||||
|
|
|
@ -1,164 +1,164 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
import useWrappedAsset from "../../hooks/useWrappedAsset";
|
||||
import {
|
||||
selectTransferAmount,
|
||||
selectTransferIsSendComplete,
|
||||
selectTransferIsSending,
|
||||
selectTransferIsTargetComplete,
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceParsedTokenAccount,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
|
||||
import { uint8ArrayToHex } from "../../utils/array";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
import transferFrom, {
|
||||
transferFromEth,
|
||||
transferFromSolana,
|
||||
} from "../../utils/transferFrom";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: move attest to its own workflow
|
||||
|
||||
function Send() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const amount = useSelector(selectTransferAmount);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||
const isSending = useSelector(selectTransferIsSending);
|
||||
const isSendComplete = useSelector(selectTransferIsSendComplete);
|
||||
const { provider, signer, signerAddress } = useEthereumProvider();
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const sourceParsedTokenAccount = useSelector(
|
||||
selectTransferSourceParsedTokenAccount
|
||||
);
|
||||
const tokenPK = sourceParsedTokenAccount?.publicKey;
|
||||
const decimals = sourceParsedTokenAccount?.decimals;
|
||||
const {
|
||||
isLoading: isCheckingWrapped,
|
||||
// isWrapped,
|
||||
wrappedAsset,
|
||||
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
|
||||
// TODO: check this and send to separate flow
|
||||
const isWrapped = true;
|
||||
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
|
||||
// TODO: dynamically get "to" wallet
|
||||
const handleTransferClick = useCallback(() => {
|
||||
// TODO: we should separate state for transaction vs fetching vaa
|
||||
// TODO: more generic way of calling these
|
||||
if (transferFrom[sourceChain]) {
|
||||
if (
|
||||
sourceChain === CHAIN_ID_ETH &&
|
||||
transferFrom[sourceChain] === transferFromEth &&
|
||||
decimals
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await transferFromEth(
|
||||
provider,
|
||||
signer,
|
||||
sourceAsset,
|
||||
decimals,
|
||||
amount,
|
||||
targetChain,
|
||||
solPK?.toBytes()
|
||||
);
|
||||
console.log("bytes in transfer", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (
|
||||
sourceChain === CHAIN_ID_SOLANA &&
|
||||
transferFrom[sourceChain] === transferFromSolana &&
|
||||
decimals
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await transferFromSolana(
|
||||
wallet,
|
||||
solPK?.toString(),
|
||||
tokenPK,
|
||||
sourceAsset,
|
||||
amount,
|
||||
decimals,
|
||||
signerAddress,
|
||||
targetChain
|
||||
);
|
||||
console.log("bytes in transfer", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
sourceChain,
|
||||
provider,
|
||||
signer,
|
||||
signerAddress,
|
||||
wallet,
|
||||
solPK,
|
||||
tokenPK,
|
||||
sourceAsset,
|
||||
amount,
|
||||
decimals,
|
||||
targetChain,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
onClick={handleTransferClick}
|
||||
disabled={!isTargetComplete || isSending || isSendComplete}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
{isSending ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Send;
|
||||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../../contexts/SolanaWalletContext";
|
||||
import useWrappedAsset from "../../hooks/useWrappedAsset";
|
||||
import {
|
||||
selectTransferAmount,
|
||||
selectTransferIsSendComplete,
|
||||
selectTransferIsSending,
|
||||
selectTransferIsTargetComplete,
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceParsedTokenAccount,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
|
||||
import { uint8ArrayToHex } from "../../utils/array";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../../utils/consts";
|
||||
import transferFrom, {
|
||||
transferFromEth,
|
||||
transferFromSolana,
|
||||
} from "../../utils/transferFrom";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: move attest to its own workflow
|
||||
|
||||
function Send() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const amount = useSelector(selectTransferAmount);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||
const isSending = useSelector(selectTransferIsSending);
|
||||
const isSendComplete = useSelector(selectTransferIsSendComplete);
|
||||
const { provider, signer, signerAddress } = useEthereumProvider();
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const sourceParsedTokenAccount = useSelector(
|
||||
selectTransferSourceParsedTokenAccount
|
||||
);
|
||||
const tokenPK = sourceParsedTokenAccount?.publicKey;
|
||||
const decimals = sourceParsedTokenAccount?.decimals;
|
||||
const {
|
||||
isLoading: isCheckingWrapped,
|
||||
// isWrapped,
|
||||
wrappedAsset,
|
||||
} = useWrappedAsset(targetChain, sourceChain, sourceAsset, provider);
|
||||
// TODO: check this and send to separate flow
|
||||
const isWrapped = true;
|
||||
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
|
||||
// TODO: dynamically get "to" wallet
|
||||
const handleTransferClick = useCallback(() => {
|
||||
// TODO: we should separate state for transaction vs fetching vaa
|
||||
// TODO: more generic way of calling these
|
||||
if (transferFrom[sourceChain]) {
|
||||
if (
|
||||
sourceChain === CHAIN_ID_ETH &&
|
||||
transferFrom[sourceChain] === transferFromEth &&
|
||||
decimals
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await transferFromEth(
|
||||
provider,
|
||||
signer,
|
||||
sourceAsset,
|
||||
decimals,
|
||||
amount,
|
||||
targetChain,
|
||||
solPK?.toBytes()
|
||||
);
|
||||
console.log("bytes in transfer", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (
|
||||
sourceChain === CHAIN_ID_SOLANA &&
|
||||
transferFrom[sourceChain] === transferFromSolana &&
|
||||
decimals
|
||||
) {
|
||||
//TODO: just for testing, this should eventually use the store to communicate between steps
|
||||
(async () => {
|
||||
dispatch(setIsSending(true));
|
||||
try {
|
||||
const vaaBytes = await transferFromSolana(
|
||||
wallet,
|
||||
solPK?.toString(),
|
||||
tokenPK,
|
||||
sourceAsset,
|
||||
amount,
|
||||
decimals,
|
||||
signerAddress,
|
||||
targetChain
|
||||
);
|
||||
console.log("bytes in transfer", vaaBytes);
|
||||
vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(setIsSending(false));
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
sourceChain,
|
||||
provider,
|
||||
signer,
|
||||
signerAddress,
|
||||
wallet,
|
||||
solPK,
|
||||
tokenPK,
|
||||
sourceAsset,
|
||||
amount,
|
||||
decimals,
|
||||
targetChain,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.transferButton}
|
||||
onClick={handleTransferClick}
|
||||
disabled={!isTargetComplete || isSending || isSendComplete}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
{isSending ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Send;
|
||||
|
|
|
@ -1,105 +1,105 @@
|
|||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectTransferAmount,
|
||||
selectTransferIsSourceComplete,
|
||||
selectTransferShouldLockFields,
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceBalanceString,
|
||||
selectTransferSourceChain,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
incrementStep,
|
||||
setAmount,
|
||||
setSourceAsset,
|
||||
setSourceChain,
|
||||
} from "../../store/transferSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferField: {
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Source() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const uiAmountString = useSelector(selectTransferSourceBalanceString);
|
||||
const amount = useSelector(selectTransferAmount);
|
||||
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
|
||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
const handleSourceChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleAssetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceAsset(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleAmountChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setAmount(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={sourceChain}
|
||||
onChange={handleSourceChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{CHAINS.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
|
||||
<TextField
|
||||
placeholder="Asset"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={sourceAsset}
|
||||
onChange={handleAssetChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Amount"
|
||||
type="number"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={amount}
|
||||
onChange={handleAmountChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Source;
|
||||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectTransferAmount,
|
||||
selectTransferIsSourceComplete,
|
||||
selectTransferShouldLockFields,
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceBalanceString,
|
||||
selectTransferSourceChain,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
incrementStep,
|
||||
setAmount,
|
||||
setSourceAsset,
|
||||
setSourceChain,
|
||||
} from "../../store/transferSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
transferField: {
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Source() {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const uiAmountString = useSelector(selectTransferSourceBalanceString);
|
||||
const amount = useSelector(selectTransferAmount);
|
||||
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
|
||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
const handleSourceChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleAssetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceAsset(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleAmountChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setAmount(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={sourceChain}
|
||||
onChange={handleSourceChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{CHAINS.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
|
||||
<TextField
|
||||
placeholder="Asset"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={sourceAsset}
|
||||
onChange={handleAssetChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Amount"
|
||||
type="number"
|
||||
fullWidth
|
||||
className={classes.transferField}
|
||||
value={amount}
|
||||
onChange={handleAmountChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Source;
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
import { Button, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectTransferIsTargetComplete,
|
||||
selectTransferShouldLockFields,
|
||||
selectTransferSourceChain,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Target() {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const chains = useMemo(
|
||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||
[sourceChain]
|
||||
);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
const handleTargetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setTargetChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={targetChain}
|
||||
onChange={handleTargetChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{chains.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{/* TODO: determine "to" token address */}
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<Button
|
||||
disabled={!isTargetComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Target;
|
||||
import { Button, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectTransferIsTargetComplete,
|
||||
selectTransferShouldLockFields,
|
||||
selectTransferSourceChain,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Target() {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const chains = useMemo(
|
||||
() => CHAINS.filter((c) => c.id !== sourceChain),
|
||||
[sourceChain]
|
||||
);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
const handleTargetChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setTargetChain(event.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
value={targetChain}
|
||||
onChange={handleTargetChange}
|
||||
disabled={shouldLockFields}
|
||||
>
|
||||
{chains.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{/* TODO: determine "to" token address */}
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<Button
|
||||
disabled={!isTargetComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Target;
|
||||
|
|
|
@ -1,73 +1,73 @@
|
|||
import {
|
||||
Container,
|
||||
Step,
|
||||
StepButton,
|
||||
StepContent,
|
||||
Stepper,
|
||||
} from "@material-ui/core";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
||||
import {
|
||||
selectTransferActiveStep,
|
||||
selectTransferSignedVAAHex,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/transferSlice";
|
||||
import Redeem from "./Redeem";
|
||||
import Send from "./Send";
|
||||
import Source from "./Source";
|
||||
import Target from "./Target";
|
||||
|
||||
// TODO: ensure that both wallets are connected to the same known network
|
||||
// TODO: loaders and such, navigation block?
|
||||
// TODO: refresh displayed token amount after transfer somehow, could be resolved by having different components appear
|
||||
// TODO: warn if amount exceeds balance
|
||||
|
||||
function Transfer() {
|
||||
useGetBalanceEffect();
|
||||
const dispatch = useDispatch();
|
||||
const activeStep = useSelector(selectTransferActiveStep);
|
||||
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Stepper activeStep={activeStep} orientation="vertical">
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
||||
Select a source
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Source />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(1))}>
|
||||
Select a target
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Target />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(2))}>
|
||||
Send tokens
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Send />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton
|
||||
onClick={() => dispatch(setStep(3))}
|
||||
disabled={!signedVAAHex}
|
||||
>
|
||||
Redeem tokens
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Redeem />
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Transfer;
|
||||
import {
|
||||
Container,
|
||||
Step,
|
||||
StepButton,
|
||||
StepContent,
|
||||
Stepper,
|
||||
} from "@material-ui/core";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useGetBalanceEffect from "../../hooks/useGetBalanceEffect";
|
||||
import {
|
||||
selectTransferActiveStep,
|
||||
selectTransferSignedVAAHex,
|
||||
} from "../../store/selectors";
|
||||
import { setStep } from "../../store/transferSlice";
|
||||
import Redeem from "./Redeem";
|
||||
import Send from "./Send";
|
||||
import Source from "./Source";
|
||||
import Target from "./Target";
|
||||
|
||||
// TODO: ensure that both wallets are connected to the same known network
|
||||
// TODO: loaders and such, navigation block?
|
||||
// TODO: refresh displayed token amount after transfer somehow, could be resolved by having different components appear
|
||||
// TODO: warn if amount exceeds balance
|
||||
|
||||
function Transfer() {
|
||||
useGetBalanceEffect();
|
||||
const dispatch = useDispatch();
|
||||
const activeStep = useSelector(selectTransferActiveStep);
|
||||
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Stepper activeStep={activeStep} orientation="vertical">
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
||||
Select a source
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Source />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(1))}>
|
||||
Select a target
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Target />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton onClick={() => dispatch(setStep(2))}>
|
||||
Send tokens
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Send />
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepButton
|
||||
onClick={() => dispatch(setStep(3))}
|
||||
disabled={!signedVAAHex}
|
||||
>
|
||||
Redeem tokens
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Redeem />
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Transfer;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectAttestSignedVAAHex } from "../store/selectors";
|
||||
import { hexToUint8Array } from "../utils/array";
|
||||
|
||||
export default function useAttestSignedVAA() {
|
||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||
const signedVAA = useMemo(
|
||||
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
|
||||
[signedVAAHex]
|
||||
);
|
||||
return signedVAA;
|
||||
}
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectAttestSignedVAAHex } from "../store/selectors";
|
||||
import { hexToUint8Array } from "../utils/array";
|
||||
|
||||
export default function useAttestSignedVAA() {
|
||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||
const signedVAA = useMemo(
|
||||
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
|
||||
[signedVAAHex]
|
||||
);
|
||||
return signedVAA;
|
||||
}
|
||||
|
|
|
@ -1,115 +1,115 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { formatUnits } from "ethers/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { TokenImplementation__factory } from "../ethers-contracts";
|
||||
import {
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceChain,
|
||||
} from "../store/selectors";
|
||||
import { setSourceParsedTokenAccount } from "../store/transferSlice";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, SOLANA_HOST } from "../utils/consts";
|
||||
|
||||
function createParsedTokenAccount(
|
||||
publicKey: PublicKey | undefined,
|
||||
amount: string,
|
||||
decimals: number,
|
||||
uiAmount: number,
|
||||
uiAmountString: string
|
||||
) {
|
||||
return {
|
||||
publicKey: publicKey?.toString(),
|
||||
amount,
|
||||
decimals,
|
||||
uiAmount,
|
||||
uiAmountString,
|
||||
};
|
||||
}
|
||||
|
||||
function useGetBalanceEffect() {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const { provider, signerAddress } = useEthereumProvider();
|
||||
useEffect(() => {
|
||||
// TODO: loading state
|
||||
dispatch(setSourceParsedTokenAccount(undefined));
|
||||
if (!sourceAsset) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
if (sourceChain === CHAIN_ID_SOLANA && solPK) {
|
||||
let mint;
|
||||
try {
|
||||
mint = new PublicKey(sourceAsset);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||
connection
|
||||
.getParsedTokenAccountsByOwner(solPK, { mint })
|
||||
.then(({ value }) => {
|
||||
if (!cancelled) {
|
||||
if (value.length) {
|
||||
dispatch(
|
||||
setSourceParsedTokenAccount(
|
||||
createParsedTokenAccount(
|
||||
value[0].pubkey,
|
||||
value[0].account.data.parsed?.info?.tokenAmount?.amount,
|
||||
value[0].account.data.parsed?.info?.tokenAmount?.decimals,
|
||||
value[0].account.data.parsed?.info?.tokenAmount?.uiAmount,
|
||||
value[0].account.data.parsed?.info?.tokenAmount
|
||||
?.uiAmountString
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// TODO: error state
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
// TODO: error state
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sourceChain === CHAIN_ID_ETH && provider && signerAddress) {
|
||||
const token = TokenImplementation__factory.connect(sourceAsset, provider);
|
||||
token
|
||||
.decimals()
|
||||
.then((decimals) => {
|
||||
token.balanceOf(signerAddress).then((n) => {
|
||||
if (!cancelled) {
|
||||
dispatch(
|
||||
setSourceParsedTokenAccount(
|
||||
// TODO: verify accuracy
|
||||
createParsedTokenAccount(
|
||||
undefined,
|
||||
n.toString(),
|
||||
decimals,
|
||||
Number(formatUnits(n, decimals)),
|
||||
formatUnits(n, decimals)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
// TODO: error state
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [dispatch, sourceChain, sourceAsset, solPK, provider, signerAddress]);
|
||||
}
|
||||
|
||||
export default useGetBalanceEffect;
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { formatUnits } from "ethers/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { TokenImplementation__factory } from "../ethers-contracts";
|
||||
import {
|
||||
selectTransferSourceAsset,
|
||||
selectTransferSourceChain,
|
||||
} from "../store/selectors";
|
||||
import { setSourceParsedTokenAccount } from "../store/transferSlice";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, SOLANA_HOST } from "../utils/consts";
|
||||
|
||||
function createParsedTokenAccount(
|
||||
publicKey: PublicKey | undefined,
|
||||
amount: string,
|
||||
decimals: number,
|
||||
uiAmount: number,
|
||||
uiAmountString: string
|
||||
) {
|
||||
return {
|
||||
publicKey: publicKey?.toString(),
|
||||
amount,
|
||||
decimals,
|
||||
uiAmount,
|
||||
uiAmountString,
|
||||
};
|
||||
}
|
||||
|
||||
function useGetBalanceEffect() {
|
||||
const dispatch = useDispatch();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||
const { wallet } = useSolanaWallet();
|
||||
const solPK = wallet?.publicKey;
|
||||
const { provider, signerAddress } = useEthereumProvider();
|
||||
useEffect(() => {
|
||||
// TODO: loading state
|
||||
dispatch(setSourceParsedTokenAccount(undefined));
|
||||
if (!sourceAsset) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
if (sourceChain === CHAIN_ID_SOLANA && solPK) {
|
||||
let mint;
|
||||
try {
|
||||
mint = new PublicKey(sourceAsset);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
const connection = new Connection(SOLANA_HOST, "finalized");
|
||||
connection
|
||||
.getParsedTokenAccountsByOwner(solPK, { mint })
|
||||
.then(({ value }) => {
|
||||
if (!cancelled) {
|
||||
if (value.length) {
|
||||
dispatch(
|
||||
setSourceParsedTokenAccount(
|
||||
createParsedTokenAccount(
|
||||
value[0].pubkey,
|
||||
value[0].account.data.parsed?.info?.tokenAmount?.amount,
|
||||
value[0].account.data.parsed?.info?.tokenAmount?.decimals,
|
||||
value[0].account.data.parsed?.info?.tokenAmount?.uiAmount,
|
||||
value[0].account.data.parsed?.info?.tokenAmount
|
||||
?.uiAmountString
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// TODO: error state
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
// TODO: error state
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sourceChain === CHAIN_ID_ETH && provider && signerAddress) {
|
||||
const token = TokenImplementation__factory.connect(sourceAsset, provider);
|
||||
token
|
||||
.decimals()
|
||||
.then((decimals) => {
|
||||
token.balanceOf(signerAddress).then((n) => {
|
||||
if (!cancelled) {
|
||||
dispatch(
|
||||
setSourceParsedTokenAccount(
|
||||
// TODO: verify accuracy
|
||||
createParsedTokenAccount(
|
||||
undefined,
|
||||
n.toString(),
|
||||
decimals,
|
||||
Number(formatUnits(n, decimals)),
|
||||
formatUnits(n, decimals)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
// TODO: error state
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [dispatch, sourceChain, sourceAsset, solPK, provider, signerAddress]);
|
||||
}
|
||||
|
||||
export default useGetBalanceEffect;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectTransferSignedVAAHex } from "../store/selectors";
|
||||
import { hexToUint8Array } from "../utils/array";
|
||||
|
||||
export default function useTransferSignedVAA() {
|
||||
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
|
||||
const signedVAA = useMemo(
|
||||
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
|
||||
[signedVAAHex]
|
||||
);
|
||||
return signedVAA;
|
||||
}
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectTransferSignedVAAHex } from "../store/selectors";
|
||||
import { hexToUint8Array } from "../utils/array";
|
||||
|
||||
export default function useTransferSignedVAA() {
|
||||
const signedVAAHex = useSelector(selectTransferSignedVAAHex);
|
||||
const signedVAA = useMemo(
|
||||
() => (signedVAAHex ? hexToUint8Array(signedVAAHex) : undefined),
|
||||
[signedVAAHex]
|
||||
);
|
||||
return signedVAA;
|
||||
}
|
||||
|
|
|
@ -1,74 +1,74 @@
|
|||
import { ethers } from "ethers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||
import {
|
||||
getAttestedAssetEth,
|
||||
getAttestedAssetSol,
|
||||
} from "../utils/getAttestedAsset";
|
||||
export interface WrappedAssetState {
|
||||
isLoading: boolean;
|
||||
isWrapped: boolean;
|
||||
wrappedAsset: string | null;
|
||||
}
|
||||
|
||||
function useWrappedAsset(
|
||||
checkChain: ChainId,
|
||||
originChain: ChainId,
|
||||
originAsset: string,
|
||||
provider: ethers.providers.Web3Provider | undefined
|
||||
) {
|
||||
const [state, setState] = useState<WrappedAssetState>({
|
||||
isLoading: false,
|
||||
isWrapped: false,
|
||||
wrappedAsset: null,
|
||||
});
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
if (checkChain === CHAIN_ID_ETH && provider) {
|
||||
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
|
||||
const asset = await getAttestedAssetEth(
|
||||
provider,
|
||||
originChain,
|
||||
originAsset
|
||||
);
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
isLoading: false,
|
||||
isWrapped: !!asset && asset !== ethers.constants.AddressZero,
|
||||
wrappedAsset: asset,
|
||||
});
|
||||
}
|
||||
} else if (checkChain === CHAIN_ID_SOLANA) {
|
||||
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
|
||||
try {
|
||||
const asset = await getAttestedAssetSol(originChain, originAsset);
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
isLoading: false,
|
||||
isWrapped: !!asset,
|
||||
wrappedAsset: asset,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
// TODO: warning for this
|
||||
setState({
|
||||
isLoading: false,
|
||||
isWrapped: false,
|
||||
wrappedAsset: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setState({ isLoading: false, isWrapped: false, wrappedAsset: null });
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [checkChain, originChain, originAsset, provider]);
|
||||
return state;
|
||||
}
|
||||
|
||||
export default useWrappedAsset;
|
||||
import { ethers } from "ethers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
|
||||
import {
|
||||
getAttestedAssetEth,
|
||||
getAttestedAssetSol,
|
||||
} from "../utils/getAttestedAsset";
|
||||
export interface WrappedAssetState {
|
||||
isLoading: boolean;
|
||||
isWrapped: boolean;
|
||||
wrappedAsset: string | null;
|
||||
}
|
||||
|
||||
function useWrappedAsset(
|
||||
checkChain: ChainId,
|
||||
originChain: ChainId,
|
||||
originAsset: string,
|
||||
provider: ethers.providers.Web3Provider | undefined
|
||||
) {
|
||||
const [state, setState] = useState<WrappedAssetState>({
|
||||
isLoading: false,
|
||||
isWrapped: false,
|
||||
wrappedAsset: null,
|
||||
});
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
if (checkChain === CHAIN_ID_ETH && provider) {
|
||||
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
|
||||
const asset = await getAttestedAssetEth(
|
||||
provider,
|
||||
originChain,
|
||||
originAsset
|
||||
);
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
isLoading: false,
|
||||
isWrapped: !!asset && asset !== ethers.constants.AddressZero,
|
||||
wrappedAsset: asset,
|
||||
});
|
||||
}
|
||||
} else if (checkChain === CHAIN_ID_SOLANA) {
|
||||
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
|
||||
try {
|
||||
const asset = await getAttestedAssetSol(originChain, originAsset);
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
isLoading: false,
|
||||
isWrapped: !!asset,
|
||||
wrappedAsset: asset,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
// TODO: warning for this
|
||||
setState({
|
||||
isLoading: false,
|
||||
isWrapped: false,
|
||||
wrappedAsset: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setState({ isLoading: false, isWrapped: false, wrappedAsset: null });
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [checkChain, originChain, originAsset, provider]);
|
||||
return state;
|
||||
}
|
||||
|
||||
export default useWrappedAsset;
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
import { createTheme, responsiveFontSizes } from "@material-ui/core";
|
||||
|
||||
export const theme = responsiveFontSizes(
|
||||
createTheme({
|
||||
palette: {
|
||||
type: "dark",
|
||||
background: {
|
||||
default: "#010114",
|
||||
paper: "#010114",
|
||||
},
|
||||
divider: "#4e4e54",
|
||||
primary: {
|
||||
main: "rgba(0, 116, 255, 0.8)", // #0074FF
|
||||
},
|
||||
secondary: {
|
||||
main: "rgb(0,239,216,0.8)", // #00EFD8
|
||||
},
|
||||
error: {
|
||||
main: "#FD3503",
|
||||
},
|
||||
},
|
||||
overrides: {
|
||||
MuiButton: {
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
textTransform: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
import { createTheme, responsiveFontSizes } from "@material-ui/core";
|
||||
|
||||
export const theme = responsiveFontSizes(
|
||||
createTheme({
|
||||
palette: {
|
||||
type: "dark",
|
||||
background: {
|
||||
default: "#010114",
|
||||
paper: "#010114",
|
||||
},
|
||||
divider: "#4e4e54",
|
||||
primary: {
|
||||
main: "rgba(0, 116, 255, 0.8)", // #0074FF
|
||||
},
|
||||
secondary: {
|
||||
main: "rgb(0,239,216,0.8)", // #00EFD8
|
||||
},
|
||||
error: {
|
||||
main: "#FD3503",
|
||||
},
|
||||
},
|
||||
overrides: {
|
||||
MuiButton: {
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
textTransform: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1 +1 @@
|
|||
/// <reference types="react-scripts" />
|
||||
/// <reference types="react-scripts" />
|
||||
|
|
|
@ -1,58 +1,58 @@
|
|||
import {
|
||||
AccountMeta,
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
GrpcWebImpl,
|
||||
PublicrpcClientImpl,
|
||||
} from "../proto/publicrpc/v1/publicrpc";
|
||||
import { ChainId } from "../utils/consts";
|
||||
|
||||
// begin from clients\solana\main.ts
|
||||
export function ixFromRust(data: any): TransactionInstruction {
|
||||
let keys: Array<AccountMeta> = data.accounts.map(accountMetaFromRust);
|
||||
return new TransactionInstruction({
|
||||
programId: new PublicKey(data.program_id),
|
||||
data: Buffer.from(data.data),
|
||||
keys: keys,
|
||||
});
|
||||
}
|
||||
|
||||
function accountMetaFromRust(meta: any): AccountMeta {
|
||||
return {
|
||||
pubkey: new PublicKey(meta.pubkey),
|
||||
isSigner: meta.is_signer,
|
||||
isWritable: meta.is_writable,
|
||||
};
|
||||
}
|
||||
// end from clients\solana\main.ts
|
||||
|
||||
export async function getSignedVAA(
|
||||
emitterChain: ChainId,
|
||||
emitterAddress: string,
|
||||
sequence: string
|
||||
) {
|
||||
const rpc = new GrpcWebImpl("http://localhost:8080", {});
|
||||
const api = new PublicrpcClientImpl(rpc);
|
||||
// TODO: potential infinite loop, support cancellation?
|
||||
let result;
|
||||
while (!result) {
|
||||
console.log("wait 1 second");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
console.log("check for signed vaa", emitterChain, emitterAddress, sequence);
|
||||
try {
|
||||
result = await api.GetSignedVAA({
|
||||
messageId: {
|
||||
emitterChain,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
},
|
||||
});
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
import {
|
||||
AccountMeta,
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
GrpcWebImpl,
|
||||
PublicrpcClientImpl,
|
||||
} from "../proto/publicrpc/v1/publicrpc";
|
||||
import { ChainId } from "../utils/consts";
|
||||
|
||||
// begin from clients\solana\main.ts
|
||||
export function ixFromRust(data: any): TransactionInstruction {
|
||||
let keys: Array<AccountMeta> = data.accounts.map(accountMetaFromRust);
|
||||
return new TransactionInstruction({
|
||||
programId: new PublicKey(data.program_id),
|
||||
data: Buffer.from(data.data),
|
||||
keys: keys,
|
||||
});
|
||||
}
|
||||
|
||||
function accountMetaFromRust(meta: any): AccountMeta {
|
||||
return {
|
||||
pubkey: new PublicKey(meta.pubkey),
|
||||
isSigner: meta.is_signer,
|
||||
isWritable: meta.is_writable,
|
||||
};
|
||||
}
|
||||
// end from clients\solana\main.ts
|
||||
|
||||
export async function getSignedVAA(
|
||||
emitterChain: ChainId,
|
||||
emitterAddress: string,
|
||||
sequence: string
|
||||
) {
|
||||
const rpc = new GrpcWebImpl("http://localhost:8080", {});
|
||||
const api = new PublicrpcClientImpl(rpc);
|
||||
// TODO: potential infinite loop, support cancellation?
|
||||
let result;
|
||||
while (!result) {
|
||||
console.log("wait 1 second");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
console.log("check for signed vaa", emitterChain, emitterAddress, sequence);
|
||||
try {
|
||||
result = await api.GetSignedVAA({
|
||||
messageId: {
|
||||
emitterChain,
|
||||
emitterAddress,
|
||||
sequence,
|
||||
},
|
||||
});
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,105 +1,105 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TEST_TOKEN_ADDRESS,
|
||||
SOL_TEST_TOKEN_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
|
||||
const LAST_STEP = 3;
|
||||
|
||||
type Steps = 0 | 1 | 2 | 3;
|
||||
|
||||
export interface AttestState {
|
||||
activeStep: Steps;
|
||||
sourceChain: ChainId;
|
||||
sourceAsset: string;
|
||||
targetChain: ChainId;
|
||||
signedVAAHex: string | undefined;
|
||||
isSending: boolean;
|
||||
isCreating: boolean;
|
||||
}
|
||||
|
||||
const initialState: AttestState = {
|
||||
activeStep: 0,
|
||||
sourceChain: CHAIN_ID_SOLANA,
|
||||
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
|
||||
targetChain: CHAIN_ID_ETH,
|
||||
signedVAAHex: undefined,
|
||||
isSending: false,
|
||||
isCreating: false,
|
||||
};
|
||||
|
||||
export const attestSlice = createSlice({
|
||||
name: "attest",
|
||||
initialState,
|
||||
reducers: {
|
||||
incrementStep: (state) => {
|
||||
if (state.activeStep < LAST_STEP) state.activeStep++;
|
||||
},
|
||||
decrementStep: (state) => {
|
||||
if (state.activeStep > 0) state.activeStep--;
|
||||
},
|
||||
setStep: (state, action: PayloadAction<Steps>) => {
|
||||
state.activeStep = action.payload;
|
||||
},
|
||||
setSourceChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevSourceChain = state.sourceChain;
|
||||
state.sourceChain = action.payload;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (action.payload === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (action.payload === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === action.payload) {
|
||||
state.targetChain = prevSourceChain;
|
||||
}
|
||||
},
|
||||
setSourceAsset: (state, action: PayloadAction<string>) => {
|
||||
state.sourceAsset = action.payload;
|
||||
},
|
||||
setTargetChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevTargetChain = state.targetChain;
|
||||
state.targetChain = action.payload;
|
||||
if (state.sourceChain === action.payload) {
|
||||
state.sourceChain = prevTargetChain;
|
||||
state.activeStep = 0;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (state.targetChain === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
}
|
||||
},
|
||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||
state.signedVAAHex = action.payload;
|
||||
state.isSending = false;
|
||||
state.activeStep = 3;
|
||||
},
|
||||
setIsSending: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSending = action.payload;
|
||||
},
|
||||
setIsCreating: (state, action: PayloadAction<boolean>) => {
|
||||
state.isCreating = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
incrementStep,
|
||||
decrementStep,
|
||||
setStep,
|
||||
setSourceChain,
|
||||
setSourceAsset,
|
||||
setTargetChain,
|
||||
setSignedVAAHex,
|
||||
setIsSending,
|
||||
setIsCreating,
|
||||
} = attestSlice.actions;
|
||||
|
||||
export default attestSlice.reducer;
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TEST_TOKEN_ADDRESS,
|
||||
SOL_TEST_TOKEN_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
|
||||
const LAST_STEP = 3;
|
||||
|
||||
type Steps = 0 | 1 | 2 | 3;
|
||||
|
||||
export interface AttestState {
|
||||
activeStep: Steps;
|
||||
sourceChain: ChainId;
|
||||
sourceAsset: string;
|
||||
targetChain: ChainId;
|
||||
signedVAAHex: string | undefined;
|
||||
isSending: boolean;
|
||||
isCreating: boolean;
|
||||
}
|
||||
|
||||
const initialState: AttestState = {
|
||||
activeStep: 0,
|
||||
sourceChain: CHAIN_ID_SOLANA,
|
||||
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
|
||||
targetChain: CHAIN_ID_ETH,
|
||||
signedVAAHex: undefined,
|
||||
isSending: false,
|
||||
isCreating: false,
|
||||
};
|
||||
|
||||
export const attestSlice = createSlice({
|
||||
name: "attest",
|
||||
initialState,
|
||||
reducers: {
|
||||
incrementStep: (state) => {
|
||||
if (state.activeStep < LAST_STEP) state.activeStep++;
|
||||
},
|
||||
decrementStep: (state) => {
|
||||
if (state.activeStep > 0) state.activeStep--;
|
||||
},
|
||||
setStep: (state, action: PayloadAction<Steps>) => {
|
||||
state.activeStep = action.payload;
|
||||
},
|
||||
setSourceChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevSourceChain = state.sourceChain;
|
||||
state.sourceChain = action.payload;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (action.payload === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (action.payload === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === action.payload) {
|
||||
state.targetChain = prevSourceChain;
|
||||
}
|
||||
},
|
||||
setSourceAsset: (state, action: PayloadAction<string>) => {
|
||||
state.sourceAsset = action.payload;
|
||||
},
|
||||
setTargetChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevTargetChain = state.targetChain;
|
||||
state.targetChain = action.payload;
|
||||
if (state.sourceChain === action.payload) {
|
||||
state.sourceChain = prevTargetChain;
|
||||
state.activeStep = 0;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (state.targetChain === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
}
|
||||
},
|
||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||
state.signedVAAHex = action.payload;
|
||||
state.isSending = false;
|
||||
state.activeStep = 3;
|
||||
},
|
||||
setIsSending: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSending = action.payload;
|
||||
},
|
||||
setIsCreating: (state, action: PayloadAction<boolean>) => {
|
||||
state.isCreating = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
incrementStep,
|
||||
decrementStep,
|
||||
setStep,
|
||||
setSourceChain,
|
||||
setSourceAsset,
|
||||
setTargetChain,
|
||||
setSignedVAAHex,
|
||||
setIsSending,
|
||||
setIsCreating,
|
||||
} = attestSlice.actions;
|
||||
|
||||
export default attestSlice.reducer;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import attestReducer from "./attestSlice";
|
||||
import transferReducer from "./transferSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
attest: attestReducer,
|
||||
transfer: transferReducer,
|
||||
},
|
||||
});
|
||||
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import attestReducer from "./attestSlice";
|
||||
import transferReducer from "./transferSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
attest: attestReducer,
|
||||
transfer: transferReducer,
|
||||
},
|
||||
});
|
||||
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
|
|
@ -1,86 +1,86 @@
|
|||
import { parseUnits } from "ethers/lib/utils";
|
||||
import { RootState } from ".";
|
||||
import { CHAIN_ID_SOLANA } from "../utils/consts";
|
||||
|
||||
/*
|
||||
* Attest
|
||||
*/
|
||||
|
||||
export const selectAttestActiveStep = (state: RootState) =>
|
||||
state.attest.activeStep;
|
||||
export const selectAttestSourceChain = (state: RootState) =>
|
||||
state.attest.sourceChain;
|
||||
export const selectAttestSourceAsset = (state: RootState) =>
|
||||
state.attest.sourceAsset;
|
||||
export const selectAttestTargetChain = (state: RootState) =>
|
||||
state.attest.targetChain;
|
||||
export const selectAttestSignedVAAHex = (state: RootState) =>
|
||||
state.attest.signedVAAHex;
|
||||
export const selectAttestIsSending = (state: RootState) =>
|
||||
state.attest.isSending;
|
||||
export const selectAttestIsCreating = (state: RootState) =>
|
||||
state.attest.isCreating;
|
||||
|
||||
// safety checks
|
||||
// TODO: could make this return a string with a user informative message
|
||||
export const selectAttestIsSourceComplete = (state: RootState) =>
|
||||
!!state.attest.sourceChain && !!state.attest.sourceAsset;
|
||||
// TODO: check wrapped asset exists or is native attest
|
||||
export const selectAttestIsTargetComplete = (state: RootState) =>
|
||||
selectAttestIsSourceComplete(state) && !!state.attest.targetChain;
|
||||
export const selectAttestIsSendComplete = (state: RootState) =>
|
||||
!!selectAttestSignedVAAHex(state);
|
||||
export const selectAttestShouldLockFields = (state: RootState) =>
|
||||
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
|
||||
|
||||
/*
|
||||
* Transfer
|
||||
*/
|
||||
|
||||
export const selectTransferActiveStep = (state: RootState) =>
|
||||
state.transfer.activeStep;
|
||||
export const selectTransferSourceChain = (state: RootState) =>
|
||||
state.transfer.sourceChain;
|
||||
export const selectTransferSourceAsset = (state: RootState) =>
|
||||
state.transfer.sourceAsset;
|
||||
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
||||
state.transfer.sourceParsedTokenAccount;
|
||||
export const selectTransferSourceBalanceString = (state: RootState) =>
|
||||
state.transfer.sourceParsedTokenAccount?.uiAmountString || "";
|
||||
export const selectTransferAmount = (state: RootState) => state.transfer.amount;
|
||||
export const selectTransferTargetChain = (state: RootState) =>
|
||||
state.transfer.targetChain;
|
||||
export const selectTransferSignedVAAHex = (state: RootState) =>
|
||||
state.transfer.signedVAAHex;
|
||||
export const selectTransferIsSending = (state: RootState) =>
|
||||
state.transfer.isSending;
|
||||
export const selectTransferIsRedeeming = (state: RootState) =>
|
||||
state.transfer.isRedeeming;
|
||||
|
||||
// safety checks
|
||||
// TODO: could make this return a string with a user informative message
|
||||
export const selectTransferIsSourceComplete = (state: RootState) =>
|
||||
!!state.transfer.sourceChain &&
|
||||
!!state.transfer.sourceAsset &&
|
||||
!!state.transfer.sourceParsedTokenAccount &&
|
||||
!!state.transfer.amount &&
|
||||
(state.transfer.sourceChain !== CHAIN_ID_SOLANA ||
|
||||
!!state.transfer.sourceParsedTokenAccount.publicKey) &&
|
||||
!!state.transfer.sourceParsedTokenAccount.uiAmountString &&
|
||||
// TODO: make safe with too many decimals
|
||||
parseUnits(
|
||||
state.transfer.amount,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
).lte(
|
||||
parseUnits(
|
||||
state.transfer.sourceParsedTokenAccount.uiAmountString,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
)
|
||||
);
|
||||
// TODO: check wrapped asset exists or is native transfer
|
||||
export const selectTransferIsTargetComplete = (state: RootState) =>
|
||||
selectTransferIsSourceComplete(state) && !!state.transfer.targetChain;
|
||||
export const selectTransferIsSendComplete = (state: RootState) =>
|
||||
!!selectTransferSignedVAAHex(state);
|
||||
export const selectTransferShouldLockFields = (state: RootState) =>
|
||||
selectTransferIsSending(state) || selectTransferIsSendComplete(state);
|
||||
import { parseUnits } from "ethers/lib/utils";
|
||||
import { RootState } from ".";
|
||||
import { CHAIN_ID_SOLANA } from "../utils/consts";
|
||||
|
||||
/*
|
||||
* Attest
|
||||
*/
|
||||
|
||||
export const selectAttestActiveStep = (state: RootState) =>
|
||||
state.attest.activeStep;
|
||||
export const selectAttestSourceChain = (state: RootState) =>
|
||||
state.attest.sourceChain;
|
||||
export const selectAttestSourceAsset = (state: RootState) =>
|
||||
state.attest.sourceAsset;
|
||||
export const selectAttestTargetChain = (state: RootState) =>
|
||||
state.attest.targetChain;
|
||||
export const selectAttestSignedVAAHex = (state: RootState) =>
|
||||
state.attest.signedVAAHex;
|
||||
export const selectAttestIsSending = (state: RootState) =>
|
||||
state.attest.isSending;
|
||||
export const selectAttestIsCreating = (state: RootState) =>
|
||||
state.attest.isCreating;
|
||||
|
||||
// safety checks
|
||||
// TODO: could make this return a string with a user informative message
|
||||
export const selectAttestIsSourceComplete = (state: RootState) =>
|
||||
!!state.attest.sourceChain && !!state.attest.sourceAsset;
|
||||
// TODO: check wrapped asset exists or is native attest
|
||||
export const selectAttestIsTargetComplete = (state: RootState) =>
|
||||
selectAttestIsSourceComplete(state) && !!state.attest.targetChain;
|
||||
export const selectAttestIsSendComplete = (state: RootState) =>
|
||||
!!selectAttestSignedVAAHex(state);
|
||||
export const selectAttestShouldLockFields = (state: RootState) =>
|
||||
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
|
||||
|
||||
/*
|
||||
* Transfer
|
||||
*/
|
||||
|
||||
export const selectTransferActiveStep = (state: RootState) =>
|
||||
state.transfer.activeStep;
|
||||
export const selectTransferSourceChain = (state: RootState) =>
|
||||
state.transfer.sourceChain;
|
||||
export const selectTransferSourceAsset = (state: RootState) =>
|
||||
state.transfer.sourceAsset;
|
||||
export const selectTransferSourceParsedTokenAccount = (state: RootState) =>
|
||||
state.transfer.sourceParsedTokenAccount;
|
||||
export const selectTransferSourceBalanceString = (state: RootState) =>
|
||||
state.transfer.sourceParsedTokenAccount?.uiAmountString || "";
|
||||
export const selectTransferAmount = (state: RootState) => state.transfer.amount;
|
||||
export const selectTransferTargetChain = (state: RootState) =>
|
||||
state.transfer.targetChain;
|
||||
export const selectTransferSignedVAAHex = (state: RootState) =>
|
||||
state.transfer.signedVAAHex;
|
||||
export const selectTransferIsSending = (state: RootState) =>
|
||||
state.transfer.isSending;
|
||||
export const selectTransferIsRedeeming = (state: RootState) =>
|
||||
state.transfer.isRedeeming;
|
||||
|
||||
// safety checks
|
||||
// TODO: could make this return a string with a user informative message
|
||||
export const selectTransferIsSourceComplete = (state: RootState) =>
|
||||
!!state.transfer.sourceChain &&
|
||||
!!state.transfer.sourceAsset &&
|
||||
!!state.transfer.sourceParsedTokenAccount &&
|
||||
!!state.transfer.amount &&
|
||||
(state.transfer.sourceChain !== CHAIN_ID_SOLANA ||
|
||||
!!state.transfer.sourceParsedTokenAccount.publicKey) &&
|
||||
!!state.transfer.sourceParsedTokenAccount.uiAmountString &&
|
||||
// TODO: make safe with too many decimals
|
||||
parseUnits(
|
||||
state.transfer.amount,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
).lte(
|
||||
parseUnits(
|
||||
state.transfer.sourceParsedTokenAccount.uiAmountString,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
)
|
||||
);
|
||||
// TODO: check wrapped asset exists or is native transfer
|
||||
export const selectTransferIsTargetComplete = (state: RootState) =>
|
||||
selectTransferIsSourceComplete(state) && !!state.transfer.targetChain;
|
||||
export const selectTransferIsSendComplete = (state: RootState) =>
|
||||
!!selectTransferSignedVAAHex(state);
|
||||
export const selectTransferShouldLockFields = (state: RootState) =>
|
||||
selectTransferIsSending(state) || selectTransferIsSendComplete(state);
|
||||
|
|
|
@ -1,128 +1,128 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TEST_TOKEN_ADDRESS,
|
||||
SOL_TEST_TOKEN_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
|
||||
const LAST_STEP = 3;
|
||||
|
||||
type Steps = 0 | 1 | 2 | 3;
|
||||
|
||||
export interface ParsedTokenAccount {
|
||||
publicKey: string | undefined;
|
||||
amount: string;
|
||||
decimals: number;
|
||||
uiAmount: number;
|
||||
uiAmountString: string;
|
||||
}
|
||||
|
||||
export interface TransferState {
|
||||
activeStep: Steps;
|
||||
sourceChain: ChainId;
|
||||
sourceAsset: string;
|
||||
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
||||
amount: string;
|
||||
targetChain: ChainId;
|
||||
signedVAAHex: string | undefined;
|
||||
isSending: boolean;
|
||||
isRedeeming: boolean;
|
||||
}
|
||||
|
||||
const initialState: TransferState = {
|
||||
activeStep: 0,
|
||||
sourceChain: CHAIN_ID_SOLANA,
|
||||
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
|
||||
sourceParsedTokenAccount: undefined,
|
||||
amount: "",
|
||||
targetChain: CHAIN_ID_ETH,
|
||||
signedVAAHex: undefined,
|
||||
isSending: false,
|
||||
isRedeeming: false,
|
||||
};
|
||||
|
||||
export const transferSlice = createSlice({
|
||||
name: "transfer",
|
||||
initialState,
|
||||
reducers: {
|
||||
incrementStep: (state) => {
|
||||
if (state.activeStep < LAST_STEP) state.activeStep++;
|
||||
},
|
||||
decrementStep: (state) => {
|
||||
if (state.activeStep > 0) state.activeStep--;
|
||||
},
|
||||
setStep: (state, action: PayloadAction<Steps>) => {
|
||||
state.activeStep = action.payload;
|
||||
},
|
||||
setSourceChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevSourceChain = state.sourceChain;
|
||||
state.sourceChain = action.payload;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (action.payload === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (action.payload === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === action.payload) {
|
||||
state.targetChain = prevSourceChain;
|
||||
}
|
||||
},
|
||||
setSourceAsset: (state, action: PayloadAction<string>) => {
|
||||
state.sourceAsset = action.payload;
|
||||
},
|
||||
setSourceParsedTokenAccount: (
|
||||
state,
|
||||
action: PayloadAction<ParsedTokenAccount | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccount = action.payload;
|
||||
},
|
||||
setAmount: (state, action: PayloadAction<string>) => {
|
||||
state.amount = action.payload;
|
||||
},
|
||||
setTargetChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevTargetChain = state.targetChain;
|
||||
state.targetChain = action.payload;
|
||||
if (state.sourceChain === action.payload) {
|
||||
state.sourceChain = prevTargetChain;
|
||||
state.activeStep = 0;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (state.targetChain === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
}
|
||||
},
|
||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||
state.signedVAAHex = action.payload;
|
||||
state.isSending = false;
|
||||
state.activeStep = 3;
|
||||
},
|
||||
setIsSending: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSending = action.payload;
|
||||
},
|
||||
setIsRedeeming: (state, action: PayloadAction<boolean>) => {
|
||||
state.isRedeeming = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
incrementStep,
|
||||
decrementStep,
|
||||
setStep,
|
||||
setSourceChain,
|
||||
setSourceAsset,
|
||||
setSourceParsedTokenAccount,
|
||||
setAmount,
|
||||
setTargetChain,
|
||||
setSignedVAAHex,
|
||||
setIsSending,
|
||||
setIsRedeeming,
|
||||
} = transferSlice.actions;
|
||||
|
||||
export default transferSlice.reducer;
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TEST_TOKEN_ADDRESS,
|
||||
SOL_TEST_TOKEN_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
|
||||
const LAST_STEP = 3;
|
||||
|
||||
type Steps = 0 | 1 | 2 | 3;
|
||||
|
||||
export interface ParsedTokenAccount {
|
||||
publicKey: string | undefined;
|
||||
amount: string;
|
||||
decimals: number;
|
||||
uiAmount: number;
|
||||
uiAmountString: string;
|
||||
}
|
||||
|
||||
export interface TransferState {
|
||||
activeStep: Steps;
|
||||
sourceChain: ChainId;
|
||||
sourceAsset: string;
|
||||
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
|
||||
amount: string;
|
||||
targetChain: ChainId;
|
||||
signedVAAHex: string | undefined;
|
||||
isSending: boolean;
|
||||
isRedeeming: boolean;
|
||||
}
|
||||
|
||||
const initialState: TransferState = {
|
||||
activeStep: 0,
|
||||
sourceChain: CHAIN_ID_SOLANA,
|
||||
sourceAsset: SOL_TEST_TOKEN_ADDRESS,
|
||||
sourceParsedTokenAccount: undefined,
|
||||
amount: "",
|
||||
targetChain: CHAIN_ID_ETH,
|
||||
signedVAAHex: undefined,
|
||||
isSending: false,
|
||||
isRedeeming: false,
|
||||
};
|
||||
|
||||
export const transferSlice = createSlice({
|
||||
name: "transfer",
|
||||
initialState,
|
||||
reducers: {
|
||||
incrementStep: (state) => {
|
||||
if (state.activeStep < LAST_STEP) state.activeStep++;
|
||||
},
|
||||
decrementStep: (state) => {
|
||||
if (state.activeStep > 0) state.activeStep--;
|
||||
},
|
||||
setStep: (state, action: PayloadAction<Steps>) => {
|
||||
state.activeStep = action.payload;
|
||||
},
|
||||
setSourceChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevSourceChain = state.sourceChain;
|
||||
state.sourceChain = action.payload;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (action.payload === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (action.payload === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === action.payload) {
|
||||
state.targetChain = prevSourceChain;
|
||||
}
|
||||
},
|
||||
setSourceAsset: (state, action: PayloadAction<string>) => {
|
||||
state.sourceAsset = action.payload;
|
||||
},
|
||||
setSourceParsedTokenAccount: (
|
||||
state,
|
||||
action: PayloadAction<ParsedTokenAccount | undefined>
|
||||
) => {
|
||||
state.sourceParsedTokenAccount = action.payload;
|
||||
},
|
||||
setAmount: (state, action: PayloadAction<string>) => {
|
||||
state.amount = action.payload;
|
||||
},
|
||||
setTargetChain: (state, action: PayloadAction<ChainId>) => {
|
||||
const prevTargetChain = state.targetChain;
|
||||
state.targetChain = action.payload;
|
||||
if (state.sourceChain === action.payload) {
|
||||
state.sourceChain = prevTargetChain;
|
||||
state.activeStep = 0;
|
||||
// TODO: remove or check env - for testing purposes
|
||||
if (state.targetChain === CHAIN_ID_ETH) {
|
||||
state.sourceAsset = ETH_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
if (state.targetChain === CHAIN_ID_SOLANA) {
|
||||
state.sourceAsset = SOL_TEST_TOKEN_ADDRESS;
|
||||
}
|
||||
}
|
||||
},
|
||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||
state.signedVAAHex = action.payload;
|
||||
state.isSending = false;
|
||||
state.activeStep = 3;
|
||||
},
|
||||
setIsSending: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSending = action.payload;
|
||||
},
|
||||
setIsRedeeming: (state, action: PayloadAction<boolean>) => {
|
||||
state.isRedeeming = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
incrementStep,
|
||||
decrementStep,
|
||||
setStep,
|
||||
setSourceChain,
|
||||
setSourceAsset,
|
||||
setSourceParsedTokenAccount,
|
||||
setAmount,
|
||||
setTargetChain,
|
||||
setSignedVAAHex,
|
||||
setIsSending,
|
||||
setIsRedeeming,
|
||||
} = transferSlice.actions;
|
||||
|
||||
export default transferSlice.reducer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const uint8ArrayToHex = (a: Uint8Array) =>
|
||||
Buffer.from(a).toString("hex");
|
||||
export const hexToUint8Array = (h: string) =>
|
||||
new Uint8Array(Buffer.from(h, "hex"));
|
||||
export const uint8ArrayToHex = (a: Uint8Array) =>
|
||||
Buffer.from(a).toString("hex");
|
||||
export const hexToUint8Array = (h: string) =>
|
||||
new Uint8Array(Buffer.from(h, "hex"));
|
||||
|
|
|
@ -1,157 +1,157 @@
|
|||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||
import { Bridge__factory, Implementation__factory } from "../ethers-contracts";
|
||||
import { getSignedVAA, ixFromRust } from "../sdk";
|
||||
import {
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
// TODO: allow for / handle cancellation?
|
||||
// TODO: overall better input checking and error handling
|
||||
export async function attestFromEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
tokenAddress: string
|
||||
) {
|
||||
if (!provider || !signer) return;
|
||||
//TODO: more catches
|
||||
const signerAddress = await signer.getAddress();
|
||||
console.log("Signer:", signerAddress);
|
||||
console.log("Token:", tokenAddress);
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
console.log("Initiating attestation");
|
||||
console.log("Nonce:", nonceBuffer);
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.attestToken(tokenAddress, nonceBuffer);
|
||||
const receipt = await v.wait();
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: dangerous!(?)
|
||||
const bridgeLog = receipt.logs.filter((l) => {
|
||||
console.log(l.address, ETH_BRIDGE_ADDRESS);
|
||||
return l.address === ETH_BRIDGE_ADDRESS;
|
||||
})[0];
|
||||
const {
|
||||
args: { sequence },
|
||||
} = Implementation__factory.createInterface().parseLog(bridgeLog);
|
||||
console.log("SEQ:", sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
// TODO: need to check transfer native vs transfer wrapped
|
||||
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
|
||||
export async function attestFromSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
mintAddress: string
|
||||
) {
|
||||
if (!wallet || !wallet.publicKey || !payerAddress) return;
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
const nonce = nonceBuffer.readUInt32LE(0);
|
||||
console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("bridge:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("payer:", payerAddress);
|
||||
console.log("token:", mintAddress);
|
||||
console.log("nonce:", nonce);
|
||||
const bridge = await import("bridge");
|
||||
const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
|
||||
const bridgeStatePK = new PublicKey(bridge.state_address(SOL_BRIDGE_ADDRESS));
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK);
|
||||
if (bridgeStateAccountInfo?.data === undefined) {
|
||||
throw new Error("bridge state not found");
|
||||
}
|
||||
const bridgeState = bridge.parse_state(
|
||||
new Uint8Array(bridgeStateAccountInfo?.data)
|
||||
);
|
||||
const transferIx = SystemProgram.transfer({
|
||||
fromPubkey: new PublicKey(payerAddress),
|
||||
toPubkey: new PublicKey(feeAccount),
|
||||
lamports: bridgeState.config.fee,
|
||||
});
|
||||
// TODO: pass in connection
|
||||
// Add transfer instruction to transaction
|
||||
const { attest_ix, emitter_address } = await import("token-bridge");
|
||||
const messageKey = Keypair.generate();
|
||||
const ix = ixFromRust(
|
||||
attest_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
messageKey.publicKey.toString(),
|
||||
mintAddress,
|
||||
nonce
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(transferIx, ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
transaction.partialSign(messageKey);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: better parsing, safer
|
||||
const SEQ_LOG = "Program log: Sequence: ";
|
||||
const sequence = info?.meta?.logMessages
|
||||
?.filter((msg) => msg.startsWith(SEQ_LOG))[0]
|
||||
.replace(SEQ_LOG, "");
|
||||
if (!sequence) {
|
||||
throw new Error("sequence not found");
|
||||
}
|
||||
console.log("SEQ", sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(
|
||||
new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
|
||||
32
|
||||
)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
const attestFrom = {
|
||||
[CHAIN_ID_ETH]: attestFromEth,
|
||||
[CHAIN_ID_SOLANA]: attestFromSolana,
|
||||
};
|
||||
|
||||
export default attestFrom;
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||
import { Bridge__factory, Implementation__factory } from "../ethers-contracts";
|
||||
import { getSignedVAA, ixFromRust } from "../sdk";
|
||||
import {
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
// TODO: allow for / handle cancellation?
|
||||
// TODO: overall better input checking and error handling
|
||||
export async function attestFromEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
tokenAddress: string
|
||||
) {
|
||||
if (!provider || !signer) return;
|
||||
//TODO: more catches
|
||||
const signerAddress = await signer.getAddress();
|
||||
console.log("Signer:", signerAddress);
|
||||
console.log("Token:", tokenAddress);
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
console.log("Initiating attestation");
|
||||
console.log("Nonce:", nonceBuffer);
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.attestToken(tokenAddress, nonceBuffer);
|
||||
const receipt = await v.wait();
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: dangerous!(?)
|
||||
const bridgeLog = receipt.logs.filter((l) => {
|
||||
console.log(l.address, ETH_BRIDGE_ADDRESS);
|
||||
return l.address === ETH_BRIDGE_ADDRESS;
|
||||
})[0];
|
||||
const {
|
||||
args: { sequence },
|
||||
} = Implementation__factory.createInterface().parseLog(bridgeLog);
|
||||
console.log("SEQ:", sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
// TODO: need to check transfer native vs transfer wrapped
|
||||
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
|
||||
export async function attestFromSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
mintAddress: string
|
||||
) {
|
||||
if (!wallet || !wallet.publicKey || !payerAddress) return;
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
const nonce = nonceBuffer.readUInt32LE(0);
|
||||
console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("bridge:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("payer:", payerAddress);
|
||||
console.log("token:", mintAddress);
|
||||
console.log("nonce:", nonce);
|
||||
const bridge = await import("bridge");
|
||||
const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
|
||||
const bridgeStatePK = new PublicKey(bridge.state_address(SOL_BRIDGE_ADDRESS));
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK);
|
||||
if (bridgeStateAccountInfo?.data === undefined) {
|
||||
throw new Error("bridge state not found");
|
||||
}
|
||||
const bridgeState = bridge.parse_state(
|
||||
new Uint8Array(bridgeStateAccountInfo?.data)
|
||||
);
|
||||
const transferIx = SystemProgram.transfer({
|
||||
fromPubkey: new PublicKey(payerAddress),
|
||||
toPubkey: new PublicKey(feeAccount),
|
||||
lamports: bridgeState.config.fee,
|
||||
});
|
||||
// TODO: pass in connection
|
||||
// Add transfer instruction to transaction
|
||||
const { attest_ix, emitter_address } = await import("token-bridge");
|
||||
const messageKey = Keypair.generate();
|
||||
const ix = ixFromRust(
|
||||
attest_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
messageKey.publicKey.toString(),
|
||||
mintAddress,
|
||||
nonce
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(transferIx, ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
transaction.partialSign(messageKey);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: better parsing, safer
|
||||
const SEQ_LOG = "Program log: Sequence: ";
|
||||
const sequence = info?.meta?.logMessages
|
||||
?.filter((msg) => msg.startsWith(SEQ_LOG))[0]
|
||||
.replace(SEQ_LOG, "");
|
||||
if (!sequence) {
|
||||
throw new Error("sequence not found");
|
||||
}
|
||||
console.log("SEQ", sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(
|
||||
new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
|
||||
32
|
||||
)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence.toString()
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
const attestFrom = {
|
||||
[CHAIN_ID_ETH]: attestFromEth,
|
||||
[CHAIN_ID_SOLANA]: attestFromSolana,
|
||||
};
|
||||
|
||||
export default attestFrom;
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
import { getAddress } from "ethers/lib/utils";
|
||||
|
||||
export type ChainId = 1 | 2 | 3 | 4;
|
||||
export const CHAIN_ID_SOLANA: ChainId = 1;
|
||||
export const CHAIN_ID_ETH: ChainId = 2;
|
||||
export const CHAIN_ID_TERRA: ChainId = 3;
|
||||
export const CHAIN_ID_BSC: ChainId = 4;
|
||||
export interface ChainInfo {
|
||||
id: ChainId;
|
||||
name: string;
|
||||
}
|
||||
export const CHAINS = [
|
||||
{
|
||||
id: CHAIN_ID_BSC,
|
||||
name: "Binance Smart Chain",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_ETH,
|
||||
name: "Ethereum",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_SOLANA,
|
||||
name: "Solana",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_TERRA,
|
||||
name: "Terra",
|
||||
},
|
||||
];
|
||||
export type ChainsById = { [key in ChainId]: ChainInfo };
|
||||
export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => {
|
||||
obj[chain.id] = chain;
|
||||
return obj;
|
||||
}, {} as ChainsById);
|
||||
export const SOLANA_HOST = "http://localhost:8899";
|
||||
export const ETH_TEST_TOKEN_ADDRESS = getAddress(
|
||||
"0x0290FB167208Af455bB137780163b7B7a9a10C16"
|
||||
);
|
||||
export const ETH_BRIDGE_ADDRESS = getAddress(
|
||||
"0x254dffcd3277c0b1660f6d42efbb754edababc2b"
|
||||
);
|
||||
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||
"0xe982e462b094850f12af94d21d470e21be9d0e9c"
|
||||
);
|
||||
export const SOL_TEST_TOKEN_ADDRESS =
|
||||
"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ";
|
||||
export const SOL_BRIDGE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
||||
export const SOL_TOKEN_BRIDGE_ADDRESS =
|
||||
"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
|
||||
import { getAddress } from "ethers/lib/utils";
|
||||
|
||||
export type ChainId = 1 | 2 | 3 | 4;
|
||||
export const CHAIN_ID_SOLANA: ChainId = 1;
|
||||
export const CHAIN_ID_ETH: ChainId = 2;
|
||||
export const CHAIN_ID_TERRA: ChainId = 3;
|
||||
export const CHAIN_ID_BSC: ChainId = 4;
|
||||
export interface ChainInfo {
|
||||
id: ChainId;
|
||||
name: string;
|
||||
}
|
||||
export const CHAINS = [
|
||||
{
|
||||
id: CHAIN_ID_BSC,
|
||||
name: "Binance Smart Chain",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_ETH,
|
||||
name: "Ethereum",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_SOLANA,
|
||||
name: "Solana",
|
||||
},
|
||||
{
|
||||
id: CHAIN_ID_TERRA,
|
||||
name: "Terra",
|
||||
},
|
||||
];
|
||||
export type ChainsById = { [key in ChainId]: ChainInfo };
|
||||
export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => {
|
||||
obj[chain.id] = chain;
|
||||
return obj;
|
||||
}, {} as ChainsById);
|
||||
export const SOLANA_HOST = "http://localhost:8899";
|
||||
export const ETH_TEST_TOKEN_ADDRESS = getAddress(
|
||||
"0x0290FB167208Af455bB137780163b7B7a9a10C16"
|
||||
);
|
||||
export const ETH_BRIDGE_ADDRESS = getAddress(
|
||||
"0x254dffcd3277c0b1660f6d42efbb754edababc2b"
|
||||
);
|
||||
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
|
||||
"0xe982e462b094850f12af94d21d470e21be9d0e9c"
|
||||
);
|
||||
export const SOL_TEST_TOKEN_ADDRESS =
|
||||
"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ";
|
||||
export const SOL_BRIDGE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
|
||||
export const SOL_TOKEN_BRIDGE_ADDRESS =
|
||||
"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
|
||||
|
|
|
@ -1,52 +1,80 @@
|
|||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { ixFromRust } from "../sdk";
|
||||
import {
|
||||
CHAIN_ID_SOLANA,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
export async function createWrappedOnSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
if (!wallet || !wallet.publicKey || !payerAddress) return;
|
||||
console.log("creating wrapped");
|
||||
console.log("PROGRAM:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("BRIDGE:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("PAYER:", payerAddress);
|
||||
console.log("VAA:", signedVAA);
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const { create_wrapped_ix } = await import("token-bridge");
|
||||
const ix = ixFromRust(
|
||||
create_wrapped_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
signedVAA
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
}
|
||||
|
||||
const createWrappedOn = {
|
||||
[CHAIN_ID_SOLANA]: createWrappedOnSolana,
|
||||
};
|
||||
|
||||
export default createWrappedOn;
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { ixFromRust } from "../sdk";
|
||||
import {
|
||||
CHAIN_ID_SOLANA,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
CHAIN_ID_ETH,
|
||||
} from "./consts";
|
||||
import { ethers } from "ethers";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { postVaa } from "./postVaa";
|
||||
|
||||
export async function createWrappedOnEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
console.log(provider, signer, signedVAA);
|
||||
if (!provider || !signer) return;
|
||||
console.log("creating wrapped");
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.createWrapped(signedVAA);
|
||||
const receipt = await v.wait();
|
||||
console.log(receipt);
|
||||
}
|
||||
|
||||
export async function createWrappedOnSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
if (!wallet || !wallet.publicKey || !payerAddress) return;
|
||||
console.log("creating wrapped");
|
||||
console.log("PROGRAM:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("BRIDGE:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("PAYER:", payerAddress);
|
||||
console.log("VAA:", signedVAA);
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const { create_wrapped_ix } = await import("token-bridge");
|
||||
|
||||
await postVaa(
|
||||
connection,
|
||||
wallet,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
Buffer.from(signedVAA)
|
||||
);
|
||||
const ix = ixFromRust(
|
||||
create_wrapped_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
signedVAA
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
}
|
||||
|
||||
const createWrappedOn = {
|
||||
[CHAIN_ID_SOLANA]: createWrappedOnSolana,
|
||||
[CHAIN_ID_ETH]: createWrappedOnEth,
|
||||
};
|
||||
|
||||
export default createWrappedOn;
|
||||
|
|
|
@ -1,60 +1,60 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify, isHexString, zeroPad } from "ethers/lib/utils";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
export async function getAttestedAssetEth(
|
||||
provider: ethers.providers.Web3Provider,
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
const tokenBridge = Bridge__factory.connect(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider
|
||||
);
|
||||
try {
|
||||
// TODO: address conversion may be more complex than this
|
||||
const originAssetBytes = zeroPad(
|
||||
originChain === CHAIN_ID_SOLANA
|
||||
? new PublicKey(originAsset).toBytes()
|
||||
: arrayify(originAsset),
|
||||
32
|
||||
);
|
||||
return await tokenBridge.wrappedAsset(originChain, originAssetBytes);
|
||||
} catch (e) {
|
||||
return ethers.constants.AddressZero;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAttestedAssetSol(
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
if (!isHexString(originAsset)) return null;
|
||||
const { wrapped_address } = await import("token-bridge");
|
||||
// TODO: address conversion may be more complex than this
|
||||
const originAssetBytes = zeroPad(
|
||||
arrayify(originAsset, { hexPad: "left" }),
|
||||
32
|
||||
);
|
||||
const wrappedAddress = wrapped_address(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
originAssetBytes,
|
||||
originChain
|
||||
);
|
||||
const wrappedAddressPK = new PublicKey(wrappedAddress);
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const wrappedAssetAccountInfo = await connection.getAccountInfo(
|
||||
wrappedAddressPK
|
||||
);
|
||||
console.log("WAAI", wrappedAssetAccountInfo);
|
||||
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
|
||||
}
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify, isHexString, zeroPad } from "ethers/lib/utils";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
export async function getAttestedAssetEth(
|
||||
provider: ethers.providers.Web3Provider,
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
const tokenBridge = Bridge__factory.connect(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
provider
|
||||
);
|
||||
try {
|
||||
// TODO: address conversion may be more complex than this
|
||||
const originAssetBytes = zeroPad(
|
||||
originChain === CHAIN_ID_SOLANA
|
||||
? new PublicKey(originAsset).toBytes()
|
||||
: arrayify(originAsset),
|
||||
32
|
||||
);
|
||||
return await tokenBridge.wrappedAsset(originChain, originAssetBytes);
|
||||
} catch (e) {
|
||||
return ethers.constants.AddressZero;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAttestedAssetSol(
|
||||
originChain: ChainId,
|
||||
originAsset: string
|
||||
) {
|
||||
if (!isHexString(originAsset)) return null;
|
||||
const { wrapped_address } = await import("token-bridge");
|
||||
// TODO: address conversion may be more complex than this
|
||||
const originAssetBytes = zeroPad(
|
||||
arrayify(originAsset, { hexPad: "left" }),
|
||||
32
|
||||
);
|
||||
const wrappedAddress = wrapped_address(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
originAssetBytes,
|
||||
originChain
|
||||
);
|
||||
const wrappedAddressPK = new PublicKey(wrappedAddress);
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const wrappedAssetAccountInfo = await connection.getAccountInfo(
|
||||
wrappedAddressPK
|
||||
);
|
||||
console.log("WAAI", wrappedAssetAccountInfo);
|
||||
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import { ixFromRust } from "../sdk";
|
||||
|
||||
export async function postVaa(
|
||||
connection: Connection,
|
||||
wallet: Wallet,
|
||||
bridge_id: string,
|
||||
payer: string,
|
||||
vaa: Buffer
|
||||
) {
|
||||
const {
|
||||
guardian_set_address,
|
||||
parse_guardian_set,
|
||||
verify_signatures_ix,
|
||||
post_vaa_ix,
|
||||
} = await import("bridge");
|
||||
let bridge_state = await getBridgeState(connection, bridge_id);
|
||||
let guardian_addr = new PublicKey(
|
||||
guardian_set_address(bridge_id, bridge_state.guardianSetIndex)
|
||||
);
|
||||
let acc = await connection.getAccountInfo(guardian_addr);
|
||||
if (acc?.data === undefined) {
|
||||
return;
|
||||
}
|
||||
let guardian_data = parse_guardian_set(new Uint8Array(acc?.data));
|
||||
|
||||
let signature_set = Keypair.generate();
|
||||
let txs = verify_signatures_ix(
|
||||
bridge_id,
|
||||
payer,
|
||||
bridge_state.guardianSetIndex,
|
||||
guardian_data,
|
||||
signature_set.publicKey.toString(),
|
||||
vaa
|
||||
);
|
||||
// Add transfer instruction to transaction
|
||||
for (let tx of txs) {
|
||||
let ixs: Array<TransactionInstruction> = tx.map((v: any) => {
|
||||
return ixFromRust(v);
|
||||
});
|
||||
let transaction = new Transaction().add(ixs[0], ixs[1]);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payer);
|
||||
transaction.partialSign(signature_set);
|
||||
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
}
|
||||
|
||||
let ix = ixFromRust(
|
||||
post_vaa_ix(bridge_id, payer, signature_set.publicKey.toString(), vaa)
|
||||
);
|
||||
let transaction = new Transaction().add(ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payer);
|
||||
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
}
|
||||
|
||||
async function getBridgeState(
|
||||
connection: Connection,
|
||||
bridge_id: string
|
||||
): Promise<BridgeState> {
|
||||
const { parse_state, state_address } = await import("bridge");
|
||||
let bridge_state = new PublicKey(state_address(bridge_id));
|
||||
let acc = await connection.getAccountInfo(bridge_state);
|
||||
if (acc?.data === undefined) {
|
||||
throw new Error("bridge state not found");
|
||||
}
|
||||
return parse_state(new Uint8Array(acc?.data));
|
||||
}
|
||||
|
||||
interface BridgeState {
|
||||
// The current guardian set index, used to decide which signature sets to accept.
|
||||
guardianSetIndex: number;
|
||||
|
||||
// Lamports in the collection account
|
||||
lastLamports: number;
|
||||
|
||||
// Bridge configuration, which is set once upon initialization.
|
||||
config: BridgeConfig;
|
||||
}
|
||||
|
||||
interface BridgeConfig {
|
||||
// Period for how long a guardian set is valid after it has been replaced by a new one. This
|
||||
// guarantees that VAAs issued by that set can still be submitted for a certain period. In
|
||||
// this period we still trust the old guardian set.
|
||||
guardianSetExpirationTime: number;
|
||||
|
||||
// Amount of lamports that needs to be paid to the protocol to post a message
|
||||
fee: number;
|
||||
}
|
|
@ -1,23 +1,81 @@
|
|||
import { ethers } from "ethers";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import { CHAIN_ID_ETH, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts";
|
||||
|
||||
export async function redeemOnEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
console.log(provider, signer, signedVAA);
|
||||
if (!provider || !signer) return;
|
||||
console.log("completing transfer");
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.completeTransfer(signedVAA);
|
||||
const receipt = await v.wait();
|
||||
console.log(receipt);
|
||||
}
|
||||
|
||||
const redeemOn = {
|
||||
[CHAIN_ID_ETH]: redeemOnEth,
|
||||
};
|
||||
|
||||
export default redeemOn;
|
||||
import { ethers } from "ethers";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
import {
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
} from "./consts";
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { postVaa } from "./postVaa";
|
||||
import { ixFromRust } from "../sdk";
|
||||
|
||||
export async function redeemOnEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
console.log(provider, signer, signedVAA);
|
||||
if (!provider || !signer) return;
|
||||
console.log("completing transfer");
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.completeTransfer(signedVAA);
|
||||
const receipt = await v.wait();
|
||||
console.log(receipt);
|
||||
}
|
||||
|
||||
export async function redeemOnSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
signedVAA: Uint8Array
|
||||
) {
|
||||
if (!wallet || !wallet.publicKey || !payerAddress) return;
|
||||
console.log("completing transfer");
|
||||
console.log("PROGRAM:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("BRIDGE:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("PAYER:", payerAddress);
|
||||
console.log("VAA:", signedVAA);
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const { complete_transfer_wrapped_ix } = await import("token-bridge");
|
||||
|
||||
await postVaa(
|
||||
connection,
|
||||
wallet,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
Buffer.from(signedVAA)
|
||||
);
|
||||
console.log(Buffer.from(signedVAA).toString("hex"));
|
||||
const ix = ixFromRust(
|
||||
complete_transfer_wrapped_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
signedVAA
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
}
|
||||
|
||||
const redeemOn = {
|
||||
[CHAIN_ID_ETH]: redeemOnEth,
|
||||
[CHAIN_ID_SOLANA]: redeemOnSolana,
|
||||
};
|
||||
|
||||
export default redeemOn;
|
||||
|
|
|
@ -1,227 +1,228 @@
|
|||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import {
|
||||
Connection, Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils";
|
||||
import {
|
||||
Bridge__factory,
|
||||
Implementation__factory,
|
||||
TokenImplementation__factory,
|
||||
} from "../ethers-contracts";
|
||||
import { getSignedVAA, ixFromRust } from "../sdk";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
// TODO: allow for / handle cancellation?
|
||||
// TODO: overall better input checking and error handling
|
||||
export async function transferFromEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
tokenAddress: string,
|
||||
decimals: number,
|
||||
amount: string,
|
||||
recipientChain: ChainId,
|
||||
recipientAddress: Uint8Array | undefined
|
||||
) {
|
||||
if (!provider || !signer || !recipientAddress) return;
|
||||
//TODO: check if token attestation exists on the target chain
|
||||
//TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain?
|
||||
//TODO: more catches
|
||||
const amountParsed = parseUnits(amount, decimals);
|
||||
const signerAddress = await signer.getAddress();
|
||||
console.log("Signer:", signerAddress);
|
||||
console.log("Token:", tokenAddress);
|
||||
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
||||
const allowance = await token.allowance(
|
||||
signerAddress,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS
|
||||
);
|
||||
console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't?
|
||||
const transaction = await token.approve(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
amountParsed
|
||||
);
|
||||
console.log(transaction);
|
||||
const fee = 0; // for now, this won't do anything, we may add later
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
console.log("Initiating transfer");
|
||||
console.log("Amount:", formatUnits(amountParsed, decimals));
|
||||
console.log("To chain:", recipientChain);
|
||||
console.log("To address:", recipientAddress);
|
||||
console.log("Fees:", fee);
|
||||
console.log("Nonce:", nonceBuffer);
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.transferTokens(
|
||||
tokenAddress,
|
||||
amountParsed,
|
||||
recipientChain,
|
||||
recipientAddress,
|
||||
fee,
|
||||
nonceBuffer
|
||||
);
|
||||
const receipt = await v.wait();
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: dangerous!(?)
|
||||
const bridgeLog = receipt.logs.filter((l) => {
|
||||
console.log(l.address, ETH_BRIDGE_ADDRESS);
|
||||
return l.address === ETH_BRIDGE_ADDRESS;
|
||||
})[0];
|
||||
const {
|
||||
args: { sender, sequence },
|
||||
} = Implementation__factory.createInterface().parseLog(bridgeLog);
|
||||
console.log(sender, sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
// TODO: need to check transfer native vs transfer wrapped
|
||||
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
|
||||
export async function transferFromSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
fromAddress: string | undefined,
|
||||
mintAddress: string,
|
||||
amount: string,
|
||||
decimals: number,
|
||||
targetAddressStr: string | undefined,
|
||||
targetChain: ChainId
|
||||
) {
|
||||
if (
|
||||
!wallet ||
|
||||
!wallet.publicKey ||
|
||||
!payerAddress ||
|
||||
!fromAddress ||
|
||||
!targetAddressStr
|
||||
)
|
||||
return;
|
||||
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
const nonce = nonceBuffer.readUInt32LE(0);
|
||||
const amountParsed = parseUnits(amount, decimals).toBigInt();
|
||||
const fee = BigInt(0); // for now, this won't do anything, we may add later
|
||||
console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("bridge:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("payer:", payerAddress);
|
||||
console.log("from:", fromAddress);
|
||||
console.log("token:", mintAddress);
|
||||
console.log("nonce:", nonce);
|
||||
console.log("amount:", amountParsed);
|
||||
console.log("fee:", fee);
|
||||
console.log("target:", targetAddressStr, targetAddress);
|
||||
console.log("chain:", targetChain);
|
||||
const bridge = await import("bridge");
|
||||
const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
|
||||
const bridgeStatePK = new PublicKey(bridge.state_address(SOL_BRIDGE_ADDRESS));
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK);
|
||||
if (bridgeStateAccountInfo?.data === undefined) {
|
||||
throw new Error("bridge state not found");
|
||||
}
|
||||
const bridgeState = bridge.parse_state(
|
||||
new Uint8Array(bridgeStateAccountInfo?.data)
|
||||
);
|
||||
const transferIx = SystemProgram.transfer({
|
||||
fromPubkey: new PublicKey(payerAddress),
|
||||
toPubkey: new PublicKey(feeAccount),
|
||||
lamports: bridgeState.config.fee,
|
||||
});
|
||||
// TODO: pass in connection
|
||||
// Add transfer instruction to transaction
|
||||
const { transfer_native_ix, approval_authority_address, emitter_address } =
|
||||
await import("token-bridge");
|
||||
const approvalIx = Token.createApproveInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(fromAddress),
|
||||
new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)),
|
||||
new PublicKey(payerAddress),
|
||||
[],
|
||||
Number(amountParsed)
|
||||
);
|
||||
|
||||
let messageKey = Keypair.generate();
|
||||
const ix = ixFromRust(
|
||||
transfer_native_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
messageKey.publicKey.toString(),
|
||||
fromAddress,
|
||||
mintAddress,
|
||||
nonce,
|
||||
amountParsed,
|
||||
fee,
|
||||
targetAddress,
|
||||
targetChain
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(transferIx, approvalIx, ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
transaction.partialSign(messageKey);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: better parsing, safer
|
||||
const SEQ_LOG = "Program log: Sequence: ";
|
||||
const sequence = info?.meta?.logMessages
|
||||
?.filter((msg) => msg.startsWith(SEQ_LOG))[0]
|
||||
.replace(SEQ_LOG, "");
|
||||
if (!sequence) {
|
||||
throw new Error("sequence not found");
|
||||
}
|
||||
console.log("SEQ", sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(
|
||||
new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
|
||||
32
|
||||
)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
const transferFrom = {
|
||||
[CHAIN_ID_ETH]: transferFromEth,
|
||||
[CHAIN_ID_SOLANA]: transferFromSolana,
|
||||
};
|
||||
|
||||
export default transferFrom;
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils";
|
||||
import {
|
||||
Bridge__factory,
|
||||
Implementation__factory,
|
||||
TokenImplementation__factory,
|
||||
} from "../ethers-contracts";
|
||||
import { getSignedVAA, ixFromRust } from "../sdk";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
ETH_BRIDGE_ADDRESS,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
SOLANA_HOST,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
} from "./consts";
|
||||
|
||||
// TODO: allow for / handle cancellation?
|
||||
// TODO: overall better input checking and error handling
|
||||
export async function transferFromEth(
|
||||
provider: ethers.providers.Web3Provider | undefined,
|
||||
signer: ethers.Signer | undefined,
|
||||
tokenAddress: string,
|
||||
decimals: number,
|
||||
amount: string,
|
||||
recipientChain: ChainId,
|
||||
recipientAddress: Uint8Array | undefined
|
||||
) {
|
||||
if (!provider || !signer || !recipientAddress) return;
|
||||
//TODO: check if token attestation exists on the target chain
|
||||
//TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain?
|
||||
//TODO: more catches
|
||||
const amountParsed = parseUnits(amount, decimals);
|
||||
const signerAddress = await signer.getAddress();
|
||||
console.log("Signer:", signerAddress);
|
||||
console.log("Token:", tokenAddress);
|
||||
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
||||
const allowance = await token.allowance(
|
||||
signerAddress,
|
||||
ETH_TOKEN_BRIDGE_ADDRESS
|
||||
);
|
||||
console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't?
|
||||
const transaction = await token.approve(
|
||||
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||
amountParsed
|
||||
);
|
||||
console.log(transaction);
|
||||
const fee = 0; // for now, this won't do anything, we may add later
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
console.log("Initiating transfer");
|
||||
console.log("Amount:", formatUnits(amountParsed, decimals));
|
||||
console.log("To chain:", recipientChain);
|
||||
console.log("To address:", recipientAddress);
|
||||
console.log("Fees:", fee);
|
||||
console.log("Nonce:", nonceBuffer);
|
||||
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
|
||||
const v = await bridge.transferTokens(
|
||||
tokenAddress,
|
||||
amountParsed,
|
||||
recipientChain,
|
||||
recipientAddress,
|
||||
fee,
|
||||
nonceBuffer
|
||||
);
|
||||
const receipt = await v.wait();
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: dangerous!(?)
|
||||
const bridgeLog = receipt.logs.filter((l) => {
|
||||
console.log(l.address, ETH_BRIDGE_ADDRESS);
|
||||
return l.address === ETH_BRIDGE_ADDRESS;
|
||||
})[0];
|
||||
const {
|
||||
args: { sender, sequence },
|
||||
} = Implementation__factory.createInterface().parseLog(bridgeLog);
|
||||
console.log(sender, sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_ETH,
|
||||
emitterAddress,
|
||||
sequence.toString()
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
// TODO: need to check transfer native vs transfer wrapped
|
||||
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
|
||||
export async function transferFromSolana(
|
||||
wallet: Wallet | undefined,
|
||||
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
||||
fromAddress: string | undefined,
|
||||
mintAddress: string,
|
||||
amount: string,
|
||||
decimals: number,
|
||||
targetAddressStr: string | undefined,
|
||||
targetChain: ChainId
|
||||
) {
|
||||
if (
|
||||
!wallet ||
|
||||
!wallet.publicKey ||
|
||||
!payerAddress ||
|
||||
!fromAddress ||
|
||||
!targetAddressStr
|
||||
)
|
||||
return;
|
||||
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
|
||||
const nonceConst = Math.random() * 100000;
|
||||
const nonceBuffer = Buffer.alloc(4);
|
||||
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
||||
const nonce = nonceBuffer.readUInt32LE(0);
|
||||
const amountParsed = parseUnits(amount, decimals).toBigInt();
|
||||
const fee = BigInt(0); // for now, this won't do anything, we may add later
|
||||
console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
|
||||
console.log("bridge:", SOL_BRIDGE_ADDRESS);
|
||||
console.log("payer:", payerAddress);
|
||||
console.log("from:", fromAddress);
|
||||
console.log("token:", mintAddress);
|
||||
console.log("nonce:", nonce);
|
||||
console.log("amount:", amountParsed);
|
||||
console.log("fee:", fee);
|
||||
console.log("target:", targetAddressStr, targetAddress);
|
||||
console.log("chain:", targetChain);
|
||||
const bridge = await import("bridge");
|
||||
const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
|
||||
const bridgeStatePK = new PublicKey(bridge.state_address(SOL_BRIDGE_ADDRESS));
|
||||
// TODO: share connection in context?
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK);
|
||||
if (bridgeStateAccountInfo?.data === undefined) {
|
||||
throw new Error("bridge state not found");
|
||||
}
|
||||
const bridgeState = bridge.parse_state(
|
||||
new Uint8Array(bridgeStateAccountInfo?.data)
|
||||
);
|
||||
const transferIx = SystemProgram.transfer({
|
||||
fromPubkey: new PublicKey(payerAddress),
|
||||
toPubkey: new PublicKey(feeAccount),
|
||||
lamports: bridgeState.config.fee,
|
||||
});
|
||||
// TODO: pass in connection
|
||||
// Add transfer instruction to transaction
|
||||
const { transfer_native_ix, approval_authority_address, emitter_address } =
|
||||
await import("token-bridge");
|
||||
const approvalIx = Token.createApproveInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(fromAddress),
|
||||
new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)),
|
||||
new PublicKey(payerAddress),
|
||||
[],
|
||||
Number(amountParsed)
|
||||
);
|
||||
|
||||
let messageKey = Keypair.generate();
|
||||
const ix = ixFromRust(
|
||||
transfer_native_ix(
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
SOL_BRIDGE_ADDRESS,
|
||||
payerAddress,
|
||||
messageKey.publicKey.toString(),
|
||||
fromAddress,
|
||||
mintAddress,
|
||||
nonce,
|
||||
amountParsed,
|
||||
fee,
|
||||
targetAddress,
|
||||
targetChain
|
||||
)
|
||||
);
|
||||
const transaction = new Transaction().add(transferIx, approvalIx, ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = new PublicKey(payerAddress);
|
||||
transaction.partialSign(messageKey);
|
||||
// Sign transaction, broadcast, and confirm
|
||||
const signed = await wallet.signTransaction(transaction);
|
||||
console.log("SIGNED", signed);
|
||||
const txid = await connection.sendRawTransaction(signed.serialize());
|
||||
console.log("SENT", txid);
|
||||
const conf = await connection.confirmTransaction(txid);
|
||||
console.log("CONFIRMED", conf);
|
||||
const info = await connection.getTransaction(txid);
|
||||
console.log("INFO", info);
|
||||
// TODO: log parsing should be part of a utility
|
||||
// TODO: better parsing, safer
|
||||
const SEQ_LOG = "Program log: Sequence: ";
|
||||
const sequence = info?.meta?.logMessages
|
||||
?.filter((msg) => msg.startsWith(SEQ_LOG))[0]
|
||||
.replace(SEQ_LOG, "");
|
||||
if (!sequence) {
|
||||
throw new Error("sequence not found");
|
||||
}
|
||||
console.log("SEQ", sequence);
|
||||
const emitterAddress = Buffer.from(
|
||||
zeroPad(
|
||||
new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
|
||||
32
|
||||
)
|
||||
).toString("hex");
|
||||
const { vaaBytes } = await getSignedVAA(
|
||||
CHAIN_ID_SOLANA,
|
||||
emitterAddress,
|
||||
sequence
|
||||
);
|
||||
console.log("SIGNED VAA:", vaaBytes);
|
||||
return vaaBytes;
|
||||
}
|
||||
|
||||
const transferFrom = {
|
||||
[CHAIN_ID_ETH]: transferFromEth,
|
||||
[CHAIN_ID_SOLANA]: transferFromSolana,
|
||||
};
|
||||
|
||||
export default transferFrom;
|
||||
|
|
Loading…
Reference in New Issue