Parcourir la source

sdk/js: move solana and eth transactions

Change-Id: I04cdb6591c21507a23b3bd809d9a8b557b860a90
Evan Gray il y a 4 ans
Parent
commit
1175eb1315
34 fichiers modifiés avec 1079 ajouts et 850 suppressions
  1. 6 14
      bridge_ui/src/components/Attest/Create.tsx
  2. 33 46
      bridge_ui/src/components/Attest/Send.tsx
  3. 5 14
      bridge_ui/src/components/Transfer/Redeem.tsx
  4. 48 63
      bridge_ui/src/components/Transfer/Send.tsx
  5. 2 2
      bridge_ui/src/store/transferSlice.ts
  6. 31 121
      bridge_ui/src/utils/attestFrom.ts
  7. 19 56
      bridge_ui/src/utils/createWrappedOn.ts
  8. 13 32
      bridge_ui/src/utils/getForeignAsset.ts
  9. 7 43
      bridge_ui/src/utils/getIsWrappedAsset.ts
  10. 34 64
      bridge_ui/src/utils/getOriginalAsset.ts
  11. 18 105
      bridge_ui/src/utils/redeemOn.ts
  12. 22 0
      bridge_ui/src/utils/solana.ts
  13. 41 185
      bridge_ui/src/utils/transferFrom.ts
  14. 131 90
      sdk/js/package-lock.json
  15. 4 0
      sdk/js/package.json
  16. 15 0
      sdk/js/src/bridge/getEmitterAddress.ts
  17. 2 0
      sdk/js/src/bridge/index.ts
  18. 30 0
      sdk/js/src/bridge/parseSequenceFromLog.ts
  19. 2 0
      sdk/js/src/index.ts
  20. 24 0
      sdk/js/src/solana/getBridgeFeeIx.ts
  21. 2 0
      sdk/js/src/solana/index.ts
  22. 6 13
      sdk/js/src/solana/postVaa.ts
  23. 49 0
      sdk/js/src/token_bridge/attest.ts
  24. 38 0
      sdk/js/src/token_bridge/createWrapped.ts
  25. 53 0
      sdk/js/src/token_bridge/getForeignAsset.ts
  26. 45 0
      sdk/js/src/token_bridge/getIsWrappedAsset.ts
  27. 90 0
      sdk/js/src/token_bridge/getOriginalAsset.ts
  28. 7 0
      sdk/js/src/token_bridge/index.ts
  29. 90 0
      sdk/js/src/token_bridge/redeem.ts
  30. 118 0
      sdk/js/src/token_bridge/transfer.ts
  31. 6 0
      sdk/js/src/utils/createNonce.ts
  32. 1 0
      sdk/js/src/utils/index.ts
  33. 3 2
      sdk/js/tsconfig.json
  34. 84 0
      sdk/js/types/buffer-layout.d.ts

+ 6 - 14
bridge_ui/src/components/Attest/Create.tsx

@@ -10,7 +10,7 @@ import {
   selectAttestIsCreating,
   selectAttestTargetChain,
 } from "../../store/selectors";
