Forráskód Böngészése

bridge_ui: eth wrapped assets and getSignedVAA

Change-Id: I1beaeefb7863c0543e180ed2e15e91c645b89299
Evan Gray 4 éve
szülő
commit
5187120fa0

+ 5 - 16
bridge_ui/package-lock.json

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

+ 1 - 0
bridge_ui/package.json

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

+ 84 - 34
bridge_ui/src/components/Transfer.tsx

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

+ 0 - 2
bridge_ui/src/hooks/useEthereumBalance.ts

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

+ 41 - 0
bridge_ui/src/hooks/useWrappedAsset.ts

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

+ 1 - 1
bridge_ui/src/muiTheme.js

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

+ 34 - 0
bridge_ui/src/sdk/index.ts

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

+ 11 - 4
bridge_ui/src/utils/consts.ts

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

+ 57 - 40
bridge_ui/src/utils/transferFrom.ts

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

+ 24 - 0
bridge_ui/src/utils/wrappedAsset.ts

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

+ 1 - 0
bridge_ui/tsconfig.json

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

+ 1 - 1
buf.gen.web.yaml

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

+ 7 - 7
tools/package-lock.json

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

+ 2 - 3
tools/package.json

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