Minor fixes and bidirectional transfers

Change-Id: I7cbb02fe79b799c1ce350cee9c5b73ea17483385
This commit is contained in:
Hendrik Hofstadt 2021-08-11 13:48:15 +02:00
parent 1da690aa42
commit 8b3e0f00e0
35 changed files with 2459 additions and 2245 deletions

View File

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

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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;

View File

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

View File

@ -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;

View File

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

View File

@ -1 +1 @@
/// <reference types="react-scripts" />
/// <reference types="react-scripts" />

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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"));

View File

@ -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;

View File

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

View File

@ -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;

View File

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

View File

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

View File

@ -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;

View File

@ -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;