-import createWrappedOn, {
+import {
   createWrappedOnEth,
   createWrappedOnSolana,
 } from "../../utils/createWrappedOn";
@@ -31,27 +31,19 @@ function Create() {
   const solPK = wallet?.publicKey;
   const signedVAA = useAttestSignedVAA();
   const isCreating = useSelector(selectAttestIsCreating);
-  const { provider, signer } = useEthereumProvider();
+  const { signer } = useEthereumProvider();
   const handleCreateClick = useCallback(() => {
-    if (
-      targetChain === CHAIN_ID_SOLANA &&
-      createWrappedOn[targetChain] === createWrappedOnSolana &&
-      signedVAA
-    ) {
+    if (targetChain === CHAIN_ID_SOLANA && signedVAA) {
       dispatch(setIsCreating(true));
       createWrappedOnSolana(wallet, solPK?.toString(), signedVAA);
     }
-    if (
-      targetChain === CHAIN_ID_ETH &&
-      createWrappedOn[targetChain] === createWrappedOnEth &&
-      signedVAA
-    ) {
+    if (targetChain === CHAIN_ID_ETH && signedVAA) {
       (async () => {
         dispatch(setIsCreating(true));
-        createWrappedOnEth(provider, signer, signedVAA);
+        createWrappedOnEth(signer, signedVAA);
       })();
     }
-  }, [dispatch, targetChain, wallet, solPK, signedVAA, provider, signer]);
+  }, [dispatch, targetChain, wallet, solPK, signedVAA, signer]);
   return (
     <div style={{ position: "relative" }}>
       <Button

+ 33 - 46
bridge_ui/src/components/Attest/Send.tsx

@@ -13,10 +13,7 @@ import {
   selectAttestSourceChain,
 } from "../../store/selectors";
 import { uint8ArrayToHex } from "../../utils/array";
-import attestFrom, {
-  attestFromEth,
-  attestFromSolana,
-} from "../../utils/attestFrom";
+import { attestFromEth, attestFromSolana } from "../../utils/attestFrom";
 
 const useStyles = makeStyles((theme) => ({
   transferButton: {
@@ -36,53 +33,43 @@ function Send() {
   const isTargetComplete = useSelector(selectAttestIsTargetComplete);
   const isSending = useSelector(selectAttestIsSending);
   const isSendComplete = useSelector(selectAttestIsSendComplete);
-  const { provider, signer } = useEthereumProvider();
+  const { signer } = useEthereumProvider();
   const { wallet } = useSolanaWallet();
   const solPK = wallet?.publicKey;
   // 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));
-          }
-        })();
-      }
+    if (sourceChain === CHAIN_ID_ETH) {
+      //TODO: just for testing, this should eventually use the store to communicate between steps
+      (async () => {
+        dispatch(setIsSending(true));
+        try {
+          const vaaBytes = await attestFromEth(signer, sourceAsset);
+          console.log("bytes in attest", vaaBytes);
+          vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
+        } catch (e) {
+          console.error(e);
+          dispatch(setIsSending(false));
+        }
+      })();
+    } else if (sourceChain === CHAIN_ID_SOLANA) {
+      //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]);
+  }, [dispatch, sourceChain, signer, wallet, solPK, sourceAsset]);
   return (
     <>
       <div style={{ position: "relative" }}>

+ 5 - 14
bridge_ui/src/components/Transfer/Redeem.tsx

@@ -13,7 +13,7 @@ import {
   selectTransferTargetChain,
 } from "../../store/selectors";
 import { setIsRedeeming } from "../../store/transferSlice";
-import redeemOn, { redeemOnEth, redeemOnSolana } from "../../utils/redeemOn";
+import { redeemOnEth, redeemOnSolana } from "../../utils/redeemOn";
 
 const useStyles = makeStyles((theme) => ({
   transferButton: {
@@ -34,23 +34,15 @@ function Redeem() {
   const targetAsset = useSelector(selectTransferTargetAsset);
   const { wallet } = useSolanaWallet();
   const solPK = wallet?.publicKey;
-  const { provider, signer } = useEthereumProvider();
+  const { signer } = useEthereumProvider();
   const signedVAA = useTransferSignedVAA();
   const isRedeeming = useSelector(selectTransferIsRedeeming);
   const handleRedeemClick = useCallback(() => {
-    if (
-      targetChain === CHAIN_ID_ETH &&
-      redeemOn[targetChain] === redeemOnEth &&
-      signedVAA
-    ) {
+    if (targetChain === CHAIN_ID_ETH && signedVAA) {
       dispatch(setIsRedeeming(true));
-      redeemOnEth(provider, signer, signedVAA);
+      redeemOnEth(signer, signedVAA);
     }
-    if (
-      targetChain === CHAIN_ID_SOLANA &&
-      redeemOn[targetChain] === redeemOnSolana &&
-      signedVAA
-    ) {
+    if (targetChain === CHAIN_ID_SOLANA && signedVAA) {
       dispatch(setIsRedeeming(true));
       redeemOnSolana(
         wallet,
@@ -63,7 +55,6 @@ function Redeem() {
   }, [
     dispatch,
     targetChain,
-    provider,
     signer,
     signedVAA,
     wallet,

+ 48 - 63
bridge_ui/src/components/Transfer/Send.tsx

@@ -27,10 +27,7 @@ import {
 } from "../../store/selectors";
 import { setIsSending, setSignedVAAHex } from "../../store/transferSlice";
 import { uint8ArrayToHex } from "../../utils/array";
-import transferFrom, {
-  transferFromEth,
-  transferFromSolana,
-} from "../../utils/transferFrom";
+import { transferFromEth, transferFromSolana } from "../../utils/transferFrom";
 
 const useStyles = makeStyles((theme) => ({
   transferButton: {
@@ -55,7 +52,7 @@ function Send() {
   const isTargetComplete = useSelector(selectTransferIsTargetComplete);
   const isSending = useSelector(selectTransferIsSending);
   const isSendComplete = useSelector(selectTransferIsSendComplete);
-  const { provider, signer, signerAddress } = useEthereumProvider();
+  const { signer, signerAddress } = useEthereumProvider();
   const { wallet } = useSolanaWallet();
   const solPK = wallet?.publicKey;
   const sourceParsedTokenAccount = useSelector(
@@ -102,68 +99,56 @@ function Send() {
   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 {
-            console.log("actually sending", tpkRef.current);
-            const vaaBytes = await transferFromEth(
-              provider,
-              signer,
-              sourceAsset,
-              decimals,
-              amount,
-              targetChain,
-              tpkRef.current
-            );
-            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(),
-              sourceTokenPublicKey,
-              sourceAsset,
-              amount, //TODO: avoid decimals, pass in parsed amount
-              decimals,
-              signerAddress,
-              targetChain,
-              originAsset,
-              originChain
-            );
-            console.log("bytes in transfer", vaaBytes);
-            vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
-          } catch (e) {
-            console.error(e);
-            dispatch(setIsSending(false));
-          }
-        })();
-      }
+    if (sourceChain === CHAIN_ID_ETH && decimals) {
+      //TODO: just for testing, this should eventually use the store to communicate between steps
+      (async () => {
+        dispatch(setIsSending(true));
+        try {
+          console.log("actually sending", tpkRef.current);
+          const vaaBytes = await transferFromEth(
+            signer,
+            sourceAsset,
+            decimals,
+            amount,
+            targetChain,
+            tpkRef.current
+          );
+          console.log("bytes in transfer", vaaBytes);
+          vaaBytes && dispatch(setSignedVAAHex(uint8ArrayToHex(vaaBytes)));
+        } catch (e) {
+          console.error(e);
+          dispatch(setIsSending(false));
+        }
+      })();
+    }
+    if (sourceChain === CHAIN_ID_SOLANA && 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(),
+            sourceTokenPublicKey,
+            sourceAsset,
+            amount, //TODO: avoid decimals, pass in parsed amount
+            decimals,
+            signerAddress,
+            targetChain,
+            originAsset,
+            originChain
+          );
+          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,

+ 2 - 2
bridge_ui/src/store/transferSlice.ts

@@ -8,7 +8,7 @@ import {
   ETH_TEST_TOKEN_ADDRESS,
   SOL_TEST_TOKEN_ADDRESS,
 } from "../utils/consts";
-import { WormholeWrappedInfo } from "../utils/getOriginalAsset";
+import { StateSafeWormholeWrappedInfo } from "../utils/getOriginalAsset";
 
 const LAST_STEP = 3;
 
@@ -88,7 +88,7 @@ export const transferSlice = createSlice({
     },
     setSourceWormholeWrappedInfo: (
       state,
-      action: PayloadAction<WormholeWrappedInfo | undefined>
+      action: PayloadAction<StateSafeWormholeWrappedInfo | undefined>
     ) => {
       if (action.payload) {
         state.isSourceAssetWormholeWrapped = action.payload.isWrapped;

+ 31 - 121
bridge_ui/src/utils/attestFrom.ts

@@ -1,20 +1,16 @@
 import {
+  attestFromEth as attestEthTx,
+  attestFromSolana as attestSolanaTx,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
-  ixFromRust,
-  Bridge__factory,
-  Implementation__factory,
+  getEmitterAddressEth,
+  getEmitterAddressSolana,
+  parseSequenceFromLogEth,
+  parseSequenceFromLogSolana,
 } from "@certusone/wormhole-sdk";
 import Wallet from "@project-serum/sol-wallet-adapter";
-import {
-  Connection,
-  Keypair,
-  PublicKey,
-  SystemProgram,
-  Transaction,
-} from "@solana/web3.js";
+import { Connection } from "@solana/web3.js";
 import { ethers } from "ethers";
-import { arrayify, zeroPad } from "ethers/lib/utils";
 import {
   ETH_BRIDGE_ADDRESS,
   ETH_TOKEN_BRIDGE_ADDRESS,
@@ -23,141 +19,55 @@ import {
   SOL_TOKEN_BRIDGE_ADDRESS,
 } from "./consts";
 import { getSignedVAAWithRetry } from "./getSignedVAAWithRetry";
+import { signSendConfirmAndGet } from "./solana";
 
-// 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");
+  if (!signer) return;
+  const receipt = await attestEthTx(
+    ETH_TOKEN_BRIDGE_ADDRESS,
+    signer,
+    tokenAddress
+  );
+  const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
+  const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
   const { vaaBytes } = await getSignedVAAWithRetry(
     CHAIN_ID_ETH,
     emitterAddress,
-    sequence.toString()
+    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("@certusone/wormhole-sdk/lib/solana/core/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(
-    "@certusone/wormhole-sdk/lib/solana/token/token_bridge"
+  const transaction = await attestSolanaTx(
+    connection,
+    SOL_BRIDGE_ADDRESS,
+    SOL_TOKEN_BRIDGE_ADDRESS,
+    payerAddress,
+    mintAddress
   );
-  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");
+  const info = await signSendConfirmAndGet(wallet, connection, transaction);
+  if (!info) {
+    throw new Error("An error occurred while fetching the transaction info");
   }
-  console.log("SEQ", sequence);
-  const emitterAddress = Buffer.from(
-    zeroPad(
-      new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
-      32
-    )
-  ).toString("hex");
+  const sequence = parseSequenceFromLogSolana(info);
+  const emitterAddress = await getEmitterAddressSolana(
+    SOL_TOKEN_BRIDGE_ADDRESS
+  );
   const { vaaBytes } = await getSignedVAAWithRetry(
     CHAIN_ID_SOLANA,
     emitterAddress,
-    sequence.toString()
+    sequence
   );
-  console.log("SIGNED VAA:", vaaBytes);
   return vaaBytes;
 }
-
-const attestFrom = {
-  [CHAIN_ID_ETH]: attestFromEth,
-  [CHAIN_ID_SOLANA]: attestFromSolana,
-};
-
-export default attestFrom;

+ 19 - 56
bridge_ui/src/utils/createWrappedOn.ts

@@ -1,32 +1,25 @@
+import {
+  postVaaSolana,
+  createWrappedOnEth as createWrappedOnEthTx,
+  createWrappedOnSolana as createWrappedOnSolanaTx,
+} from "@certusone/wormhole-sdk";
 import Wallet from "@project-serum/sol-wallet-adapter";
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { Connection } from "@solana/web3.js";
+import { ethers } from "ethers";
 import {
+  ETH_TOKEN_BRIDGE_ADDRESS,
   SOLANA_HOST,
   SOL_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
-  ETH_TOKEN_BRIDGE_ADDRESS,
 } from "./consts";
-import { ethers } from "ethers";
-import { postVaa } from "./postVaa";
-import {
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  ixFromRust,
-  Bridge__factory,
-} from "@certusone/wormhole-sdk";
+import { signSendAndConfirm } from "./solana";
 
 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);
+  if (!signer) return;
+  await createWrappedOnEthTx(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
 }
 
 export async function createWrappedOnSolana(
@@ -35,51 +28,21 @@ export async function createWrappedOnSolana(
   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(
-    "@certusone/wormhole-sdk/lib/solana/token/token_bridge"
-  );
-
-  await postVaa(
+  await postVaaSolana(
     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 = await createWrappedOnSolanaTx(
+    connection,
+    SOL_BRIDGE_ADDRESS,
+    SOL_TOKEN_BRIDGE_ADDRESS,
+    payerAddress,
+    signedVAA
   );
-  console.log(ix.keys.map((x) => x.pubkey.toString()));
-  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);
+  await signSendAndConfirm(wallet, connection, transaction);
 }
-
-const createWrappedOn = {
-  [CHAIN_ID_SOLANA]: createWrappedOnSolana,
-  [CHAIN_ID_ETH]: createWrappedOnEth,
-};
-
-export default createWrappedOn;

+ 13 - 32
bridge_ui/src/utils/getForeignAsset.ts

@@ -1,7 +1,8 @@
 import {
   ChainId,
   CHAIN_ID_SOLANA,
-  Bridge__factory,
+  getForeignAssetEth as getForeignAssetEthTx,
+  getForeignAssetSol as getForeignAssetSolTx,
 } from "@certusone/wormhole-sdk";
 import { Connection, PublicKey } from "@solana/web3.js";
 import { ethers } from "ethers";
@@ -12,22 +13,11 @@ import {
   SOL_TOKEN_BRIDGE_ADDRESS,
 } from "./consts";
 
-/**
- * Returns a foreign asset address on Ethereum for a provided native chain and asset address
- * @param provider
- * @param originChain
- * @param originAsset
- * @returns
- */
 export async function getForeignAssetEth(
   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(
@@ -36,42 +26,33 @@ export async function getForeignAssetEth(
         : arrayify(originAsset),
       32
     );
-    return await tokenBridge.wrappedAsset(originChain, originAssetBytes);
+    return await getForeignAssetEthTx(
+      ETH_TOKEN_BRIDGE_ADDRESS,
+      provider,
+      originChain,
+      originAssetBytes
+    );
   } catch (e) {
     return ethers.constants.AddressZero;
   }
 }
 
-/**
- * Returns a foreign asset address on Solana for a provided native chain and asset address
- * @param originChain
- * @param originAsset
- * @returns
- */
 export async function getForeignAssetSol(
   originChain: ChainId,
   originAsset: string
 ) {
   if (!isHexString(originAsset)) return null;
-  const { wrapped_address } = await import(
-    "@certusone/wormhole-sdk/lib/solana/token/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
+  return await getForeignAssetSolTx(
+    connection,
+    SOL_TOKEN_BRIDGE_ADDRESS,
+    originChain,
+    originAssetBytes
   );
-  console.log("WAAI", wrappedAssetAccountInfo);
-  return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
 }

+ 7 - 43
bridge_ui/src/utils/getIsWrappedAsset.ts

@@ -1,49 +1,13 @@
-import { Bridge__factory } from "@certusone/wormhole-sdk";
-import { Connection, PublicKey } from "@solana/web3.js";
-import { ethers } from "ethers";
-import {
-  ETH_TOKEN_BRIDGE_ADDRESS,
-  SOLANA_HOST,
-  SOL_TOKEN_BRIDGE_ADDRESS,
-} from "./consts";
+import { getIsWrappedAssetSol as getIsWrappedAssetSolTx } from "@certusone/wormhole-sdk";
+import { Connection } from "@solana/web3.js";
+import { SOLANA_HOST, SOL_TOKEN_BRIDGE_ADDRESS } from "./consts";
 
-/**
- * Returns whether or not an asset address on Ethereum is a wormhole wrapped asset
- * @param provider
- * @param assetAddress
- * @returns
- */
-export async function getIsWrappedAssetEth(
-  provider: ethers.providers.Web3Provider,
-  assetAddress: string
-) {
-  if (!assetAddress) return false;
-  const tokenBridge = Bridge__factory.connect(
-    ETH_TOKEN_BRIDGE_ADDRESS,
-    provider
-  );
-  return await tokenBridge.isWrappedAsset(assetAddress);
-}
-
-/**
- * Returns whether or not an asset on Solana is a wormhole wrapped asset
- * @param assetAddress
- * @returns
- */
 export async function getIsWrappedAssetSol(mintAddress: string) {
-  if (!mintAddress) return false;
-  const { wrapped_meta_address } = await import(
-    "@certusone/wormhole-sdk/lib/solana/token/token_bridge"
-  );
-  const wrappedMetaAddress = wrapped_meta_address(
-    SOL_TOKEN_BRIDGE_ADDRESS,
-    new PublicKey(mintAddress).toBytes()
-  );
-  const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
   // TODO: share connection in context?
   const connection = new Connection(SOLANA_HOST, "confirmed");
-  const wrappedMetaAccountInfo = await connection.getAccountInfo(
-    wrappedMetaAddressPK
+  return await getIsWrappedAssetSolTx(
+    connection,
+    SOL_TOKEN_BRIDGE_ADDRESS,
+    mintAddress
   );
-  return !!wrappedMetaAccountInfo;
 }

+ 34 - 64
bridge_ui/src/utils/getOriginalAsset.ts

@@ -1,84 +1,54 @@
 import {
   ChainId,
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  TokenImplementation__factory,
+  getOriginalAssetEth as getOriginalAssetEthTx,
+  getOriginalAssetSol as getOriginalAssetSolTx,
+  WormholeWrappedInfo,
 } from "@certusone/wormhole-sdk";
-import { Connection, PublicKey } from "@solana/web3.js";
+import { Connection } from "@solana/web3.js";
 import { ethers } from "ethers";
-import { arrayify } from "ethers/lib/utils";
 import { uint8ArrayToHex } from "./array";
-import { SOLANA_HOST, SOL_TOKEN_BRIDGE_ADDRESS } from "./consts";
-import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
+import {
+  ETH_TOKEN_BRIDGE_ADDRESS,
+  SOLANA_HOST,
+  SOL_TOKEN_BRIDGE_ADDRESS,
+} from "./consts";
 
-export interface WormholeWrappedInfo {
+export interface StateSafeWormholeWrappedInfo {
   isWrapped: boolean;
   chainId: ChainId;
   assetAddress: string;
 }
 
-/**
- * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
- * @param provider
- * @param wrappedAddress
- * @returns
- */
+const makeStateSafe = (
+  info: WormholeWrappedInfo
+): StateSafeWormholeWrappedInfo => ({
+  ...info,
+  assetAddress: uint8ArrayToHex(info.assetAddress),
+});
+
 export async function getOriginalAssetEth(
   provider: ethers.providers.Web3Provider,
   wrappedAddress: string
-): Promise<WormholeWrappedInfo> {
-  const isWrapped = await getIsWrappedAssetEth(provider, wrappedAddress);
-  if (isWrapped) {
-    const token = TokenImplementation__factory.connect(
-      wrappedAddress,
-      provider
-    );
-    const chainId = (await token.chainId()) as ChainId; // origin chain
-    const assetAddress = await token.nativeContract(); // origin address
-    // TODO: type this?
-    return {
-      isWrapped: true,
-      chainId,
-      assetAddress: uint8ArrayToHex(arrayify(assetAddress)),
-    };
-  }
-  return {
-    isWrapped: false,
-    chainId: CHAIN_ID_ETH,
-    assetAddress: wrappedAddress,
-  };
+): Promise<StateSafeWormholeWrappedInfo> {
+  return makeStateSafe(
+    await getOriginalAssetEthTx(
+      ETH_TOKEN_BRIDGE_ADDRESS,
+      provider,
+      wrappedAddress
+    )
+  );
 }
 
 export async function getOriginalAssetSol(
   mintAddress: string
-): Promise<WormholeWrappedInfo> {
-  if (mintAddress) {
-    // TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
-    const { parse_wrapped_meta, wrapped_meta_address } = await import(
-      "@certusone/wormhole-sdk/lib/solana/token/token_bridge"
-    );
-    const wrappedMetaAddress = wrapped_meta_address(
+): Promise<StateSafeWormholeWrappedInfo> {
+  // TODO: share connection in context?
+  const connection = new Connection(SOLANA_HOST, "confirmed");
+  return makeStateSafe(
+    await getOriginalAssetSolTx(
+      connection,
       SOL_TOKEN_BRIDGE_ADDRESS,
-      new PublicKey(mintAddress).toBytes()
-    );
-    const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
-    // TODO: share connection in context?
-    const connection = new Connection(SOLANA_HOST, "confirmed");
-    const wrappedMetaAccountInfo = await connection.getAccountInfo(
-      wrappedMetaAddressPK
-    );
-    if (wrappedMetaAccountInfo) {
-      const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data);
-      return {
-        isWrapped: true,
-        chainId: parsed.chain,
-        assetAddress: uint8ArrayToHex(parsed.token_address),
-      };
-    }
-  }
-  return {
-    isWrapped: false,
-    chainId: CHAIN_ID_SOLANA,
-    assetAddress: mintAddress,
-  };
+      mintAddress
+    )
+  );
 }

+ 18 - 105
bridge_ui/src/utils/redeemOn.ts

@@ -1,16 +1,10 @@
 import {
-  Bridge__factory,
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  ixFromRust,
+  postVaaSolana,
+  redeemOnEth as redeemOnEthTx,
+  redeemOnSolana as redeemOnSolanaTx,
 } from "@certusone/wormhole-sdk";
 import Wallet from "@project-serum/sol-wallet-adapter";
-import {
-  ASSOCIATED_TOKEN_PROGRAM_ID,
-  Token,
-  TOKEN_PROGRAM_ID,
-} from "@solana/spl-token";
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { Connection } from "@solana/web3.js";
 import { ethers } from "ethers";
 import {
   ETH_TOKEN_BRIDGE_ADDRESS,
@@ -18,20 +12,14 @@ import {
   SOL_BRIDGE_ADDRESS,
   SOL_TOKEN_BRIDGE_ADDRESS,
 } from "./consts";
-import { postVaa } from "./postVaa";
+import { signSendAndConfirm } from "./solana";
 
 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);
+  if (!signer) return;
+  await redeemOnEthTx(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
 }
 
 export async function redeemOnSolana(
@@ -42,98 +30,23 @@ export async function redeemOnSolana(
   mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
 ) {
   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, complete_transfer_native_ix } =
-    await import("@certusone/wormhole-sdk/lib/solana/token/token_bridge");
-
-  await postVaa(
+  await postVaaSolana(
     connection,
     wallet,
     SOL_BRIDGE_ADDRESS,
     payerAddress,
     Buffer.from(signedVAA)
   );
-  console.log(Buffer.from(signedVAA).toString("hex"));
-  const ixs = [];
-  if (isSolanaNative) {
-    console.log("COMPLETE TRANSFER NATIVE");
-    ixs.push(
-      ixFromRust(
-        complete_transfer_native_ix(
-          SOL_TOKEN_BRIDGE_ADDRESS,
-          SOL_BRIDGE_ADDRESS,
-          payerAddress,
-          signedVAA
-        )
-      )
-    );
-  } else {
-    // TODO: we should always do this, they could buy wrapped somewhere else and transfer it back for the first time, but again, do it based on vaa
-    if (mintAddress) {
-      console.log("CHECK ASSOCIATED TOKEN ACCOUNT FOR", mintAddress);
-      const mintPublicKey = new PublicKey(mintAddress);
-      const associatedAddress = await Token.getAssociatedTokenAddress(
-        ASSOCIATED_TOKEN_PROGRAM_ID,
-        TOKEN_PROGRAM_ID,
-        mintPublicKey,
-        wallet.publicKey
-      );
-      const associatedAddressInfo = await connection.getAccountInfo(
-        associatedAddress
-      );
-      console.log(
-        "CREATE ASSOCIATED TOKEN ACCOUNT",
-        associatedAddress.toString()
-      );
-      if (!associatedAddressInfo) {
-        ixs.push(
-          await Token.createAssociatedTokenAccountInstruction(
-            ASSOCIATED_TOKEN_PROGRAM_ID,
-            TOKEN_PROGRAM_ID,
-            mintPublicKey,
-            associatedAddress,
-            wallet.publicKey,
-            wallet.publicKey
-          )
-        );
-      }
-    }
-    console.log("COMPLETE TRANSFER WRAPPED");
-    ixs.push(
-      ixFromRust(
-        complete_transfer_wrapped_ix(
-          SOL_TOKEN_BRIDGE_ADDRESS,
-          SOL_BRIDGE_ADDRESS,
-          payerAddress,
-          signedVAA
-        )
-      )
-    );
-  }
-  const transaction = new Transaction().add(...ixs);
-  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 transaction = await redeemOnSolanaTx(
+    connection,
+    SOL_BRIDGE_ADDRESS,
+    SOL_TOKEN_BRIDGE_ADDRESS,
+    payerAddress,
+    signedVAA,
+    isSolanaNative,
+    mintAddress
+  );
+  await signSendAndConfirm(wallet, connection, transaction);
 }
-
-const redeemOn = {
-  [CHAIN_ID_ETH]: redeemOnEth,
-  [CHAIN_ID_SOLANA]: redeemOnSolana,
-};
-
-export default redeemOn;

+ 22 - 0
bridge_ui/src/utils/solana.ts

@@ -0,0 +1,22 @@
+import Wallet from "@project-serum/sol-wallet-adapter";
+import { Connection, Transaction } from "@solana/web3.js";
+
+export async function signSendAndConfirm(
+  wallet: Wallet,
+  connection: Connection,
+  transaction: Transaction
+) {
+  const signed = await wallet.signTransaction(transaction);
+  const txid = await connection.sendRawTransaction(signed.serialize());
+  await connection.confirmTransaction(txid);
+  return txid;
+}
+
+export async function signSendConfirmAndGet(
+  wallet: Wallet,
+  connection: Connection,
+  transaction: Transaction
+) {
+  const txid = await signSendAndConfirm(wallet, connection, transaction);
+  return await connection.getTransaction(txid);
+}

+ 41 - 185
bridge_ui/src/utils/transferFrom.ts

@@ -1,23 +1,18 @@
 import {
-  Bridge__factory,
   ChainId,
   CHAIN_ID_ETH,
   CHAIN_ID_SOLANA,
-  Implementation__factory,
-  ixFromRust,
-  TokenImplementation__factory,
+  getEmitterAddressEth,
+  getEmitterAddressSolana,
+  parseSequenceFromLogEth,
+  parseSequenceFromLogSolana,
+  transferFromEth as transferFromEthTx,
+  transferFromSolana as transferFromSolanaTx,
 } from "@certusone/wormhole-sdk";
 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 { Connection } from "@solana/web3.js";
 import { ethers } from "ethers";
-import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils";
+import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils";
 import { hexToUint8Array } from "./array";
 import {
   ETH_BRIDGE_ADDRESS,
@@ -27,11 +22,10 @@ import {
   SOL_TOKEN_BRIDGE_ADDRESS,
 } from "./consts";
 import { getSignedVAAWithRetry } from "./getSignedVAAWithRetry";
+import { signSendConfirmAndGet } from "./solana";
 
-// 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,
@@ -39,69 +33,27 @@ export async function transferFromEth(
   recipientChain: ChainId,
   recipientAddress: Uint8Array | undefined
 ) {
-  if (!provider || !signer || !recipientAddress) return;
+  if (!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(
+  const receipt = await transferFromEthTx(
     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(
+    signer,
     tokenAddress,
     amountParsed,
     recipientChain,
-    recipientAddress,
-    fee,
-    nonceBuffer
+    recipientAddress
   );
-  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 sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
+  const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
   const { vaaBytes } = await getSignedVAAWithRetry(
     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
@@ -111,7 +63,7 @@ export async function transferFromSolana(
   decimals: number,
   targetAddressStr: string | undefined,
   targetChain: ChainId,
-  originAddress?: string,
+  originAddressStr?: string,
   originChain?: ChainId
 ) {
   if (
@@ -120,137 +72,41 @@ export async function transferFromSolana(
     !payerAddress ||
     !fromAddress ||
     !targetAddressStr ||
-    (originChain && !originAddress)
+    (originChain && !originAddressStr)
   )
     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("@certusone/wormhole-sdk/lib/solana/core/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,
-    transfer_wrapped_ix,
-    approval_authority_address,
-    emitter_address,
-  } = await import("@certusone/wormhole-sdk/lib/solana/token/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 isSolanaNative =
-    originChain === undefined || originChain === CHAIN_ID_SOLANA;
-  console.log(isSolanaNative ? "SENDING NATIVE" : "SENDING WRAPPED");
-  const ix = ixFromRust(
-    isSolanaNative
-      ? transfer_native_ix(
-          SOL_TOKEN_BRIDGE_ADDRESS,
-          SOL_BRIDGE_ADDRESS,
-          payerAddress,
-          messageKey.publicKey.toString(),
-          fromAddress,
-          mintAddress,
-          nonce,
-          amountParsed,
-          fee,
-          targetAddress,
-          targetChain
-        )
-      : transfer_wrapped_ix(
-          SOL_TOKEN_BRIDGE_ADDRESS,
-          SOL_BRIDGE_ADDRESS,
-          payerAddress,
-          messageKey.publicKey.toString(),
-          fromAddress,
-          payerAddress,
-          originChain as number, // checked by isSolanaNative
-          zeroPad(hexToUint8Array(originAddress as string), 32), // checked by initial check
-          nonce,
-          amountParsed,
-          fee,
-          targetAddress,
-          targetChain
-        )
+  const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
+  const amountParsed = parseUnits(amount, decimals).toBigInt();
+  const originAddress = originAddressStr
+    ? zeroPad(hexToUint8Array(originAddressStr), 32)
+    : undefined;
+  const transaction = await transferFromSolanaTx(
+    connection,
+    SOL_BRIDGE_ADDRESS,
+    SOL_TOKEN_BRIDGE_ADDRESS,
+    payerAddress,
+    fromAddress,
+    mintAddress,
+    amountParsed,
+    targetAddress,
+    targetChain,
+    originAddress,
+    originChain
   );
-  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");
+  const info = await signSendConfirmAndGet(wallet, connection, transaction);
+  if (!info) {
+    throw new Error("An error occurred while fetching the transaction info");
   }
-  console.log("SEQ", sequence);
-  const emitterAddress = Buffer.from(
-    zeroPad(
-      new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
-      32
-    )
-  ).toString("hex");
+  const sequence = parseSequenceFromLogSolana(info);
+  const emitterAddress = await getEmitterAddressSolana(
+    SOL_TOKEN_BRIDGE_ADDRESS
+  );
   const { vaaBytes } = await getSignedVAAWithRetry(
     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;

+ 131 - 90
sdk/js/package-lock.json

@@ -11,6 +11,8 @@
       "license": "Apache-2.0",
       "dependencies": {
         "@improbable-eng/grpc-web": "^0.14.0",
+        "@project-serum/sol-wallet-adapter": "^0.2.5",
+        "@solana/spl-token": "^0.1.8",
         "@solana/web3.js": "^1.24.0",
         "protobufjs": "^6.11.2",
         "rxjs": "^7.3.0"
@@ -19,7 +21,9 @@
         "@openzeppelin/contracts": "^4.2.0",
         "@typechain/ethers-v5": "^7.0.1",
         "@types/long": "^4.0.1",
+        "@types/node": "^16.6.1",
         "copy-dir": "^1.3.0",
+        "ethers": "^5.4.4",
         "prettier": "^2.3.2",
         "tslint": "^6.1.3",
         "tslint-config-prettier": "^1.18.0",
@@ -87,7 +91,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/address": "^5.4.0",
         "@ethersproject/bignumber": "^5.4.0",
@@ -115,7 +118,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -141,7 +143,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abstract-provider": "^5.4.0",
         "@ethersproject/bignumber": "^5.4.0",
@@ -165,7 +166,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -189,7 +189,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0"
       }
@@ -209,7 +208,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/properties": "^5.4.0"
@@ -230,7 +228,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0",
@@ -241,8 +238,7 @@
       "version": "4.12.0",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
       "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/@ethersproject/bytes": {
       "version": "5.4.0",
@@ -259,7 +255,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/logger": "^5.4.0"
       }
@@ -279,7 +274,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bignumber": "^5.4.0"
       }
@@ -299,7 +293,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abi": "^5.4.0",
         "@ethersproject/abstract-provider": "^5.4.0",
@@ -328,7 +321,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abstract-signer": "^5.4.0",
         "@ethersproject/address": "^5.4.0",
@@ -355,7 +347,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abstract-signer": "^5.4.0",
         "@ethersproject/basex": "^5.4.0",
@@ -386,7 +377,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abstract-signer": "^5.4.0",
         "@ethersproject/address": "^5.4.0",
@@ -418,7 +408,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "js-sha3": "0.5.7"
@@ -428,8 +417,7 @@
       "version": "0.5.7",
       "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz",
       "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/@ethersproject/logger": {
       "version": "5.4.0",
@@ -445,8 +433,7 @@
           "type": "individual",
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
-      ],
-      "peer": true
+      ]
     },
     "node_modules/@ethersproject/networks": {
       "version": "5.4.2",
@@ -463,7 +450,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/logger": "^5.4.0"
       }
@@ -483,7 +469,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/sha2": "^5.4.0"
@@ -504,7 +489,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/logger": "^5.4.0"
       }
@@ -524,7 +508,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abstract-provider": "^5.4.0",
         "@ethersproject/abstract-signer": "^5.4.0",
@@ -552,7 +535,6 @@
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
       "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
       "dev": true,
-      "peer": true,
       "engines": {
         "node": ">=8.3.0"
       },
@@ -584,7 +566,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0"
@@ -605,7 +586,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0"
@@ -626,7 +606,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0",
@@ -648,7 +627,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0",
@@ -662,8 +640,7 @@
       "version": "4.12.0",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
       "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/@ethersproject/solidity": {
       "version": "5.4.0",
@@ -680,7 +657,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -704,7 +680,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/constants": "^5.4.0",
@@ -726,7 +701,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/address": "^5.4.0",
         "@ethersproject/bignumber": "^5.4.0",
@@ -754,7 +728,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/constants": "^5.4.0",
@@ -776,7 +749,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/abstract-provider": "^5.4.0",
         "@ethersproject/abstract-signer": "^5.4.0",
@@ -810,7 +782,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/base64": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -834,7 +805,6 @@
           "url": "https://www.buymeacoffee.com/ricmoo"
         }
       ],
-      "peer": true,
       "dependencies": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/hash": "^5.4.0",
@@ -862,6 +832,21 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@project-serum/sol-wallet-adapter": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.2.5.tgz",
+      "integrity": "sha512-Y0XHe+FXXJ7P8XZtx3luAlatO0ge2LdrZUCmqMSzJf+K+fko+qTYIBSUuWwO7y/O4brIXVReR1mEUvF6QKDF2w==",
+      "dependencies": {
+        "bs58": "^4.0.1",
+        "eventemitter3": "^4.0.7"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "@solana/web3.js": "^1.5.0"
+      }
+    },
     "node_modules/@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -950,6 +935,45 @@
         "ieee754": "^1.2.1"
       }
     },
+    "node_modules/@solana/spl-token": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz",
+      "integrity": "sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.10.5",
+        "@solana/web3.js": "^1.21.0",
+        "bn.js": "^5.1.0",
+        "buffer": "6.0.3",
+        "buffer-layout": "^1.2.0",
+        "dotenv": "10.0.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@solana/spl-token/node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
     "node_modules/@solana/web3.js": {
       "version": "1.24.0",
       "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.24.0.tgz",
@@ -1066,8 +1090,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
       "integrity": "sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/ansi-styles": {
       "version": "3.2.1",
@@ -1153,8 +1176,7 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
       "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/bn.js": {
       "version": "5.2.0",
@@ -1223,6 +1245,14 @@
         "ieee754": "^1.2.1"
       }
     },
+    "node_modules/buffer-layout": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz",
+      "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==",
+      "engines": {
+        "node": ">=4.5"
+      }
+    },
     "node_modules/bufferutil": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
@@ -1372,6 +1402,14 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/dotenv": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
+      "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/elliptic": {
       "version": "6.5.4",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
@@ -1442,7 +1480,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@ethersproject/abi": "5.4.0",
         "@ethersproject/abstract-provider": "5.4.1",
@@ -2010,8 +2047,7 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
       "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/secp256k1": {
       "version": "4.0.2",
@@ -2366,7 +2402,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.4.0.tgz",
       "integrity": "sha512-9gU2H+/yK1j2eVMdzm6xvHSnMxk8waIHQGYCZg5uvAyH0rsAzxkModzBSpbAkAuhKFEovC2S9hM4nPuLym8IZw==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/address": "^5.4.0",
         "@ethersproject/bignumber": "^5.4.0",
@@ -2384,7 +2419,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.4.1.tgz",
       "integrity": "sha512-3EedfKI3LVpjSKgAxoUaI+gB27frKsxzm+r21w9G60Ugk+3wVLQwhi1LsEJAKNV7WoZc8CIpNrATlL1QFABjtQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -2400,7 +2434,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz",
       "integrity": "sha512-SkkFL5HVq1k4/25dM+NWP9MILgohJCgGv5xT5AcRruGz4ILpfHeBtO/y6j+Z3UN/PAjDeb4P7E51Yh8wcGNLGA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abstract-provider": "^5.4.0",
         "@ethersproject/bignumber": "^5.4.0",
@@ -2414,7 +2447,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.4.0.tgz",
       "integrity": "sha512-SD0VgOEkcACEG/C6xavlU1Hy3m5DGSXW3CUHkaaEHbAPPsgi0coP5oNPsxau8eTlZOk/bpa/hKeCNoK5IzVI2Q==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -2428,7 +2460,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.4.0.tgz",
       "integrity": "sha512-CjQw6E17QDSSC5jiM9YpF7N1aSCHmYGMt9bWD8PWv6YPMxjsys2/Q8xLrROKI3IWJ7sFfZ8B3flKDTM5wlWuZQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0"
       }
@@ -2438,7 +2469,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.4.0.tgz",
       "integrity": "sha512-J07+QCVJ7np2bcpxydFVf/CuYo9mZ7T73Pe7KQY4c1lRlrixMeblauMxHXD0MPwFmUHZIILDNViVkykFBZylbg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/properties": "^5.4.0"
@@ -2449,7 +2479,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.1.tgz",
       "integrity": "sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0",
@@ -2460,8 +2489,7 @@
           "version": "4.12.0",
           "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
           "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
-          "dev": true,
-          "peer": true
+          "dev": true
         }
       }
     },
@@ -2470,7 +2498,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.4.0.tgz",
       "integrity": "sha512-H60ceqgTHbhzOj4uRc/83SCN9d+BSUnOkrr2intevqdtEMO1JFVZ1XL84OEZV+QjV36OaZYxtnt4lGmxcGsPfA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/logger": "^5.4.0"
       }
@@ -2480,7 +2507,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.4.0.tgz",
       "integrity": "sha512-tzjn6S7sj9+DIIeKTJLjK9WGN2Tj0P++Z8ONEIlZjyoTkBuODN+0VfhAyYksKi43l1Sx9tX2VlFfzjfmr5Wl3Q==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bignumber": "^5.4.0"
       }
@@ -2490,7 +2516,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.4.1.tgz",
       "integrity": "sha512-m+z2ZgPy4pyR15Je//dUaymRUZq5MtDajF6GwFbGAVmKz/RF+DNIPwF0k5qEcL3wPGVqUjFg2/krlCRVTU4T5w==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abi": "^5.4.0",
         "@ethersproject/abstract-provider": "^5.4.0",
@@ -2509,7 +2534,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.4.0.tgz",
       "integrity": "sha512-xymAM9tmikKgbktOCjW60Z5sdouiIIurkZUr9oW5NOex5uwxrbsYG09kb5bMcNjlVeJD3yPivTNzViIs1GCbqA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abstract-signer": "^5.4.0",
         "@ethersproject/address": "^5.4.0",
@@ -2526,7 +2550,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.4.0.tgz",
       "integrity": "sha512-pKxdS0KAaeVGfZPp1KOiDLB0jba11tG6OP1u11QnYfb7pXn6IZx0xceqWRr6ygke8+Kw74IpOoSi7/DwANhy8Q==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abstract-signer": "^5.4.0",
         "@ethersproject/basex": "^5.4.0",
@@ -2547,7 +2570,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.4.0.tgz",
       "integrity": "sha512-igWcu3fx4aiczrzEHwG1xJZo9l1cFfQOWzTqwRw/xcvxTk58q4f9M7cjh51EKphMHvrJtcezJ1gf1q1AUOfEQQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abstract-signer": "^5.4.0",
         "@ethersproject/address": "^5.4.0",
@@ -2569,7 +2591,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.4.0.tgz",
       "integrity": "sha512-FBI1plWet+dPUvAzPAeHzRKiPpETQzqSUWR1wXJGHVWi4i8bOSrpC3NwpkPjgeXG7MnugVc1B42VbfnQikyC/A==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "js-sha3": "0.5.7"
@@ -2579,8 +2600,7 @@
           "version": "0.5.7",
           "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz",
           "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=",
-          "dev": true,
-          "peer": true
+          "dev": true
         }
       }
     },
@@ -2588,15 +2608,13 @@
       "version": "5.4.0",
       "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.4.0.tgz",
       "integrity": "sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "@ethersproject/networks": {
       "version": "5.4.2",
       "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.4.2.tgz",
       "integrity": "sha512-eekOhvJyBnuibfJnhtK46b8HimBc5+4gqpvd1/H9LEl7Q7/qhsIhM81dI9Fcnjpk3jB1aTy6bj0hz3cifhNeYw==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/logger": "^5.4.0"
       }
@@ -2606,7 +2624,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.4.0.tgz",
       "integrity": "sha512-x94aIv6tiA04g6BnazZSLoRXqyusawRyZWlUhKip2jvoLpzJuLb//KtMM6PEovE47pMbW+Qe1uw+68ameJjB7g==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/sha2": "^5.4.0"
@@ -2617,7 +2634,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.4.0.tgz",
       "integrity": "sha512-7jczalGVRAJ+XSRvNA6D5sAwT4gavLq3OXPuV/74o3Rd2wuzSL035IMpIMgei4CYyBdialJMrTqkOnzccLHn4A==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/logger": "^5.4.0"
       }
@@ -2627,7 +2643,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.4.3.tgz",
       "integrity": "sha512-VURwkaWPoUj7jq9NheNDT5Iyy64Qcyf6BOFDwVdHsmLmX/5prNjFrgSX3GHPE4z1BRrVerDxe2yayvXKFm/NNg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abstract-provider": "^5.4.0",
         "@ethersproject/abstract-signer": "^5.4.0",
@@ -2655,7 +2670,6 @@
           "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
           "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
           "dev": true,
-          "peer": true,
           "requires": {}
         }
       }
@@ -2665,7 +2679,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.4.0.tgz",
       "integrity": "sha512-pnpWNQlf0VAZDEOVp1rsYQosmv2o0ITS/PecNw+mS2/btF8eYdspkN0vIXrCMtkX09EAh9bdk8GoXmFXM1eAKw==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0"
@@ -2676,7 +2689,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.4.0.tgz",
       "integrity": "sha512-0I7MZKfi+T5+G8atId9QaQKHRvvasM/kqLyAH4XxBCBchAooH2EX5rL9kYZWwcm3awYV+XC7VF6nLhfeQFKVPg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0"
@@ -2687,7 +2699,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.4.0.tgz",
       "integrity": "sha512-siheo36r1WD7Cy+bDdE1BJ8y0bDtqXCOxRMzPa4bV1TGt/eTUUt03BHoJNB6reWJD8A30E/pdJ8WFkq+/uz4Gg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0",
@@ -2699,7 +2710,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.4.0.tgz",
       "integrity": "sha512-q8POUeywx6AKg2/jX9qBYZIAmKSB4ubGXdQ88l40hmATj29JnG5pp331nAWwwxPn2Qao4JpWHNZsQN+bPiSW9A==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/logger": "^5.4.0",
@@ -2713,8 +2723,7 @@
           "version": "4.12.0",
           "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
           "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
-          "dev": true,
-          "peer": true
+          "dev": true
         }
       }
     },
@@ -2723,7 +2732,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.4.0.tgz",
       "integrity": "sha512-XFQTZ7wFSHOhHcV1DpcWj7VXECEiSrBuv7JErJvB9Uo+KfCdc3QtUZV+Vjh/AAaYgezUEKbCtE6Khjm44seevQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -2737,7 +2745,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.4.0.tgz",
       "integrity": "sha512-k/9DkH5UGDhv7aReXLluFG5ExurwtIpUfnDNhQA29w896Dw3i4uDTz01Quaptbks1Uj9kI8wo9tmW73wcIEaWA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/constants": "^5.4.0",
@@ -2749,7 +2756,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.4.0.tgz",
       "integrity": "sha512-s3EjZZt7xa4BkLknJZ98QGoIza94rVjaEed0rzZ/jB9WrIuu/1+tjvYCWzVrystXtDswy7TPBeIepyXwSYa4WQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/address": "^5.4.0",
         "@ethersproject/bignumber": "^5.4.0",
@@ -2767,7 +2773,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.4.0.tgz",
       "integrity": "sha512-Z88krX40KCp+JqPCP5oPv5p750g+uU6gopDYRTBGcDvOASh6qhiEYCRatuM/suC4S2XW9Zz90QI35MfSrTIaFg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bignumber": "^5.4.0",
         "@ethersproject/constants": "^5.4.0",
@@ -2779,7 +2784,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.4.0.tgz",
       "integrity": "sha512-wU29majLjM6AjCjpat21mPPviG+EpK7wY1+jzKD0fg3ui5fgedf2zEu1RDgpfIMsfn8fJHJuzM4zXZ2+hSHaSQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abstract-provider": "^5.4.0",
         "@ethersproject/abstract-signer": "^5.4.0",
@@ -2803,7 +2807,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.4.0.tgz",
       "integrity": "sha512-1bUusGmcoRLYgMn6c1BLk1tOKUIFuTg8j+6N8lYlbMpDesnle+i3pGSagGNvwjaiLo4Y5gBibwctpPRmjrh4Og==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/base64": "^5.4.0",
         "@ethersproject/bytes": "^5.4.0",
@@ -2817,7 +2820,6 @@
       "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.4.0.tgz",
       "integrity": "sha512-FemEkf6a+EBKEPxlzeVgUaVSodU7G0Na89jqKjmWMlDB0tomoU8RlEMgUvXyqtrg8N4cwpLh8nyRnm1Nay1isA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/bytes": "^5.4.0",
         "@ethersproject/hash": "^5.4.0",
@@ -2840,6 +2842,15 @@
       "integrity": "sha512-LD4NnkKpHHSMo5z9MvFsG4g1xxZUDqV3A3Futu3nvyfs4wPwXxqOgMaxOoa2PeyGL2VNeSlbxT54enbQzGcgJQ==",
       "dev": true
     },
+    "@project-serum/sol-wallet-adapter": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.2.5.tgz",
+      "integrity": "sha512-Y0XHe+FXXJ7P8XZtx3luAlatO0ge2LdrZUCmqMSzJf+K+fko+qTYIBSUuWwO7y/O4brIXVReR1mEUvF6QKDF2w==",
+      "requires": {
+        "bs58": "^4.0.1",
+        "eventemitter3": "^4.0.7"
+      }
+    },
     "@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2913,6 +2924,30 @@
         }
       }
     },
+    "@solana/spl-token": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz",
+      "integrity": "sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==",
+      "requires": {
+        "@babel/runtime": "^7.10.5",
+        "@solana/web3.js": "^1.21.0",
+        "bn.js": "^5.1.0",
+        "buffer": "6.0.3",
+        "buffer-layout": "^1.2.0",
+        "dotenv": "10.0.0"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.2.1"
+          }
+        }
+      }
+    },
     "@solana/web3.js": {
       "version": "1.24.0",
       "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.24.0.tgz",
@@ -3011,8 +3046,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
       "integrity": "sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "ansi-styles": {
       "version": "3.2.1",
@@ -3078,8 +3112,7 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
       "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "bn.js": {
       "version": "5.2.0",
@@ -3134,6 +3167,11 @@
         "ieee754": "^1.2.1"
       }
     },
+    "buffer-layout": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz",
+      "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA=="
+    },
     "bufferutil": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
@@ -3251,6 +3289,11 @@
       "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
       "dev": true
     },
+    "dotenv": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
+      "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
+    },
     "elliptic": {
       "version": "6.5.4",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
@@ -3302,7 +3345,6 @@
       "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.4.4.tgz",
       "integrity": "sha512-zaTs8yaDjfb0Zyj8tT6a+/hEkC+kWAA350MWRp6yP5W7NdGcURRPMOpOU+6GtkfxV9wyJEShWesqhE/TjdqpMA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ethersproject/abi": "5.4.0",
         "@ethersproject/abstract-provider": "5.4.1",
@@ -3763,8 +3805,7 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
       "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "secp256k1": {
       "version": "4.0.2",

+ 4 - 0
sdk/js/package.json

@@ -37,7 +37,9 @@
     "@openzeppelin/contracts": "^4.2.0",
     "@typechain/ethers-v5": "^7.0.1",
     "@types/long": "^4.0.1",
+    "@types/node": "^16.6.1",
     "copy-dir": "^1.3.0",
+    "ethers": "^5.4.4",
     "prettier": "^2.3.2",
     "tslint": "^6.1.3",
     "tslint-config-prettier": "^1.18.0",
@@ -45,6 +47,8 @@
   },
   "dependencies": {
     "@improbable-eng/grpc-web": "^0.14.0",
+    "@project-serum/sol-wallet-adapter": "^0.2.5",
+    "@solana/spl-token": "^0.1.8",
     "@solana/web3.js": "^1.24.0",
     "protobufjs": "^6.11.2",
     "rxjs": "^7.3.0"

+ 15 - 0
sdk/js/src/bridge/getEmitterAddress.ts

@@ -0,0 +1,15 @@
+import { PublicKey } from "@solana/web3.js";
+import { arrayify, BytesLike, Hexable, zeroPad } from "ethers/lib/utils";
+
+export function getEmitterAddressEth(
+  contractAddress: number | BytesLike | Hexable
+) {
+  return Buffer.from(zeroPad(arrayify(contractAddress), 32)).toString("hex");
+}
+
+export async function getEmitterAddressSolana(programAddress: string) {
+  const { emitter_address } = await import("../solana/token/token_bridge");
+  return Buffer.from(
+    zeroPad(new PublicKey(emitter_address(programAddress)).toBytes(), 32)
+  ).toString("hex");
+}

+ 2 - 0
sdk/js/src/bridge/index.ts

@@ -0,0 +1,2 @@
+export * from "./getEmitterAddress";
+export * from "./parseSequenceFromLog";

+ 30 - 0
sdk/js/src/bridge/parseSequenceFromLog.ts

@@ -0,0 +1,30 @@
+import { TransactionResponse } from "@solana/web3.js";
+import { ContractReceipt } from "ethers";
+import { Implementation__factory } from "../ethers-contracts";
+
+export function parseSequenceFromLogEth(
+  receipt: ContractReceipt,
+  bridgeAddress: string
+): string {
+  // TODO: dangerous!(?)
+  const bridgeLog = receipt.logs.filter((l) => {
+    console.log(l.address, bridgeAddress);
+    return l.address === bridgeAddress;
+  })[0];
+  const {
+    args: { sequence },
+  } = Implementation__factory.createInterface().parseLog(bridgeLog);
+  return sequence.toString();
+}
+
+const SOLANA_SEQ_LOG = "Program log: Sequence: ";
+export function parseSequenceFromLogSolana(info: TransactionResponse) {
+  // TODO: better parsing, safer
+  const sequence = info.meta?.logMessages
+    ?.filter((msg) => msg.startsWith(SOLANA_SEQ_LOG))[0]
+    .replace(SOLANA_SEQ_LOG, "");
+  if (!sequence) {
+    throw new Error("sequence not found");
+  }
+  return sequence.toString();
+}

+ 2 - 0
sdk/js/src/index.ts

@@ -2,3 +2,5 @@ export * from "./ethers-contracts";
 export * from "./solana";
 export * from "./rpc";
 export * from "./utils";
+export * from "./bridge";
+export * from "./token_bridge";

+ 24 - 0
sdk/js/src/solana/getBridgeFeeIx.ts

@@ -0,0 +1,24 @@
+import { Connection, PublicKey, SystemProgram } from "@solana/web3.js";
+
+export async function getBridgeFeeIx(
+  connection: Connection,
+  bridgeAddress: string,
+  payerAddress: string
+) {
+  const bridge = await import("./core/bridge");
+  const feeAccount = await bridge.fee_collector_address(bridgeAddress);
+  const bridgeStatePK = new PublicKey(bridge.state_address(bridgeAddress));
+  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,
+  });
+  return transferIx;
+}

+ 2 - 0
sdk/js/src/solana/index.ts

@@ -1 +1,3 @@
+export * from "./getBridgeFeeIx";
+export { postVaa as postVaaSolana } from "./postVaa";
 export * from "./rust";

+ 6 - 13
bridge_ui/src/utils/postVaa.ts → sdk/js/src/solana/postVaa.ts

@@ -6,8 +6,9 @@ import {
   TransactionInstruction,
 } from "@solana/web3.js";
 import Wallet from "@project-serum/sol-wallet-adapter";
-import { ixFromRust } from "@certusone/wormhole-sdk";
+import { ixFromRust } from "./rust";
 
+// is there a better pattern for this?
 export async function postVaa(
   connection: Connection,
   wallet: Wallet,
@@ -20,7 +21,7 @@ export async function postVaa(
     parse_guardian_set,
     verify_signatures_ix,
     post_vaa_ix,
-  } = await import("@certusone/wormhole-sdk/lib/solana/core/bridge");
+  } = await import("./core/bridge");
   let bridge_state = await getBridgeState(connection, bridge_id);
   let guardian_addr = new PublicKey(
     guardian_set_address(bridge_id, bridge_state.guardianSetIndex)
@@ -53,11 +54,8 @@ export async function postVaa(
 
     // 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);
+    await connection.confirmTransaction(txid);
   }
 
   let ix = ixFromRust(
@@ -69,20 +67,15 @@ export async function postVaa(
   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);
+  await connection.confirmTransaction(txid);
 }
 
 async function getBridgeState(
   connection: Connection,
   bridge_id: string
 ): Promise<BridgeState> {
-  const { parse_state, state_address } = await import(
-    "@certusone/wormhole-sdk/lib/solana/core/bridge"
-  );
+  const { parse_state, state_address } = await import("./core/bridge");
   let bridge_state = new PublicKey(state_address(bridge_id));
   let acc = await connection.getAccountInfo(bridge_state);
   if (acc?.data === undefined) {

+ 49 - 0
sdk/js/src/token_bridge/attest.ts

@@ -0,0 +1,49 @@
+import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { getBridgeFeeIx, ixFromRust } from "../solana";
+import { createNonce } from "../utils/createNonce";
+
+export async function attestFromEth(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  tokenAddress: string
+) {
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.attestToken(tokenAddress, createNonce());
+  const receipt = await v.wait();
+  return receipt;
+}
+
+export async function attestFromSolana(
+  connection: Connection,
+  bridgeAddress: string,
+  tokenBridgeAddress: string,
+  payerAddress: string,
+  mintAddress: string
+) {
+  const nonce = createNonce().readUInt32LE(0);
+  const transferIx = await getBridgeFeeIx(
+    connection,
+    bridgeAddress,
+    payerAddress
+  );
+  const { attest_ix } = await import("../solana/token/token_bridge");
+  const messageKey = Keypair.generate();
+  const ix = ixFromRust(
+    attest_ix(
+      tokenBridgeAddress,
+      bridgeAddress,
+      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);
+  return transaction;
+}

+ 38 - 0
sdk/js/src/token_bridge/createWrapped.ts

@@ -0,0 +1,38 @@
+import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { ixFromRust } from "../solana";
+
+export async function createWrappedOnEth(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  signedVAA: Uint8Array
+) {
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.createWrapped(signedVAA);
+  const receipt = await v.wait();
+  return receipt;
+}
+
+export async function createWrappedOnSolana(
+  connection: Connection,
+  bridgeAddress: string,
+  tokenBridgeAddress: string,
+  payerAddress: string,
+  signedVAA: Uint8Array
+) {
+  const { create_wrapped_ix } = await import("../solana/token/token_bridge");
+  const ix = ixFromRust(
+    create_wrapped_ix(
+      tokenBridgeAddress,
+      bridgeAddress,
+      payerAddress,
+      signedVAA
+    )
+  );
+  const transaction = new Transaction().add(ix);
+  const { blockhash } = await connection.getRecentBlockhash();
+  transaction.recentBlockhash = blockhash;
+  transaction.feePayer = new PublicKey(payerAddress);
+  return transaction;
+}

+ 53 - 0
sdk/js/src/token_bridge/getForeignAsset.ts

@@ -0,0 +1,53 @@
+import { Connection, PublicKey } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { ChainId } from "../utils";
+
+/**
+ * Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist
+ * @param tokenBridgeAddress
+ * @param provider
+ * @param originChain
+ * @param originAsset zero pad to 32 bytes
+ * @returns
+ */
+export async function getForeignAssetEth(
+  tokenBridgeAddress: string,
+  provider: ethers.providers.Web3Provider,
+  originChain: ChainId,
+  originAsset: Uint8Array
+) {
+  const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
+  try {
+    return await tokenBridge.wrappedAsset(originChain, originAsset);
+  } catch (e) {
+    return ethers.constants.AddressZero;
+  }
+}
+
+/**
+ * Returns a foreign asset address on Solana for a provided native chain and asset address
+ * @param connection
+ * @param tokenBridgeAddress
+ * @param originChain
+ * @param originAsset zero pad to 32 bytes
+ * @returns
+ */
+export async function getForeignAssetSol(
+  connection: Connection,
+  tokenBridgeAddress: string,
+  originChain: ChainId,
+  originAsset: Uint8Array
+) {
+  const { wrapped_address } = await import("../solana/token/token_bridge");
+  const wrappedAddress = wrapped_address(
+    tokenBridgeAddress,
+    originAsset,
+    originChain
+  );
+  const wrappedAddressPK = new PublicKey(wrappedAddress);
+  const wrappedAssetAccountInfo = await connection.getAccountInfo(
+    wrappedAddressPK
+  );
+  return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
+}

+ 45 - 0
sdk/js/src/token_bridge/getIsWrappedAsset.ts

@@ -0,0 +1,45 @@
+import { Connection, PublicKey } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+
+/**
+ * Returns whether or not an asset address on Ethereum is a wormhole wrapped asset
+ * @param tokenBridgeAddress
+ * @param provider
+ * @param assetAddress
+ * @returns
+ */
+export async function getIsWrappedAssetEth(
+  tokenBridgeAddress: string,
+  provider: ethers.providers.Web3Provider,
+  assetAddress: string
+) {
+  if (!assetAddress) return false;
+  const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
+  return await tokenBridge.isWrappedAsset(assetAddress);
+}
+
+/**
+ * Returns whether or not an asset on Solana is a wormhole wrapped asset
+ * @param connection
+ * @param tokenBridgeAddress
+ * @param mintAddress
+ * @returns
+ */
+export async function getIsWrappedAssetSol(
+  connection: Connection,
+  tokenBridgeAddress: string,
+  mintAddress: string
+) {
+  if (!mintAddress) return false;
+  const { wrapped_meta_address } = await import("../solana/token/token_bridge");
+  const wrappedMetaAddress = wrapped_meta_address(
+    tokenBridgeAddress,
+    new PublicKey(mintAddress).toBytes()
+  );
+  const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
+  const wrappedMetaAccountInfo = await connection.getAccountInfo(
+    wrappedMetaAddressPK
+  );
+  return !!wrappedMetaAccountInfo;
+}

+ 90 - 0
sdk/js/src/token_bridge/getOriginalAsset.ts

@@ -0,0 +1,90 @@
+import { Connection, PublicKey } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { arrayify } from "ethers/lib/utils";
+import { TokenImplementation__factory } from "../ethers-contracts";
+import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils";
+import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
+
+export interface WormholeWrappedInfo {
+  isWrapped: boolean;
+  chainId: ChainId;
+  assetAddress: Uint8Array;
+}
+
+/**
+ * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
+ * @param tokenBridgeAddress
+ * @param provider
+ * @param wrappedAddress
+ * @returns
+ */
+export async function getOriginalAssetEth(
+  tokenBridgeAddress: string,
+  provider: ethers.providers.Web3Provider,
+  wrappedAddress: string
+): Promise<WormholeWrappedInfo> {
+  const isWrapped = await getIsWrappedAssetEth(
+    tokenBridgeAddress,
+    provider,
+    wrappedAddress
+  );
+  if (isWrapped) {
+    const token = TokenImplementation__factory.connect(
+      wrappedAddress,
+      provider
+    );
+    const chainId = (await token.chainId()) as ChainId; // origin chain
+    const assetAddress = await token.nativeContract(); // origin address
+    return {
+      isWrapped: true,
+      chainId,
+      assetAddress: arrayify(assetAddress),
+    };
+  }
+  return {
+    isWrapped: false,
+    chainId: CHAIN_ID_ETH,
+    assetAddress: arrayify(wrappedAddress),
+  };
+}
+
+/**
+ * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
+ * @param connection
+ * @param tokenBridgeAddress
+ * @param mintAddress
+ * @returns
+ */
+export async function getOriginalAssetSol(
+  connection: Connection,
+  tokenBridgeAddress: string,
+  mintAddress: string
+): Promise<WormholeWrappedInfo> {
+  if (mintAddress) {
+    // TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
+    const { parse_wrapped_meta, wrapped_meta_address } = await import(
+      "../solana/token/token_bridge"
+    );
+    const wrappedMetaAddress = wrapped_meta_address(
+      tokenBridgeAddress,
+      new PublicKey(mintAddress).toBytes()
+    );
+    const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
+    const wrappedMetaAccountInfo = await connection.getAccountInfo(
+      wrappedMetaAddressPK
+    );
+    if (wrappedMetaAccountInfo) {
+      const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data);
+      return {
+        isWrapped: true,
+        chainId: parsed.chain,
+        assetAddress: parsed.token_address,
+      };
+    }
+  }
+  return {
+    isWrapped: false,
+    chainId: CHAIN_ID_SOLANA,
+    assetAddress: new Uint8Array(32),
+  };
+}

+ 7 - 0
sdk/js/src/token_bridge/index.ts

@@ -0,0 +1,7 @@
+export * from "./attest";
+export * from "./createWrapped";
+export * from "./getForeignAsset";
+export * from "./getIsWrappedAsset";
+export * from "./getOriginalAsset";
+export * from "./redeem";
+export * from "./transfer";

+ 90 - 0
sdk/js/src/token_bridge/redeem.ts

@@ -0,0 +1,90 @@
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  Token,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { ixFromRust } from "../solana";
+
+export async function redeemOnEth(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  signedVAA: Uint8Array
+) {
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.completeTransfer(signedVAA);
+  const receipt = await v.wait();
+  return receipt;
+}
+
+export async function redeemOnSolana(
+  connection: Connection,
+  bridgeAddress: string,
+  tokenBridgeAddress: string,
+  payerAddress: string,
+  signedVAA: Uint8Array,
+  isSolanaNative: boolean,
+  mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
+) {
+  const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
+    await import("../solana/token/token_bridge");
+  const ixs = [];
+  if (isSolanaNative) {
+    console.log("COMPLETE TRANSFER NATIVE");
+    ixs.push(
+      ixFromRust(
+        complete_transfer_native_ix(
+          tokenBridgeAddress,
+          bridgeAddress,
+          payerAddress,
+          signedVAA
+        )
+      )
+    );
+  } else {
+    // TODO: we should always do this, they could buy wrapped somewhere else and transfer it back for the first time, but again, do it based on vaa
+    if (mintAddress) {
+      const mintPublicKey = new PublicKey(mintAddress);
+      // TODO: re: todo above, this should be swapped for the address from the vaa (may not be the same as the payer)
+      const payerPublicKey = new PublicKey(payerAddress);
+      const associatedAddress = await Token.getAssociatedTokenAddress(
+        ASSOCIATED_TOKEN_PROGRAM_ID,
+        TOKEN_PROGRAM_ID,
+        mintPublicKey,
+        payerPublicKey
+      );
+      const associatedAddressInfo = await connection.getAccountInfo(
+        associatedAddress
+      );
+      if (!associatedAddressInfo) {
+        ixs.push(
+          await Token.createAssociatedTokenAccountInstruction(
+            ASSOCIATED_TOKEN_PROGRAM_ID,
+            TOKEN_PROGRAM_ID,
+            mintPublicKey,
+            associatedAddress,
+            payerPublicKey, // owner
+            payerPublicKey // payer
+          )
+        );
+      }
+    }
+    ixs.push(
+      ixFromRust(
+        complete_transfer_wrapped_ix(
+          tokenBridgeAddress,
+          bridgeAddress,
+          payerAddress,
+          signedVAA
+        )
+      )
+    );
+  }
+  const transaction = new Transaction().add(...ixs);
+  const { blockhash } = await connection.getRecentBlockhash();
+  transaction.recentBlockhash = blockhash;
+  transaction.feePayer = new PublicKey(payerAddress);
+  return transaction;
+}

+ 118 - 0
sdk/js/src/token_bridge/transfer.ts

@@ -0,0 +1,118 @@
+import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
+import { ethers } from "ethers";
+import {
+  Bridge__factory,
+  TokenImplementation__factory,
+} from "../ethers-contracts";
+import { getBridgeFeeIx, ixFromRust } from "../solana";
+import { ChainId, CHAIN_ID_SOLANA, createNonce } from "../utils";
+
+export async function transferFromEth(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  tokenAddress: string,
+  amount: ethers.BigNumberish,
+  recipientChain: ChainId,
+  recipientAddress: Uint8Array
+) {
+  //TODO: should we check if token attestation exists on the target chain
+  const token = TokenImplementation__factory.connect(tokenAddress, signer);
+  //TODO: implement / separate allowance check
+  // const signerAddress = await signer.getAddress();
+  // const allowance = await token.allowance(
+  //   signerAddress,
+  //   tokenBridgeAddress
+  // );
+  await token.approve(tokenBridgeAddress, amount);
+  const fee = 0; // for now, this won't do anything, we may add later
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.transferTokens(
+    tokenAddress,
+    amount,
+    recipientChain,
+    recipientAddress,
+    fee,
+    createNonce()
+  );
+  const receipt = await v.wait();
+  return receipt;
+}
+
+export async function transferFromSolana(
+  connection: Connection,
+  bridgeAddress: string,
+  tokenBridgeAddress: string,
+  payerAddress: string,
+  fromAddress: string,
+  mintAddress: string,
+  amount: BigInt,
+  targetAddress: Uint8Array,
+  targetChain: ChainId,
+  originAddress?: Uint8Array,
+  originChain?: ChainId
+) {
+  const nonce = createNonce().readUInt32LE(0);
+  const fee = BigInt(0); // for now, this won't do anything, we may add later
+  const transferIx = await getBridgeFeeIx(
+    connection,
+    bridgeAddress,
+    payerAddress
+  );
+  const {
+    transfer_native_ix,
+    transfer_wrapped_ix,
+    approval_authority_address,
+  } = await import("../solana/token/token_bridge");
+  const approvalIx = Token.createApproveInstruction(
+    TOKEN_PROGRAM_ID,
+    new PublicKey(fromAddress),
+    new PublicKey(approval_authority_address(tokenBridgeAddress)),
+    new PublicKey(payerAddress),
+    [],
+    Number(amount)
+  );
+  let messageKey = Keypair.generate();
+  const isSolanaNative =
+    originChain === undefined || originChain === CHAIN_ID_SOLANA;
+  if (!isSolanaNative && !originAddress) {
+    throw new Error("originAddress is required when specifying originChain");
+  }
+  const ix = ixFromRust(
+    isSolanaNative
+      ? transfer_native_ix(
+          tokenBridgeAddress,
+          bridgeAddress,
+          payerAddress,
+          messageKey.publicKey.toString(),
+          fromAddress,
+          mintAddress,
+          nonce,
+          amount,
+          fee,
+          targetAddress,
+          targetChain
+        )
+      : transfer_wrapped_ix(
+          tokenBridgeAddress,
+          bridgeAddress,
+          payerAddress,
+          messageKey.publicKey.toString(),
+          fromAddress,
+          payerAddress,
+          originChain as number, // checked by isSolanaNative
+          originAddress as Uint8Array, // checked by throw
+          nonce,
+          amount,
+          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);
+  return transaction;
+}

+ 6 - 0
sdk/js/src/utils/createNonce.ts

@@ -0,0 +1,6 @@
+export function createNonce() {
+  const nonceConst = Math.random() * 100000;
+  const nonceBuffer = Buffer.alloc(4);
+  nonceBuffer.writeUInt32LE(nonceConst, 0);
+  return nonceBuffer;
+}

+ 1 - 0
sdk/js/src/utils/index.ts

@@ -1 +1,2 @@
 export * from "./consts";
+export * from "./createNonce";

+ 3 - 2
sdk/js/tsconfig.json

@@ -1,7 +1,8 @@
 {
   "compilerOptions": {
     "target": "es5",
-    "module": "commonjs",
+    "module": "esnext",
+    "moduleResolution": "node",
     "declaration": true,
     "outDir": "./lib",
     "strict": true,
@@ -9,6 +10,6 @@
     "downlevelIteration": true,
     "allowJs": true
   },
-  "include": ["src"],
+  "include": ["src", "types"],
   "exclude": ["node_modules", "**/__tests__/*"]
 }

+ 84 - 0
sdk/js/types/buffer-layout.d.ts

@@ -0,0 +1,84 @@
+// from https://github.com/solana-labs/solana-program-library/blob/4b0f59e9c61554708de969b01892b90955d5fd69/token-lending/js/types/buffer-layout.d.ts
+declare module "buffer-layout" {
+  export class Layout<T = any> {
+    span: number;
+    property?: string;
+    constructor(span: number, property?: string);
+    decode(b: Buffer, offset?: number): T;
+    encode(src: T, b: Buffer, offset?: number): number;
+    getSpan(b: Buffer, offset?: number): number;
+    replicate(name: string): this;
+  }
+  export class Structure<T = any> extends Layout<T> {
+    span: any;
+  }
+  export function greedy(
+    elementSpan?: number,
+    property?: string
+  ): Layout<number>;
+  export function offset<T>(
+    layout: Layout<T>,
+    offset?: number,
+    property?: string
+  ): Layout<T>;
+  export function u8(property?: string): Layout<number>;
+  export function u16(property?: string): Layout<number>;
+  export function u24(property?: string): Layout<number>;
+  export function u32(property?: string): Layout<number>;
+  export function u40(property?: string): Layout<number>;
+  export function u48(property?: string): Layout<number>;
+  export function nu64(property?: string): Layout<number>;
+  export function u16be(property?: string): Layout<number>;
+  export function u24be(property?: string): Layout<number>;
+  export function u32be(property?: string): Layout<number>;
+  export function u40be(property?: string): Layout<number>;
+  export function u48be(property?: string): Layout<number>;
+  export function nu64be(property?: string): Layout<number>;
+  export function s8(property?: string): Layout<number>;
+  export function s16(property?: string): Layout<number>;
+  export function s24(property?: string): Layout<number>;
+  export function s32(property?: string): Layout<number>;
+  export function s40(property?: string): Layout<number>;
+  export function s48(property?: string): Layout<number>;
+  export function ns64(property?: string): Layout<number>;
+  export function s16be(property?: string): Layout<number>;
+  export function s24be(property?: string): Layout<number>;
+  export function s32be(property?: string): Layout<number>;
+  export function s40be(property?: string): Layout<number>;
+  export function s48be(property?: string): Layout<number>;
+  export function ns64be(property?: string): Layout<number>;
+  export function f32(property?: string): Layout<number>;
+  export function f32be(property?: string): Layout<number>;
+  export function f64(property?: string): Layout<number>;
+  export function f64be(property?: string): Layout<number>;
+  export function struct<T>(
+    fields: Layout<any>[],
+    property?: string,
+    decodePrefixes?: boolean
+  ): Layout<T>;
+  export function bits(
+    word: Layout<number>,
+    msb?: boolean,
+    property?: string
+  ): any;
+  export function seq<T>(
+    elementLayout: Layout<T>,
+    count: number | Layout<number>,
+    property?: string
+  ): Layout<T[]>;
+  export function union(
+    discr: Layout<any>,
+    defaultLayout?: any,
+    property?: string
+  ): any;
+  export function unionLayoutDiscriminator(
+    layout: Layout<any>,
+    property?: string
+  ): any;
+  export function blob(
+    length: number | Layout<number>,
+    property?: string
+  ): Layout<Buffer>;
+  export function cstr(property?: string): Layout<string>;
+  export function utf8(maxSpan: number, property?: string): Layout<string>;
+}