bridge_ui: eth wrapped assets and getSignedVAA

Change-Id: I1beaeefb7863c0543e180ed2e15e91c645b89299
This commit is contained in:
Evan Gray 2021-08-04 12:08:46 -04:00 committed by Hendrik Hofstadt
parent 3aecf65f4d
commit 5187120fa0
14 changed files with 269 additions and 108 deletions

View File

@ -9,6 +9,7 @@
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@improbable-eng/grpc-web": "^0.13.0",
"@material-ui/core": "^4.12.2",
"@metamask/detect-provider": "^1.2.0",
"@project-serum/sol-wallet-adapter": "^0.2.5",
@ -4011,8 +4012,6 @@
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz",
"integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==",
"dev": true,
"optional": true,
"dependencies": {
"browser-headers": "^0.4.0"
},
@ -10309,9 +10308,7 @@
"node_modules/browser-headers": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz",
"integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==",
"dev": true,
"optional": true
"integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg=="
},
"node_modules/browser-process-hrtime": {
"version": "1.0.0",
@ -17417,9 +17414,7 @@
"node_modules/google-protobuf": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz",
"integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==",
"dev": true,
"optional": true
"integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg=="
},
"node_modules/got": {
"version": "9.6.0",
@ -41493,8 +41488,6 @@
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz",
"integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==",
"dev": true,
"optional": true,
"requires": {
"browser-headers": "^0.4.0"
}
@ -46695,9 +46688,7 @@
"browser-headers": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz",
"integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==",
"dev": true,
"optional": true
"integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg=="
},
"browser-process-hrtime": {
"version": "1.0.0",
@ -52508,9 +52499,7 @@
"google-protobuf": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz",
"integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==",
"dev": true,
"optional": true
"integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg=="
},
"got": {
"version": "9.6.0",

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@improbable-eng/grpc-web": "^0.13.0",
"@material-ui/core": "^4.12.2",
"@metamask/detect-provider": "^1.2.0",
"@project-serum/sol-wallet-adapter": "^0.2.5",

View File

@ -1,5 +1,6 @@
import {
Button,
CircularProgress,
Grid,
makeStyles,
MenuItem,
@ -11,6 +12,7 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import useEthereumBalance from "../hooks/useEthereumBalance";
import useSolanaBalance from "../hooks/useSolanaBalance";
import useWrappedAsset from "../hooks/useWrappedAsset";
import {
ChainId,
CHAINS,
@ -105,8 +107,18 @@ function Transfer() {
decimals: solDecimals,
uiAmount: solBalance,
} = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA);
const { isLoading: isCheckingWrapped, wrappedAsset } = useWrappedAsset(
toChain,
fromChain,
assetAddress,
provider
);
console.log(isCheckingWrapped, wrappedAsset);
// TODO: make a helper function for this
const isWrapped = true;
//wrappedAsset && wrappedAsset !== ethers.constants.AddressZero;
// TODO: dynamically get "to" wallet
const handleClick = useCallback(() => {
const handleTransferClick = useCallback(() => {
// TODO: more generic way of calling these
if (transferFrom[fromChain]) {
if (
@ -210,39 +222,77 @@ function Transfer() {
value={assetAddress}
onChange={handleAssetChange}
/>
<TextField
placeholder="Amount"
type="number"
fullWidth
className={classes.transferField}
value={amount}
onChange={handleAmountChange}
/>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleClick}
disabled={!canAttemptTransfer}
>
Transfer
</Button>
{canAttemptTransfer ? null : (
<Typography variant="body2" color="error">
{!isTransferImplemented
? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}`
: !isProviderConnected
? "The source wallet is not connected"
: !isRecipientAvailable
? "The receiving wallet is not connected"
: !isAddressDefined
? "Please provide an asset address"
: !isAmountPositive
? "The amount must be positive"
: !isBalanceAtLeastAmount
? "The amount may not be greater than the balance"
: ""}
</Typography>
{isWrapped ? (
<>
<TextField
placeholder="Amount"
type="number"
fullWidth
className={classes.transferField}
value={amount}
onChange={handleAmountChange}
/>
<Button
color="primary"
variant="contained"
className={classes.transferButton}
onClick={handleTransferClick}
disabled={!canAttemptTransfer}
>
Transfer
</Button>
{canAttemptTransfer ? null : (
<Typography variant="body2" color="error">
{!isTransferImplemented
? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}`
: !isProviderConnected
? "The source wallet is not connected"
: !isRecipientAvailable
? "The receiving wallet is not connected"
: !isAddressDefined
? "Please provide an asset address"
: !isAmountPositive
? "The amount must be positive"
: !isBalanceAtLeastAmount
? "The amount may not be greater than the balance"
: ""}
</Typography>
)}
</>
) : (
<>
<div style={{ position: "relative" }}>
<Button
color="primary"
variant="contained"
disabled={isCheckingWrapped}
className={classes.transferButton}
>
Attest
</Button>
{isCheckingWrapped ? (
<CircularProgress
size={24}
color="inherit"
style={{
position: "absolute",
bottom: 0,
left: "50%",
marginLeft: -12,
marginBottom: 6,
}}
/>
) : null}
</div>
{isCheckingWrapped ? null : (
<Typography variant="body2">
<br />
This token does not exist on {CHAINS_BY_ID[toChain].name}. Someone
must attest the the token to the target chain before it can be
transferred.
</Typography>
)}
</>
)}
</div>
);

View File

@ -20,12 +20,10 @@ function useEthereumBalance(
token
.decimals()
.then((decimals) => {
console.log(decimals);
provider
?.getSigner()
.getAddress()
.then((pk) => {
console.log(pk);
token.balanceOf(pk).then((n) => {
if (!cancelled) {
setBalance(formatUnits(n, decimals));

View File

@ -0,0 +1,41 @@
import { ethers } from "ethers";
import { useEffect, useState } from "react";
import { ChainId, CHAIN_ID_ETH } from "../utils/consts";
import { wrappedAssetEth } from "../utils/wrappedAsset";
export interface WrappedAssetState {
isLoading: boolean;
wrappedAsset: string | null;
}
function useWrappedAsset(
checkChain: ChainId,
originChain: ChainId,
originAsset: string,
provider: ethers.providers.Web3Provider | undefined
) {
const [state, setState] = useState<WrappedAssetState>({
isLoading: false,
wrappedAsset: null,
});
useEffect(() => {
let cancelled = false;
(async () => {
if (provider && checkChain === CHAIN_ID_ETH) {
setState({ isLoading: true, wrappedAsset: null });
const asset = await wrappedAssetEth(provider, originChain, originAsset);
if (!cancelled) {
setState({ isLoading: false, wrappedAsset: asset });
}
} else {
setState({ isLoading: false, wrappedAsset: null });
}
})();
return () => {
cancelled = true;
};
}, [checkChain, originChain, originAsset, provider]);
return state;
}
export default useWrappedAsset;

View File

@ -8,7 +8,7 @@ export const theme = responsiveFontSizes(
default: "#010114",
paper: "#010114",
},
divider: "#FFFFFF",
divider: "#4e4e54",
primary: {
main: "#0074FF",
},

View File

@ -0,0 +1,34 @@
import {
GrpcWebImpl,
PublicrpcClientImpl,
} from "../proto/publicrpc/v1/publicrpc";
import { ChainId } from "../utils/consts";
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,3 +1,5 @@
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;
@ -31,10 +33,15 @@ export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => {
return obj;
}, {} as ChainsById);
export const SOLANA_HOST = "http://localhost:8899";
export const ETH_TEST_TOKEN_ADDRESS =
"0x0290FB167208Af455bB137780163b7B7a9a10C16";
export const ETH_TOKEN_BRIDGE_ADDRESS =
"0xe982e462b094850f12af94d21d470e21be9d0e9c";
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";

View File

@ -1,4 +1,5 @@
import Wallet from "@project-serum/sol-wallet-adapter";
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
AccountMeta,
Connection,
@ -7,17 +8,19 @@ import {
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
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 } from "../sdk";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
@ -41,46 +44,60 @@ export function transferFromEth(
//TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain?
//TODO: more catches
const amountParsed = parseUnits(amount, 18);
signer.getAddress().then((signerAddress) => {
(async () => {
const signerAddress = await signer.getAddress();
console.log("Signer:", signerAddress);
console.log("Token:", tokenAddress);
const token = TokenImplementation__factory.connect(tokenAddress, signer);
token
.allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS)
.then((allowance) => {
console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't?
token
.approve(ETH_TOKEN_BRIDGE_ADDRESS, amountParsed)
.then((transaction) => {
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, 18));
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
);
bridge
.transferTokens(
tokenAddress,
amountParsed,
recipientChain,
recipientAddress,
fee,
nonceBuffer
)
.then((v) => console.log("Success:", v))
.catch((r) => console.error(r)); //TODO: integrate toast messages
});
});
});
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, 18));
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: 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);
})();
}
// TODO: should we share this with client? ooh, should client use the SDK ;)
@ -201,8 +218,8 @@ export function transferFromSolana(
console.log("SIGNED", signed);
const txid = await connection.sendRawTransaction(signed.serialize());
console.log("SENT", txid);
await connection.confirmTransaction(txid);
console.log("CONFIRMED");
const conf = await connection.confirmTransaction(txid);
console.log("CONFIRMED", conf);
})();
}

View File

@ -0,0 +1,24 @@
import { PublicKey } from "@solana/web3.js";
import { ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { Bridge__factory } from "../ethers-contracts";
import { ChainId, CHAIN_ID_SOLANA, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts";
export function wrappedAssetEth(
provider: ethers.providers.Web3Provider,
originChain: ChainId,
originAsset: string
) {
const tokenBridge = Bridge__factory.connect(
ETH_TOKEN_BRIDGE_ADDRESS,
provider
);
// TODO: address conversion may be more complex than this
const originAssetBytes = zeroPad(
originChain === CHAIN_ID_SOLANA
? new PublicKey(originAsset).toBytes()
: arrayify(originAsset),
32
);
return tokenBridge.wrappedAsset(originChain, originAssetBytes);
}

View File

@ -17,6 +17,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true,
"noEmit": true,
"jsx": "react-jsx"
},

View File

@ -19,4 +19,4 @@ plugins:
- env=browser
- forceLong=string
- outputClientImpl=grpc-web
- explorer/src/proto
- bridge_ui/src/proto

View File

@ -7,7 +7,7 @@
"": {
"version": "1.0.0",
"devDependencies": {
"ts-proto": "^1.81.1"
"ts-proto": "^1.82.3"
}
},
"node_modules/@protobufjs/aspromise": {
@ -175,9 +175,9 @@
}
},
"node_modules/ts-proto": {
"version": "1.82.0",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.0.tgz",
"integrity": "sha512-vo4QN4QhR0D4/+C/pSbRIVSV6U7dooNcuyW3SL9DvhKRQA4lnAbF5QBs77ge3JRi+aSZJm8MlzTNk7+e++fvvQ==",
"version": "1.82.3",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.3.tgz",
"integrity": "sha512-ODveOXK2imsgTiqkBcJu9mIOklmCTSzs7Xu+mT8Xljwh3Wenhax7bhty+x2eO4J7AfNkikXH0Xs7K3lk3UT8VA==",
"dev": true,
"dependencies": {
"@types/object-hash": "^1.3.0",
@ -354,9 +354,9 @@
}
},
"ts-proto": {
"version": "1.82.0",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.0.tgz",
"integrity": "sha512-vo4QN4QhR0D4/+C/pSbRIVSV6U7dooNcuyW3SL9DvhKRQA4lnAbF5QBs77ge3JRi+aSZJm8MlzTNk7+e++fvvQ==",
"version": "1.82.3",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.3.tgz",
"integrity": "sha512-ODveOXK2imsgTiqkBcJu9mIOklmCTSzs7Xu+mT8Xljwh3Wenhax7bhty+x2eO4J7AfNkikXH0Xs7K3lk3UT8VA==",
"dev": true,
"requires": {
"@types/object-hash": "^1.3.0",

View File

@ -3,7 +3,6 @@
"version": "1.0.0",
"description": "tooling for building web code from protobufs",
"devDependencies": {
"ts-proto": "^1.81.1"
},
"dependencies": {}
"ts-proto": "^1.82.3"
}
}