Browse Source

sdk/js: Added transferFromSui and other functions

Kevin Peters 2 years ago
parent
commit
5644a90e52

+ 1 - 8
clients/js/cmds/submit.ts

@@ -134,14 +134,7 @@ exports.handler = async (argv) => {
     throw Error("OSMOSIS is not supported yet");
   } else if (chain === "sui") {
     const sui = require("../sui/utils");
-    await sui.execute_sui(
-      parsed_vaa.payload,
-      buf,
-      network,
-      argv["contract-address"],
-      undefined,
-      argv["rpc"]
-    );
+    await sui.execute_sui(parsed_vaa.payload, buf, network, argv["rpc"]);
   } else if (chain === "aptos") {
     const aptos = require("../aptos");
     await aptos.execute_aptos(

+ 2 - 2
clients/js/consts.ts

@@ -32,9 +32,9 @@ const OVERRIDES = {
   },
   DEVNET: {
     sui: {
-      core: "0xb80e44b7c40b874f0162d2440d9f79468132e911c62591eba52fb65a1c9835bb",
+      core: "0x50d49cf0c8f0ab33b0c4ad1693a2617f6b4fe4dac3e6e2d0ce6e9fbe83795b51", // wormhole module State object ID
       token_bridge:
-        "0x68da393248d51fbe2bd7456414adefdf7eac9ef91b32b561c178f01c906ae80e",
+        "0x63406070af4b2ba9ba6a7c47b04ef0fb6d7529c8fa80fe2abe701d8b392cfd3f", // token_bridge module State object ID
     },
     aptos: {
       token_bridge:

+ 71 - 30
clients/js/sui/utils.ts

@@ -15,47 +15,68 @@ import {
 import { CONTRACTS } from "../consts";
 import { NETWORKS } from "../networks";
 import { Network } from "../utils";
-import { Payload, impossible } from "../vaa";
-import { SUI_OBJECT_IDS, SuiAddresses } from "./consts";
+import { Payload, VAA, impossible, parse, serialiseVAA } from "../vaa";
 import { SuiRpcValidationError } from "./error";
 import { SuiCreateEvent, SuiPublishEvent } from "./types";
 
 const UPGRADE_CAP_TYPE = "0x2::package::UpgradeCap";
 
+// TODO(kp): remove this once it's in the sdk
+export async function getPackageId(
+  provider: JsonRpcProvider,
+  stateObjectId: string
+): Promise<string> {
+  const fields = await provider
+    .getObject({
+      id: stateObjectId,
+      options: {
+        showContent: true,
+      },
+    })
+    .then((result) => {
+      if (result.data?.content?.dataType === "moveObject") {
+        return result.data.content.fields;
+      }
+      throw new Error("Not a moveObject");
+    });
+  if ("upgrade_cap" in fields) {
+    return fields.upgrade_cap.fields.package;
+  }
+  throw new Error("upgrade_cap not found");
+}
+
 export const execute_sui = async (
   payload: Payload,
   vaa: Buffer,
   network: Network,
-  packageId?: string,
-  addresses?: Partial<SuiAddresses>,
   rpc?: string,
   privateKey?: string
 ) => {
   const chain = CHAIN_ID_TO_NAME[CHAIN_ID_SUI];
   const provider = getProvider(network, rpc);
   const signer = getSigner(provider, network, privateKey);
-  addresses = { ...SUI_OBJECT_IDS, ...addresses };
 
   switch (payload.module) {
-    case "Core":
-      packageId = packageId ?? CONTRACTS[network][chain]["core"];
-      if (!packageId) {
-        throw Error("Core bridge contract is undefined");
+    case "Core": {
+      const coreObjectId = CONTRACTS[network][chain].core;
+      if (!coreObjectId) {
+        throw Error("Core bridge object ID is undefined");
       }
-
+      const corePackageId = await getPackageId(provider, coreObjectId);
       switch (payload.type) {
         case "GuardianSetUpgrade": {
           console.log("Submitting new guardian set");
           const tx = new TransactionBlock();
           tx.moveCall({
-            target: `${packageId}::wormhole::update_guardian_set`,
+            target: `${corePackageId}::wormhole::update_guardian_set`,
             arguments: [
-              tx.object(addresses[network].core_state),
+              tx.object(coreObjectId),
               tx.pure([...vaa]),
               tx.object(SUI_CLOCK_OBJECT_ID),
             ],
           });
-          await executeTransactionBlock(signer, tx);
+          const result = await executeTransactionBlock(signer, tx);
+          console.log(JSON.stringify(result));
           break;
         }
         case "ContractUpgrade":
@@ -66,16 +87,30 @@ export const execute_sui = async (
           impossible(payload);
       }
       break;
-    case "NFTBridge":
+    }
+    case "NFTBridge": {
       throw new Error("NFT bridge not supported on Sui");
+    }
     case "TokenBridge": {
-      const coreBridgePackageId = CONTRACTS[network][chain]["core"];
-      const tokenBridgePackageId =
-        packageId ?? CONTRACTS[network][chain]["token_bridge"];
-      if (!tokenBridgePackageId) {
-        throw Error("Token bridge contract is undefined");
+      const coreBridgeStateObjectId = CONTRACTS[network][chain].core;
+      if (!coreBridgeStateObjectId) {
+        throw Error("Core bridge object ID is undefined");
       }
 
+      const tokenBridgeStateObjectId = CONTRACTS[network][chain].token_bridge;
+      if (!tokenBridgeStateObjectId) {
+        throw Error("Token bridge object ID is undefined");
+      }
+
+      const coreBridgePackageId = await getPackageId(
+        provider,
+        coreBridgeStateObjectId
+      );
+      const tokenBridgePackageId = await getPackageId(
+        provider,
+        tokenBridgeStateObjectId
+      );
+
       switch (payload.type) {
         case "ContractUpgrade":
           throw new Error("ContractUpgrade not supported on Sui");
@@ -83,14 +118,23 @@ export const execute_sui = async (
           throw new Error("RecoverChainId not supported on Sui");
         case "RegisterChain": {
           console.log("Registering chain");
-          const tx = new TransactionBlock();
-          tx.setGasBudget(1000000);
+          if (network === "DEVNET") {
+            // Modify the VAA to only have 1 guardian signature
+            // TODO: remove this when we can deploy the devnet core contract
+            // deterministically with multiple guardians in the initial guardian set
+            // Currently the core contract is setup with only 1 guardian in the set
+            const parsedVaa = parse(vaa);
+            parsedVaa.signatures = [parsedVaa.signatures[0]];
+            vaa = Buffer.from(serialiseVAA(parsedVaa as VAA<Payload>), "hex");
+          }
 
           // Get VAA
-          const [parsedVaa] = tx.moveCall({
+          const tx = new TransactionBlock();
+          tx.setGasBudget(1000000);
+          const [verifiedVaa] = tx.moveCall({
             target: `${coreBridgePackageId}::vaa::parse_and_verify`,
             arguments: [
-              tx.object(addresses[network].core_state),
+              tx.object(coreBridgeStateObjectId),
               tx.pure([...vaa]),
               tx.object(SUI_CLOCK_OBJECT_ID),
             ],
@@ -99,15 +143,15 @@ export const execute_sui = async (
           // Get decree ticket
           const [decreeTicket] = tx.moveCall({
             target: `${tokenBridgePackageId}::register_chain::authorize_governance`,
-            arguments: [tx.object(addresses[network].token_bridge_state)],
+            arguments: [tx.object(tokenBridgeStateObjectId)],
           });
 
           // Get decree receipt
           const [decreeReceipt] = tx.moveCall({
             target: `${coreBridgePackageId}::governance_message::verify_vaa`,
             arguments: [
-              tx.object(addresses[network].core_state),
-              parsedVaa,
+              tx.object(coreBridgeStateObjectId),
+              verifiedVaa,
               decreeTicket,
             ],
             typeArguments: [
@@ -118,10 +162,7 @@ export const execute_sui = async (
           // Register chain
           tx.moveCall({
             target: `${tokenBridgePackageId}::register_chain::register_chain`,
-            arguments: [
-              tx.object(addresses[network].token_bridge_state),
-              decreeReceipt,
-            ],
+            arguments: [tx.object(tokenBridgeStateObjectId), decreeReceipt],
           });
 
           await executeTransactionBlock(signer, tx);

+ 1 - 0
devnet/eth-devnet.yaml

@@ -41,6 +41,7 @@ spec:
             - --deterministic
             - --time="1970-01-01T00:00:00+00:00"
             - --host=0.0.0.0
+            - --accounts=11
           ports:
             - containerPort: 8545
               name: rpc

+ 3 - 0
devnet/sui-devnet.yaml

@@ -38,6 +38,9 @@ spec:
       containers:
         - name: sui-node
           image: sui-node
+          resources:
+            requests:
+              memory: "2048Mi"
           command:
             - /bin/sh
             - -c

+ 4 - 4
scripts/devnet-consts.json

@@ -243,7 +243,7 @@
         },
         "21": {
             "contracts": {
-                "tokenBridgeEmitterAddress": "af965b860730651b06aa0bf4c013e33ef56389ac826da3335d37cfb596449ed3"
+                "tokenBridgeEmitterAddress": "1c63eb7f00bed5fc457ad3a080e2e9555a25cfeceeecdfbf2c5e38d80ddfecd1"
             }
         },
         "22": {
@@ -369,9 +369,9 @@
             "private": "0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"
         },
         {
-            "name": "10",
-            "public": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
-            "private": "0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c"
+          "name": "10",
+          "public": "0x610bb1573d1046fcb8a70bbbd395754cd57c2b60",
+          "private": "0x77c5495fbb039eed474fc940f29955ed0531693cc9212911efd35dff0373153f"
         }
     ],
     "devnetGuardians": [

+ 1 - 0
sdk/devnet_consts.go

@@ -16,6 +16,7 @@ var knownDevnetTokenbridgeEmitters = map[vaa.ChainID]string{
 	vaa.ChainIDBSC:       "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
 	vaa.ChainIDAlgorand:  "8edf5b0e108c3a1a0a4b704cc89591f2ad8d50df24e991567e640ed720a94be2",
 	vaa.ChainIDWormchain: "0000000000000000000000001711cd63b2c545ee6545415d3cc0bda6425c43c4",
+	vaa.ChainIDSui:       "1c63eb7f00bed5fc457ad3a080e2e9555a25cfeceeecdfbf2c5e38d80ddfecd1",
 }
 
 // KnownDevnetNFTBridgeEmitters is a map of known NFT emitters used during development.

+ 2 - 0
sdk/js/package-lock.json

@@ -9859,6 +9859,7 @@
       "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz",
       "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==",
       "dev": true,
+      "hasInstallScript": true,
       "optional": true,
       "dependencies": {
         "node-gyp-build": "^4.3.0"
@@ -10221,6 +10222,7 @@
       "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz",
       "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==",
       "dev": true,
+      "hasInstallScript": true,
       "optional": true,
       "dependencies": {
         "node-gyp-build": "^4.3.0"

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

@@ -5,6 +5,7 @@ import { AptosClient, Types } from "aptos";
 import { BigNumber, ContractReceipt } from "ethers";
 import { FinalExecutionOutcome } from "near-api-js/lib/providers";
 import { Implementation__factory } from "../ethers-contracts";
+import { SuiTransactionBlockResponse } from "@mysten/sui.js";
 
 export function parseSequenceFromLogEth(
   receipt: ContractReceipt,
@@ -180,3 +181,13 @@ export function parseSequenceFromLogAptos(
 
   return null;
 }
+
+export function parseSequenceFromLogSui(
+  coreBridgePackageId: string,
+  response: SuiTransactionBlockResponse
+): string | null {
+  const event = response.events?.find(
+    (e) => e.type === `${coreBridgePackageId}::publish_message::WormholeMessage`
+  );
+  return event?.parsedJson?.sequence || null;
+}

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

@@ -15,5 +15,6 @@ export * as bridge from "./bridge";
 export * as token_bridge from "./token_bridge";
 export * as nft_bridge from "./nft_bridge";
 export * as algorand from "./algorand";
+export * as sui from "./sui";
 
 export { postVaaSolana, postVaaSolanaWithRetry } from "./solana";

+ 9 - 9
sdk/js/src/sui/test.ts → sdk/js/src/sui/build.ts

@@ -11,22 +11,22 @@ const TEMPORARY_SUI_BRANCH = "sui/integration_v2";
 
 export const getCoinBuildOutputManual = async (
   network: Network,
-  coreBridgeAddress: string,
-  tokenBridgeAddress: string,
+  coreBridgePackageId: string,
+  tokenBridgePackageId: string,
   vaa: string
 ): Promise<SuiBuildOutput> => {
   await cloneDependencies();
   setupMainToml(
     `${__dirname}/dependencies/wormhole`,
     network,
-    coreBridgeAddress
+    coreBridgePackageId
   );
   setupMainToml(
     `${__dirname}/dependencies/token_bridge`,
     network,
-    tokenBridgeAddress
+    tokenBridgePackageId
   );
-  setupCoin(coreBridgeAddress, tokenBridgeAddress, vaa);
+  setupCoin(coreBridgePackageId, tokenBridgePackageId, vaa);
   const buildOutput = buildPackage(`${__dirname}/wrapped_coin`);
   cleanupTempToml(`${__dirname}/dependencies/wormhole`);
   cleanupTempToml(`${__dirname}/dependencies/token_bridge`);
@@ -150,8 +150,8 @@ const getPackageNameFromPath = (packagePath: string): string =>
 
 // TODO(aki): parallelize
 const setupCoin = (
-  coreBridgeAddress: string,
-  tokenBridgeAddress: string,
+  coreBridgePackageId: string,
+  tokenBridgePackageId: string,
   vaa: string
 ): void => {
   fs.rmSync(`${__dirname}/wrapped_coin`, { recursive: true, force: true });
@@ -167,8 +167,8 @@ const setupCoin = (
   );
 
   const toml = new MoveToml(`${__dirname}/templates/wrapped_coin/Move.toml`)
-    .updateRow("addresses", "wormhole", coreBridgeAddress)
-    .updateRow("addresses", "token_bridge", tokenBridgeAddress)
+    .updateRow("addresses", "wormhole", coreBridgePackageId)
+    .updateRow("addresses", "token_bridge", tokenBridgePackageId)
     .serialize();
   fs.writeFileSync(`${__dirname}/wrapped_coin/Move.toml`, toml, "utf8");
 };

+ 11 - 7
sdk/js/src/sui/publish.ts

@@ -7,18 +7,22 @@ import {
 import { SuiBuildOutput } from "./types";
 
 export const publishCoin = async (
-  coreBridgeAddress: string,
-  tokenBridgeAddress: string,
+  coreBridgePackageId: string,
+  tokenBridgePackageId: string,
   vaa: string,
   signerAddress: string
 ) => {
-  const build = getCoinBuildOutput(coreBridgeAddress, tokenBridgeAddress, vaa);
+  const build = getCoinBuildOutput(
+    coreBridgePackageId,
+    tokenBridgePackageId,
+    vaa
+  );
   return publishPackage(build, signerAddress);
 };
 
 export const getCoinBuildOutput = (
-  coreBridgeAddress: string,
-  tokenBridgeAddress: string,
+  coreBridgePackageId: string,
+  tokenBridgePackageId: string,
   vaa: string
 ): SuiBuildOutput => {
   const bytecode =
@@ -30,8 +34,8 @@ export const getCoinBuildOutput = (
     dependencies: [
       normalizeSuiAddress("0x1"),
       normalizeSuiAddress("0x2"),
-      tokenBridgeAddress,
-      coreBridgeAddress,
+      tokenBridgePackageId,
+      coreBridgePackageId,
     ],
   };
 };

+ 5 - 0
sdk/js/src/sui/types.ts

@@ -27,3 +27,8 @@ export type SuiError = {
   message: string;
   data: any;
 };
+
+export type SuiCoinObject = {
+  type: string;
+  objectId: string;
+};

+ 98 - 4
sdk/js/src/sui/utils.ts

@@ -32,6 +32,7 @@ export const getFieldsFromObjectResponse = (object: SuiObjectResponse) => {
 };
 
 export const getInnerType = (type: string): string | null => {
+  if (!type) return null;
   const match = type.match(/<(.*)>/);
   if (!match || !isValidSuiType(match[1])) {
     return null;
@@ -40,6 +41,22 @@ export const getInnerType = (type: string): string | null => {
   return match[1];
 };
 
+export const getPackageIdFromType = (type: string): string | null => {
+  if (!isValidSuiType(type)) return null;
+  const packageId = type.split("::")[0];
+  if (!isValidSuiAddress(packageId)) return null;
+  return packageId;
+};
+
+export const getTableKeyType = (tableType: string): string | null => {
+  if (!tableType) return null;
+  const match = tableType.match(/0x2::table::Table<(.*)>/);
+  if (!match) return null;
+  const [keyType] = match[1].split(",");
+  if (!isValidSuiType(keyType)) return null;
+  return keyType;
+};
+
 export const getObjectFields = async (
   provider: JsonRpcProvider,
   objectId: string
@@ -59,7 +76,6 @@ export const getObjectFields = async (
 
 export const getTokenFromTokenRegistry = async (
   provider: JsonRpcProvider,
-  tokenBridgeAddress: string,
   tokenBridgeStateObjectId: string,
   tokenType: string
 ): Promise<SuiObjectResponse> => {
@@ -76,17 +92,21 @@ export const getTokenFromTokenRegistry = async (
       `Unable to fetch object fields from token bridge state. Object ID: ${tokenBridgeStateObjectId}`
     );
   }
-
   const tokenRegistryObjectId =
     tokenBridgeStateFields.token_registry?.fields?.id?.id;
   if (!tokenRegistryObjectId) {
     throw new Error("Unable to fetch token registry object ID");
   }
-
+  const tokenRegistryPackageId = getPackageIdFromType(
+    tokenBridgeStateFields.token_registry?.type
+  );
+  if (!tokenRegistryObjectId) {
+    throw new Error("Unable to fetch token registry package ID");
+  }
   return provider.getDynamicFieldObject({
     parentId: tokenRegistryObjectId,
     name: {
-      type: `${tokenBridgeAddress}::token_registry::Key<${tokenType}>`,
+      type: `${tokenRegistryPackageId}::token_registry::Key<${tokenType}>`,
       value: {
         dummy_field: false,
       },
@@ -94,6 +114,50 @@ export const getTokenFromTokenRegistry = async (
   });
 };
 
+export const getTokenCoinType = async (
+  provider: JsonRpcProvider,
+  tokenBridgeStateObjectId: string,
+  tokenAddress: Uint8Array,
+  tokenChain: number
+): Promise<string | null> => {
+  const tokenBridgeStateFields = await getObjectFields(
+    provider,
+    tokenBridgeStateObjectId
+  );
+  if (!tokenBridgeStateFields) {
+    throw new Error("Unable to fetch object fields from token bridge state");
+  }
+  const coinTypes = tokenBridgeStateFields?.token_registry?.fields?.coin_types;
+  const coinTypesObjectId = coinTypes?.fields?.id?.id;
+  if (!coinTypesObjectId) {
+    throw new Error("Unable to fetch coin types");
+  }
+  const keyType = getTableKeyType(coinTypes?.type);
+  if (!keyType) {
+    throw new Error("Unable to get key type");
+  }
+  try {
+    // This call throws if the key doesn't exist in CoinTypes
+    const coinTypeValue = await provider.getDynamicFieldObject({
+      parentId: coinTypesObjectId,
+      name: {
+        type: keyType,
+        value: {
+          addr: [...tokenAddress],
+          chain: tokenChain,
+        },
+      },
+    });
+    const fields = getFieldsFromObjectResponse(coinTypeValue);
+    return fields?.value || null;
+  } catch (e: any) {
+    if (e.code === -32000 && e.message?.includes("RPC Error")) {
+      return null;
+    }
+    throw e;
+  }
+};
+
 /**
  * Get the fully qualified type of a wrapped asset published to the given
  * package ID.
@@ -137,3 +201,33 @@ export const isValidSuiType = (type: string): boolean => {
 
   return isValidSuiAddress(tokens[0]) && !!tokens[1] && !!tokens[2];
 };
+
+export const getPackageId = async (
+  provider: JsonRpcProvider,
+  objectId: string
+): Promise<string> => {
+  const fields = await getObjectFields(provider, objectId);
+  if (fields && "upgrade_cap" in fields) {
+    return fields.upgrade_cap.fields.package;
+  }
+  throw new Error("upgrade_cap not found");
+};
+
+// TODO: can we pass in the latest core bridge package Id after an upgrade?
+// or do we have to use the first one?
+// this is the same type that the guardian will look for
+export const getEmitterAddressAndSequenceFromResponseSui = (
+  coreBridgePackageId: string,
+  response: SuiTransactionBlockResponse
+): { emitterAddress: string; sequence: string } => {
+  const eventType = `${coreBridgePackageId}::publish_message::WormholeMessage`;
+  const event = response.events?.find((e) => e.type === eventType);
+  if (event === undefined) {
+    throw new Error(`${eventType} event type not found`);
+  }
+  const { sender, sequence } = event.parsedJson || {};
+  if (sender === undefined || sequence === undefined) {
+    throw new Error("Can't find sender or sequence");
+  }
+  return { emitterAddress: sender.substring(2), sequence };
+};

+ 357 - 94
sdk/js/src/token_bridge/__tests__/sui-integration.ts

@@ -1,5 +1,12 @@
 import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
-import { afterAll, beforeAll, describe, expect, test } from "@jest/globals";
+import {
+  afterAll,
+  beforeAll,
+  describe,
+  expect,
+  jest,
+  test,
+} from "@jest/globals";
 import {
   Connection,
   Ed25519Keypair,
@@ -10,30 +17,45 @@ import {
   getPublishedObjectChanges,
 } from "@mysten/sui.js";
 import { ethers } from "ethers";
+import { parseUnits } from "ethers/lib/utils";
 import {
+  approveEth,
   attestFromEth,
   attestFromSui,
+  createWrappedOnEth,
   createWrappedOnSui,
   createWrappedOnSuiPrepare,
   getEmitterAddressEth,
+  getForeignAssetEth,
+  getForeignAssetSui,
+  getIsTransferCompletedEth,
+  getIsTransferCompletedSui,
   getIsWrappedAssetSui,
   getOriginalAssetSui,
   getSignedVAAWithRetry,
+  parseAttestMetaVaa,
   parseSequenceFromLogEth,
+  redeemOnEth,
+  redeemOnSui,
+  transferFromEth,
+  transferFromSui,
 } from "../..";
 import {
   executeTransactionBlock,
   getCoinBuildOutput,
   getInnerType,
-  getWrappedCoinType,
+  getPackageId,
 } from "../../sui";
-import { getCoinBuildOutputManual } from "../../sui/test";
+import { getCoinBuildOutputManual } from "../../sui/build";
+import { SuiCoinObject } from "../../sui/types";
 import {
   CHAIN_ID_ETH,
   CHAIN_ID_SUI,
   CONTRACTS,
-  SUI_OBJECT_IDS,
+  hexToUint8Array,
+  tryNativeToUint8Array,
 } from "../../utils";
+import { Payload, VAA, parse, serialiseVAA } from "../../vaa/generic";
 import {
   ETH_NODE_URL,
   ETH_PRIVATE_KEY10,
@@ -42,16 +64,17 @@ import {
   TEST_ERC20,
   WORMHOLE_RPC_HOSTS,
 } from "./utils/consts";
-import { assertIsNotNullOrUndefined } from "./utils/helpers";
+import {
+  assertIsNotNullOrUndefined,
+  getEmitterAddressAndSequenceFromResponseSui,
+  mintAndTransferCoinSui,
+} from "./utils/helpers";
 
-const JEST_TEST_TIMEOUT = 60000;
+jest.setTimeout(60000);
 
 // Sui constants
-const SUI_CORE_BRIDGE_ADDRESS = CONTRACTS.DEVNET.sui.core;
-const SUI_TOKEN_BRIDGE_ADDRESS = CONTRACTS.DEVNET.sui.token_bridge;
-const SUI_CORE_BRIDGE_STATE_OBJECT_ID = SUI_OBJECT_IDS.DEVNET.core_state;
-const SUI_TOKEN_BRIDGE_STATE_OBJECT_ID =
-  SUI_OBJECT_IDS.DEVNET.token_bridge_state;
+const SUI_CORE_BRIDGE_STATE_OBJECT_ID = CONTRACTS.DEVNET.sui.core;
+const SUI_TOKEN_BRIDGE_STATE_OBJECT_ID = CONTRACTS.DEVNET.sui.token_bridge;
 const SUI_DEPLOYER_PRIVATE_KEY = "AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb";
 
 const suiKeypair: Ed25519Keypair = Ed25519Keypair.fromSecretKey(
@@ -73,37 +96,55 @@ const ETH_TOKEN_BRIDGE_ADDRESS = CONTRACTS.DEVNET.ethereum.token_bridge;
 const ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
 const ethSigner = new ethers.Wallet(ETH_PRIVATE_KEY10, ethProvider);
 
+let suiCoreBridgePackageId: string;
+let suiTokenBridgePackageId: string;
+
 beforeAll(async () => {
-  expect(SUI_CORE_BRIDGE_ADDRESS).toBeDefined();
-  expect(SUI_TOKEN_BRIDGE_ADDRESS).toBeDefined();
-  expect(SUI_CORE_BRIDGE_STATE_OBJECT_ID).toBeDefined();
-  expect(SUI_TOKEN_BRIDGE_STATE_OBJECT_ID).toBeDefined();
+  suiCoreBridgePackageId = await getPackageId(
+    suiProvider,
+    SUI_CORE_BRIDGE_STATE_OBJECT_ID
+  );
+  suiTokenBridgePackageId = await getPackageId(
+    suiProvider,
+    SUI_TOKEN_BRIDGE_STATE_OBJECT_ID
+  );
 });
 
 afterAll(async () => {
   await ethProvider.destroy();
 });
 
+// Modify the VAA to only have 1 guardian signature
+// TODO: remove this when we can deploy the devnet core contract
+// deterministically with multiple guardians in the initial guardian set
+// Currently the core contract is setup with only 1 guardian in the set
+function sliceVAASignatures(vaa: Uint8Array) {
+  const parsedVAA = parse(Buffer.from([...vaa]));
+  parsedVAA.guardianSetIndex = 0;
+  parsedVAA.signatures = [parsedVAA.signatures[0]];
+  return hexToUint8Array(serialiseVAA(parsedVAA as VAA<Payload>));
+}
+
 describe("Sui SDK tests", () => {
-  test("Test prebuilt coin build output", async () => {
+  test.skip("Test prebuilt coin build output", async () => {
     const vaa =
       "0100000000010026ff86c07ef853ef955a63c58a8d08eeb2ac232b91e725bd41baeb3c05c5c18d07aef3c02dc3d5ca8ad0600a447c3d55386d0a0e85b23378d438fbb1e207c3b600000002c3a86f000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16000000000000000001020000000000000000000000002d8be6bf0baa74e0a907016679cae9190e80dd0a000212544b4e0000000000000000000000000000000000000000000000000000000000457468657265756d205465737420546f6b656e00000000000000000000000000";
     const build = getCoinBuildOutput(
-      SUI_CORE_BRIDGE_ADDRESS,
-      SUI_TOKEN_BRIDGE_ADDRESS,
+      suiCoreBridgePackageId,
+      suiTokenBridgePackageId,
       vaa
     );
     const buildManual = await getCoinBuildOutputManual(
       "DEVNET",
-      SUI_CORE_BRIDGE_ADDRESS,
-      SUI_TOKEN_BRIDGE_ADDRESS,
+      suiCoreBridgePackageId,
+      suiTokenBridgePackageId,
       vaa
     );
     expect(build).toMatchObject(buildManual);
     expect(buildManual).toMatchObject(build);
   });
-  test("Transfer native ERC-20 from Ethereum to Sui", async () => {
-    // Attest on Eth
+  test.skip("Transfer native ERC-20 from Ethereum to Sui and back", async () => {
+    // Attest on Ethereum
     const ethAttestTxRes = await attestFromEth(
       ETH_TOKEN_BRIDGE_ADDRESS,
       ethSigner,
@@ -111,17 +152,16 @@ describe("Sui SDK tests", () => {
     );
 
     // Get attest VAA
-    const sequence = parseSequenceFromLogEth(
+    const attestSequence = parseSequenceFromLogEth(
       ethAttestTxRes,
       ETH_CORE_BRIDGE_ADDRESS
     );
-    expect(sequence).toBeTruthy();
-
+    expect(attestSequence).toBeTruthy();
     const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
       WORMHOLE_RPC_HOSTS,
       CHAIN_ID_ETH,
       getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS),
-      sequence,
+      attestSequence,
       {
         transport: NodeHttpTransport(),
       },
@@ -131,11 +171,9 @@ describe("Sui SDK tests", () => {
     expect(attestVAA).toBeTruthy();
 
     // Start create wrapped on Sui
-    // const MOCK_VAA =
-    //   "0100000000010026ff86c07ef853ef955a63c58a8d08eeb2ac232b91e725bd41baeb3c05c5c18d07aef3c02dc3d5ca8ad0600a447c3d55386d0a0e85b23378d438fbb1e207c3b600000002c3a86f000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16000000000000000001020000000000000000000000002d8be6bf0baa74e0a907016679cae9190e80dd0a000212544b4e0000000000000000000000000000000000000000000000000000000000457468657265756d205465737420546f6b656e00000000000000000000000000";
     const suiPrepareRegistrationTxPayload = await createWrappedOnSuiPrepare(
-      SUI_CORE_BRIDGE_ADDRESS,
-      SUI_TOKEN_BRIDGE_ADDRESS,
+      suiCoreBridgePackageId,
+      suiTokenBridgePackageId,
       attestVAA,
       suiAddress
     );
@@ -144,6 +182,7 @@ describe("Sui SDK tests", () => {
       suiPrepareRegistrationTxPayload
     );
     // expect(coins.length).toBe(1);
+    console.log(JSON.stringify(suiPrepareRegistrationTxRes));
     expect(suiPrepareRegistrationTxRes.effects?.status.status).toBe("success");
 
     // Complete create wrapped on Sui
@@ -151,11 +190,10 @@ describe("Sui SDK tests", () => {
       suiPrepareRegistrationTxRes
     );
     expect(publishEvents.length).toBe(1);
-
     const coinPackageId = publishEvents[0].packageId;
     const suiCompleteRegistrationTxPayload = await createWrappedOnSui(
       suiProvider,
-      SUI_TOKEN_BRIDGE_ADDRESS,
+      suiTokenBridgePackageId,
       SUI_CORE_BRIDGE_STATE_OBJECT_ID,
       SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
       suiAddress,
@@ -166,36 +204,150 @@ describe("Sui SDK tests", () => {
       suiCompleteRegistrationTxPayload
     );
     expect(suiCompleteRegistrationTxRes.effects?.status.status).toBe("success");
-    expect(
-      await getIsWrappedAssetSui(
-        suiProvider,
-        SUI_TOKEN_BRIDGE_ADDRESS,
-        SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
-        getWrappedCoinType(coinPackageId)
-      )
-    ).toBe(true);
-    expect(
-      await getOriginalAssetSui(
-        suiProvider,
-        SUI_TOKEN_BRIDGE_ADDRESS,
-        SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
-        getWrappedCoinType(coinPackageId)
-      )
-    ).toMatchObject({
-      isWrapped: true,
-      chainId: CHAIN_ID_ETH,
-      assetAddress: Buffer.from(TEST_ERC20, "hex"),
-    });
+
+    // Get foreign asset
+    const foreignAsset = await getForeignAssetSui(
+      suiProvider,
+      SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+      CHAIN_ID_ETH,
+      hexToUint8Array(TEST_ERC20)
+    );
+    console.log(JSON.stringify(foreignAsset));
+    //expect(
+    //  await getIsWrappedAssetSui(
+    //    suiProvider,
+    //    SUI_TOKEN_BRIDGE_ADDRESS,
+    //    SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+    //    getWrappedCoinType(coinPackageId)
+    //  )
+    //).toBe(true);
+    //const originalAsset = await getOriginalAssetSui(
+    //  suiProvider,
+    //  SUI_TOKEN_BRIDGE_ADDRESS,
+    //  SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+    //  getWrappedCoinType(coinPackageId)
+    //);
+    //expect(originalAsset).toMatchObject({
+    //  isWrapped: true,
+    //  chainId: CHAIN_ID_ETH,
+    //  assetAddress: Buffer.from(TEST_ERC20, "hex"),
+    //});
+    //console.log(originalAsset, Buffer.from(TEST_ERC20, "hex"));
+    //const transferAmount = formatUnits(1, 18);
+    //// Transfer to Sui
+    //await approveEth(
+    //  CONTRACTS.DEVNET.ethereum.token_bridge,
+    //  TEST_ERC20,
+    //  ethSigner,
+    //  transferAmount
+    //);
+    //const transferReceipt = await transferFromEth(
+    //  CONTRACTS.DEVNET.ethereum.token_bridge,
+    //  ethSigner,
+    //  TEST_ERC20,
+    //  transferAmount,
+    //  CHAIN_ID_SUI,
+    //  tryNativeToUint8Array(suiAddress, CHAIN_ID_SUI)
+    //);
+    //const ethSequence = parseSequenceFromLogEth(
+    //  transferReceipt,
+    //  ETH_CORE_BRIDGE_ADDRESS
+    //);
+    //const { vaaBytes: ethTransferVAA } = await getSignedVAAWithRetry(
+    //  WORMHOLE_RPC_HOSTS,
+    //  CHAIN_ID_ETH,
+    //  getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS),
+    //  ethSequence,
+    //  {
+    //    transport: NodeHttpTransport(),
+    //  },
+    //  1000,
+    //  5
+    //);
+    //expect(ethTransferVAA).toBeTruthy();
+    //// Redeem on Sui
+    //const redeemPayload = await redeemOnSui(
+    //  suiProvider,
+    //  SUI_TOKEN_BRIDGE_ADDRESS,
+    //  SUI_CORE_BRIDGE_STATE_OBJECT_ID,
+    //  SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+    //  ethTransferVAA
+    //);
+    //await executeTransactionBlock(suiSigner, redeemPayload);
+    //expect(
+    //  await getIsTransferCompletedSui(
+    //    suiProvider,
+    //    SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+    //    ethTransferVAA
+    //  )
+    //).toBe(true);
+    //// Transfer back to Eth
+    //const coinType = await getForeignAssetSui(
+    //  suiProvider,
+    //  SUI_TOKEN_BRIDGE_ADDRESS,
+    //  SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+    //  CHAIN_ID_ETH,
+    //  originalAsset.assetAddress
+    //);
+    //expect(coinType).toBeTruthy();
+    //const coins = (
+    //  await suiProvider.getCoins({ owner: suiAddress, coinType })
+    //).data.map<SuiCoinObject>((c) => ({
+    //  type: c.coinType,
+    //  objectId: c.coinObjectId,
+    //}));
+    //const suiTransferTxPayload = transferFromSui(
+    //  SUI_TOKEN_BRIDGE_ADDRESS,
+    //  SUI_CORE_BRIDGE_STATE_OBJECT_ID,
+    //  SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+    //  coins,
+    //  coinType || "",
+    //  BigInt(transferAmount),
+    //  CHAIN_ID_ETH,
+    //  tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
+    //);
+    //const suiTransferTxResult = await executeTransactionBlock(
+    //  suiSigner,
+    //  suiTransferTxPayload
+    //);
+    //const { sequence, emitterAddress } =
+    //  getEmitterAddressAndSequenceFromResponseSui(
+    //    SUI_CORE_BRIDGE_ADDRESS,
+    //    suiTransferTxResult
+    //  );
+    //// Fetch the transfer VAA
+    //const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
+    //  WORMHOLE_RPC_HOSTS,
+    //  CHAIN_ID_SUI,
+    //  emitterAddress,
+    //  sequence,
+    //  {
+    //    transport: NodeHttpTransport(),
+    //  },
+    //  1000,
+    //  5
+    //);
+    //expect(transferVAA).toBeTruthy();
+    //// Redeem on Ethereum
+    //await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, ethSigner, transferVAA);
+    //expect(
+    //  await getIsTransferCompletedEth(
+    //    ETH_TOKEN_BRIDGE_ADDRESS,
+    //    ethProvider,
+    //    transferVAA
+    //  )
+    //).toBe(true);
   });
-  test("Transfer non-SUI Sui token to Ethereum", async () => {
+  test("Transfer non-SUI Sui token to Ethereum and back", async () => {
     // Get COIN_8 coin type
     const res = await suiProvider.getOwnedObjects({
       owner: suiAddress,
       options: { showContent: true, showType: true },
     });
-    const coins = res.data.filter((o) =>
-      (o.data?.type ?? "").includes("COIN_8")
-    );
+    const coins = res.data.filter((o) => {
+      const type = o.data?.type ?? "";
+      return type.includes("TreasuryCap") && type.includes("COIN_8");
+    });
     expect(coins.length).toBe(1);
 
     const coin8 = coins[0];
@@ -206,66 +358,177 @@ describe("Sui SDK tests", () => {
     expect(
       await getIsWrappedAssetSui(
         suiProvider,
-        SUI_TOKEN_BRIDGE_ADDRESS,
         SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
         coin8Type
       )
     ).toBe(false);
 
+    // Mint coins
+    const transferAmount = parseUnits("1", 8).toBigInt();
+    const suiMintTxPayload = mintAndTransferCoinSui(
+      coin8TreasuryCapObjectId,
+      coin8Type,
+      transferAmount,
+      suiAddress
+    );
+    let result = await executeTransactionBlock(suiSigner, suiMintTxPayload);
+    expect(result.effects?.status.status).toBe("success");
+
     // Attest on Sui
     const suiAttestTxPayload = await attestFromSui(
       suiProvider,
-      SUI_TOKEN_BRIDGE_ADDRESS,
+      suiTokenBridgePackageId,
       SUI_CORE_BRIDGE_STATE_OBJECT_ID,
       SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
-      coin8Type,
-      0
+      coin8Type
     );
-    const suiAttestTxRes = await executeTransactionBlock(
-      suiSigner,
-      suiAttestTxPayload
+    result = await executeTransactionBlock(suiSigner, suiAttestTxPayload);
+    expect(result.effects?.status.status).toBe("success");
+    const { sequence: attestSequence, emitterAddress: attestEmitterAddress } =
+      getEmitterAddressAndSequenceFromResponseSui(
+        suiCoreBridgePackageId,
+        result
+      );
+    expect(attestSequence).toBeTruthy();
+    expect(attestEmitterAddress).toBeTruthy();
+    const { vaaBytes } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_SUI,
+      attestEmitterAddress,
+      attestSequence,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
     );
-    expect(suiAttestTxRes.effects?.status.status).toBe("success");
+    expect(vaaBytes).toBeTruthy();
+    // Create wrapped on Ethereum
+    try {
+      await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, ethSigner, vaaBytes);
+    } catch (e) {
+      // this could fail because the token is already attested (in an unclean env)
+    }
+    const { tokenAddress } = parseAttestMetaVaa(vaaBytes);
     expect(
       await getOriginalAssetSui(
         suiProvider,
-        SUI_TOKEN_BRIDGE_ADDRESS,
         SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
         coin8Type
       )
     ).toMatchObject({
       isWrapped: false,
       chainId: CHAIN_ID_SUI,
-      assetAddress: Buffer.from(coin8Type.split("::")[0], "hex"),
+      assetAddress: new Uint8Array(tokenAddress),
     });
+    const coin8Coins = (
+      await suiProvider.getCoins({
+        owner: suiAddress,
+        coinType: coin8Type,
+      })
+    ).data.map<SuiCoinObject>((c) => ({
+      type: c.coinType,
+      objectId: c.coinObjectId,
+    }));
+    expect(coin8Coins.length).toBeGreaterThan(0);
+    // Transfer to Ethereum
+    const suiTransferTxPayload = transferFromSui(
+      SUI_CORE_BRIDGE_STATE_OBJECT_ID,
+      suiTokenBridgePackageId,
+      SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+      coin8Coins,
+      coin8Type,
+      transferAmount,
+      CHAIN_ID_ETH,
+      tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
+    );
+    result = await executeTransactionBlock(suiSigner, suiTransferTxPayload);
+    expect(result.effects?.status.status).toBe("success");
+    const { sequence, emitterAddress } =
+      getEmitterAddressAndSequenceFromResponseSui(
+        suiCoreBridgePackageId,
+        result
+      );
+    expect(sequence).toBeTruthy();
+    expect(emitterAddress).toBeTruthy();
+    // Fetch the transfer VAA
+    const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_SUI,
+      emitterAddress,
+      sequence!,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
+    );
+    // Redeem on Ethereum
+    await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, ethSigner, transferVAA);
+    expect(
+      await getIsTransferCompletedEth(
+        ETH_TOKEN_BRIDGE_ADDRESS,
+        ethProvider,
+        transferVAA
+      )
+    ).toBe(true);
 
-    // transfer tokens to Ethereum
-    // const coinsObject = (
-    //   await suiProvider.getGasObjectsOwnedByAddress(suiAddress)
-    // )[1];
-    // const suiTransferTxPayload = await transferFromSui(
-    //   suiProvider,
-    //   SUI_CORE_BRIDGE_ADDRESS,
-    //   SUI_TOKEN_BRIDGE_ADDRESS,
-    //   SUI_COIN_TYPE,
-    //   coinsObject.objectId,
-    //   feeObject.objectId,
-    //   CHAIN_ID_ETH,
-    //   tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
-    // );
-    // const suiTransferTxResult = await executeTransaction(
-    //   suiSigner,
-    //   suiTransferTxPayload
-    // );
-    // console.log(
-    //   "suiTransferTxResult",
-    //   JSON.stringify(suiTransferTxResult, null, 2)
-    // );
-
-    // fetch vaa
-
-    // redeem on Ethereum
-
-    // transfer tokens back to Sui
+    // Transfer back to Sui
+    const ethTokenAddress = await getForeignAssetEth(
+      ETH_TOKEN_BRIDGE_ADDRESS,
+      ethProvider,
+      CHAIN_ID_SUI,
+      tokenAddress
+    );
+    expect(ethTokenAddress).toBeTruthy();
+    await approveEth(
+      ETH_TOKEN_BRIDGE_ADDRESS,
+      ethTokenAddress!,
+      ethSigner,
+      transferAmount
+    );
+    const transferReceipt = await transferFromEth(
+      ETH_TOKEN_BRIDGE_ADDRESS,
+      ethSigner,
+      ethTokenAddress!,
+      transferAmount,
+      CHAIN_ID_SUI,
+      tryNativeToUint8Array(suiAddress, CHAIN_ID_SUI)
+    );
+    const ethSequence = parseSequenceFromLogEth(
+      transferReceipt,
+      ETH_CORE_BRIDGE_ADDRESS
+    );
+    expect(ethSequence).toBeTruthy();
+    const { vaaBytes: ethTransferVAA } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_ETH,
+      getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS),
+      ethSequence,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
+    );
+    const slicedVAA = sliceVAASignatures(ethTransferVAA);
+    // Redeem on Sui
+    const redeemPayload = await redeemOnSui(
+      suiProvider,
+      suiCoreBridgePackageId,
+      SUI_CORE_BRIDGE_STATE_OBJECT_ID,
+      suiTokenBridgePackageId,
+      SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+      slicedVAA
+    );
+    result = await executeTransactionBlock(suiSigner, redeemPayload);
+    expect(result.effects?.status.status).toBe("success");
+    expect(
+      await getIsTransferCompletedSui(
+        suiProvider,
+        SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
+        slicedVAA
+      )
+    ).toBe(true);
   });
 });

+ 1 - 1
sdk/js/src/token_bridge/__tests__/utils/consts.ts

@@ -23,7 +23,7 @@ export const ETH_PRIVATE_KEY7 =
 export const ETH_PRIVATE_KEY9 =
   "0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"; // account 9 - accountant tests
 export const ETH_PRIVATE_KEY10 =
-  "0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c"; // account 10 - sui tests
+  "0x77c5495fbb039eed474fc940f29955ed0531693cc9212911efd35dff0373153f"; // account 10 - sui tests
 export const SOLANA_HOST = ci
   ? "http://solana-devnet:8899"
   : "http://localhost:8899";

+ 36 - 0
sdk/js/src/token_bridge/__tests__/utils/helpers.ts

@@ -10,6 +10,9 @@ import {
   TERRA_PRIVATE_KEY,
   WORMHOLE_RPC_HOSTS,
 } from "./consts";
+import { SuiTransactionBlockResponse, TransactionBlock } from "@mysten/sui.js";
+import { SuiCoinObject } from "../../../sui/types";
+import { hexZeroPad } from "ethers/lib/utils";
 
 export async function waitForTerraExecution(
   transaction: string,
@@ -111,3 +114,36 @@ export const assertIsNotNullOrUndefined: <T>(
   expect(x).not.toBeNull();
   expect(x).not.toBeUndefined();
 };
+
+export function mintAndTransferCoinSui(
+  treasuryCap: string,
+  coinType: string,
+  amount: bigint,
+  recipient: string
+) {
+  const tx = new TransactionBlock();
+  tx.moveCall({
+    target: "0x2::coin::mint_and_transfer",
+    arguments: [tx.object(treasuryCap), tx.pure(amount), tx.pure(recipient)],
+    typeArguments: [coinType],
+  });
+  return tx;
+}
+
+export const getEmitterAddressAndSequenceFromResponseSui = (
+  coreBridgePackageId: string,
+  response: SuiTransactionBlockResponse
+): { emitterAddress: string; sequence: string } => {
+  const wormholeMessageEventType = `${coreBridgePackageId}::publish_message::WormholeMessage`;
+  const event = response.events?.find(
+    (e) => e.type === wormholeMessageEventType
+  );
+  if (event === undefined) {
+    throw new Error(`${wormholeMessageEventType} event type not found`);
+  }
+  const { sender, sequence } = event.parsedJson || {};
+  if (sender === undefined || sequence === undefined) {
+    throw new Error("Unexpected response payload");
+  }
+  return { emitterAddress: sender.substring(2), sequence };
+};

+ 5 - 5
sdk/js/src/token_bridge/attest.ts

@@ -352,27 +352,27 @@ export function attestFromAptos(
 
 export async function attestFromSui(
   provider: JsonRpcProvider,
-  tokenBridgeAddress: string,
+  tokenBridgePackageId: string,
   coreBridgeStateObjectId: string,
   tokenBridgeStateObjectId: string,
   coinType: string,
-  feeAmount: number
+  feeAmount: BigInt = BigInt(0)
 ): Promise<TransactionBlock> {
   const metadata = await provider.getCoinMetadata({ coinType });
+  console.log(metadata);
   if (!metadata || !metadata.id) {
     throw new Error(`Coin metadata for type ${coinType} not found`);
   }
-
   const tx = new TransactionBlock();
   const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure(feeAmount)]);
   tx.moveCall({
-    target: `${tokenBridgeAddress}::attest_token::attest_token`,
+    target: `${tokenBridgePackageId}::attest_token::attest_token`,
     arguments: [
       tx.object(tokenBridgeStateObjectId),
       tx.object(coreBridgeStateObjectId),
       feeCoin,
       tx.object(metadata.id),
-      tx.pure(69),
+      tx.pure(createNonce().readUInt32LE()),
       tx.object(SUI_CLOCK_OBJECT_ID),
     ],
     typeArguments: [coinType],

+ 7 - 7
sdk/js/src/token_bridge/createWrapped.ts

@@ -168,14 +168,14 @@ export function createWrappedOnAptos(
 }
 
 export async function createWrappedOnSuiPrepare(
-  coreBridgeAddress: string,
-  tokenBridgeAddress: string,
+  coreBridgePackageId: string,
+  tokenBridgePackageId: string,
   attestVAA: Uint8Array,
   signerAddress: string
 ): Promise<TransactionBlock> {
   return publishCoin(
-    coreBridgeAddress,
-    tokenBridgeAddress,
+    coreBridgePackageId,
+    tokenBridgePackageId,
     uint8ArrayToHex(attestVAA),
     signerAddress
   );
@@ -183,7 +183,7 @@ export async function createWrappedOnSuiPrepare(
 
 export async function createWrappedOnSui(
   provider: JsonRpcProvider,
-  tokenBridgeAddress: string,
+  tokenBridgePackageId: string,
   coreBridgeStateObjectId: string,
   tokenBridgeStateObjectId: string,
   signerAddress: string,
@@ -191,7 +191,7 @@ export async function createWrappedOnSui(
 ): Promise<TransactionBlock> {
   // Get WrappedAssetSetup object ID
   const coinType = getWrappedCoinType(coinPackageId);
-  const type = `${tokenBridgeAddress}::create_wrapped::WrappedAssetSetup<${coinType}>`;
+  const type = `${tokenBridgePackageId}::create_wrapped::WrappedAssetSetup<${coinType}>`;
   const res = await provider.getOwnedObjects({
     owner: signerAddress,
     filter: { StructType: type },
@@ -210,7 +210,7 @@ export async function createWrappedOnSui(
   // Construct complete registration payload
   const tx = new TransactionBlock();
   tx.moveCall({
-    target: `${tokenBridgeAddress}::create_wrapped::complete_registration`,
+    target: `${tokenBridgePackageId}::create_wrapped::complete_registration`,
     arguments: [
       tx.object(tokenBridgeStateObjectId),
       tx.object(coreBridgeStateObjectId),

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

@@ -24,6 +24,8 @@ import {
 } from "../utils";
 import { Provider } from "near-api-js/lib/providers";
 import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
+import { JsonRpcProvider } from "@mysten/sui.js";
+import { getTokenCoinType } from "../sui";
 
 /**
  * Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist
@@ -237,3 +239,18 @@ export async function getForeignAssetAptos(
     return null;
   }
 }
+
+export async function getForeignAssetSui(
+  provider: JsonRpcProvider,
+  tokenBridgeStateObjectId: string,
+  originChain: ChainId | ChainName,
+  originAddress: Uint8Array
+): Promise<string | null> {
+  const originChainId = coalesceChainId(originChain);
+  return await getTokenCoinType(
+    provider,
+    tokenBridgeStateObjectId,
+    originAddress,
+    originChainId
+  );
+}

+ 44 - 0
sdk/js/src/token_bridge/getIsTransferCompleted.ts

@@ -27,6 +27,8 @@ import { getClaim } from "../solana/wormhole";
 import { safeBigIntToNumber } from "../utils/bigint";
 import { callFunctionNear } from "../utils/near";
 import { parseVaa, SignedVaa } from "../vaa/wormhole";
+import { JsonRpcProvider } from "@mysten/sui.js";
+import { getInnerType, getObjectFields, getTableKeyType } from "../sui/utils";
 
 export async function getIsTransferCompletedEth(
   tokenBridgeAddress: string,
@@ -296,3 +298,45 @@ export async function getIsTransferCompletedAptos(
     return false;
   }
 }
+
+export async function getIsTransferCompletedSui(
+  provider: JsonRpcProvider,
+  tokenBridgeStateObjectId: string,
+  transferVAA: Uint8Array
+): Promise<boolean> {
+  const tokenBridgeStateFields = await getObjectFields(
+    provider,
+    tokenBridgeStateObjectId
+  );
+  if (!tokenBridgeStateFields) {
+    throw new Error("Unable to fetch object fields from token bridge state");
+  }
+  const hashes = tokenBridgeStateFields.consumed_vaas?.fields?.hashes;
+  const tableObjectId = hashes?.fields?.items?.fields?.id?.id;
+  if (!tableObjectId) {
+    throw new Error("Unable to fetch consumed VAAs table");
+  }
+  const keyType = getTableKeyType(hashes?.fields?.items?.type);
+  if (!keyType) {
+    throw new Error("Unable to get key type");
+  }
+  const hash = getSignedVAAHash(transferVAA);
+  try {
+    // This call throws if the VAA doesn't exist in ConsumedVAAs
+    await provider.getDynamicFieldObject({
+      parentId: tableObjectId,
+      name: {
+        type: keyType,
+        value: {
+          data: [...Buffer.from(hash.slice(2), "hex")],
+        },
+      },
+    });
+    return true;
+  } catch (e: any) {
+    if (e.code === -32000 && e.message?.includes("RPC Error")) {
+      return false;
+    }
+    throw e;
+  }
+}

+ 1 - 7
sdk/js/src/token_bridge/getIsWrappedAsset.ts

@@ -149,7 +149,6 @@ export async function getIsWrappedAssetAptos(
 
 export async function getIsWrappedAssetSui(
   provider: JsonRpcProvider,
-  tokenBridgeAddress: string,
   tokenBridgeStateObjectId: string,
   type: string
 ): Promise<boolean> {
@@ -161,12 +160,7 @@ export async function getIsWrappedAssetSui(
 
   try {
     // This call errors if the type doesn't exist in the TokenRegistry
-    await getTokenFromTokenRegistry(
-      provider,
-      tokenBridgeAddress,
-      tokenBridgeStateObjectId,
-      type
-    );
+    await getTokenFromTokenRegistry(provider, tokenBridgeStateObjectId, type);
     return true;
   } catch (e) {
     if (isSuiError(e) && e.code === -32000 && e.message.includes("RPC Error")) {

+ 21 - 21
sdk/js/src/token_bridge/getOriginalAsset.ts

@@ -318,13 +318,13 @@ export async function getOriginalAssetNear(
 /**
  * Gets the origin chain ID and address of an asset on Aptos, given its fully qualified type.
  * @param client Client used to transfer data to/from Aptos node
- * @param tokenBridgeAddress Address of token bridge
+ * @param tokenBridgePackageId Address of token bridge
  * @param fullyQualifiedType Fully qualified type of asset
  * @returns Original chain ID and address of asset
  */
 export async function getOriginalAssetAptos(
   client: AptosClient,
-  tokenBridgeAddress: string,
+  tokenBridgePackageId: string,
   fullyQualifiedType: string
 ): Promise<WormholeWrappedInfo> {
   if (!isValidAptosType(fullyQualifiedType)) {
@@ -336,7 +336,7 @@ export async function getOriginalAssetAptos(
     originInfo = (
       await client.getAccountResource(
         fullyQualifiedType.split("::")[0],
-        `${tokenBridgeAddress}::state::OriginInfo`
+        `${tokenBridgePackageId}::state::OriginInfo`
       )
     ).data as OriginInfo;
   } catch {
@@ -371,52 +371,52 @@ export async function getOriginalAssetAptos(
 
 export async function getOriginalAssetSui(
   provider: JsonRpcProvider,
-  tokenBridgeAddress: string,
   tokenBridgeStateObjectId: string,
-  type: string
+  coinType: string
 ): Promise<WormholeWrappedInfo> {
-  if (!isValidSuiType(type)) {
-    throw new Error(`Invalid Sui type: ${type}`);
+  if (!isValidSuiType(coinType)) {
+    throw new Error(`Invalid Sui type: ${coinType}`);
   }
 
   const res = await getTokenFromTokenRegistry(
     provider,
-    tokenBridgeAddress,
     tokenBridgeStateObjectId,
-    type
+    coinType
   );
   const fields = getFieldsFromObjectResponse(res);
   if (!fields) {
     throw new Error(
-      `Token of type ${type} has not been registered with the token bridge`
+      `Token of type ${coinType} has not been registered with the token bridge`
     );
   }
 
-  if (
-    fields.value.type ===
-    `${tokenBridgeAddress}::wrapped_asset::WrappedAsset<${type}>`
-  ) {
+  if (fields.value.type.includes(`wrapped_asset::WrappedAsset<${coinType}>`)) {
     return {
       isWrapped: true,
       chainId: Number(
         fields.value.fields.metadata.fields.token_chain
       ) as ChainId,
-      assetAddress:
-        fields.value.fields.metadata.fields.token_address.fields.value.fields
-          .data,
+      assetAddress: new Uint8Array(
+        fields.value.fields.metadata.fields.token_address.fields.value.fields.data
+      ),
     };
   } else if (
-    fields.value.type ===
-    `${tokenBridgeAddress}::native_asset::NativeAsset<${type}>`
+    fields.value.type.includes(`native_asset::NativeAsset<${coinType}>`)
   ) {
     return {
       isWrapped: false,
       chainId: CHAIN_ID_SUI,
-      assetAddress: fields.value.fields.token_address.fields.value.fields.data,
+      assetAddress: new Uint8Array(
+        fields.value.fields.token_address.fields.value.fields.data
+      ),
     };
   }
 
   throw new Error(
-    `Unrecognized token metadata: ${JSON.stringify(fields, null, 2)}, ${type}`
+    `Unrecognized token metadata: ${JSON.stringify(
+      fields,
+      null,
+      2
+    )}, ${coinType}`
   );
 }

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

@@ -49,6 +49,12 @@ import { Provider } from "near-api-js/lib/providers";
 import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
 import { AptosClient, Types } from "aptos";
 import { completeTransferAndRegister } from "../aptos";
+import {
+  JsonRpcProvider,
+  SUI_CLOCK_OBJECT_ID,
+  TransactionBlock,
+} from "@mysten/sui.js";
+import { getTokenCoinType, getObjectFields } from "../sui";
 
 export async function redeemOnEth(
   tokenBridgeAddress: string,
@@ -382,3 +388,47 @@ export function redeemOnAptos(
 ): Promise<Types.EntryFunctionPayload> {
   return completeTransferAndRegister(client, tokenBridgeAddress, transferVAA);
 }
+
+export async function redeemOnSui(
+  provider: JsonRpcProvider,
+  coreBridgePackageId: string,
+  coreBridgeStateObjectId: string,
+  tokenBridgePackageId: string,
+  tokenBridgeStateObjectId: string,
+  transferVAA: Uint8Array
+): Promise<TransactionBlock> {
+  const { tokenAddress, tokenChain } = parseTokenTransferVaa(transferVAA);
+  const coinType = await getTokenCoinType(
+    provider,
+    tokenBridgeStateObjectId,
+    tokenAddress,
+    tokenChain
+  );
+  if (!coinType) {
+    throw new Error("Unable to fetch token coinType");
+  }
+  const tx = new TransactionBlock();
+  const [verifiedVAA] = tx.moveCall({
+    target: `${coreBridgePackageId}::vaa::parse_and_verify`,
+    arguments: [
+      tx.object(coreBridgeStateObjectId),
+      tx.pure([...transferVAA]),
+      tx.object(SUI_CLOCK_OBJECT_ID),
+    ],
+  });
+  const [tokenBridgeMessage] = tx.moveCall({
+    target: `${tokenBridgePackageId}::vaa::verify_only_once`,
+    arguments: [tx.object(tokenBridgeStateObjectId), verifiedVAA],
+  });
+  const [coins] = tx.moveCall({
+    target: `${tokenBridgePackageId}::complete_transfer::complete_transfer`,
+    arguments: [tx.object(tokenBridgeStateObjectId), tokenBridgeMessage],
+    typeArguments: [coinType],
+  });
+  tx.moveCall({
+    target: `${tokenBridgePackageId}::coin_utils::return_nonzero`,
+    arguments: [coins],
+    typeArguments: [coinType],
+  });
+  return tx;
+}

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

@@ -71,6 +71,12 @@ import {
   transferTokens as transferTokensAptos,
   transferTokensWithPayload,
 } from "../aptos";
+import {
+  SUI_CLOCK_OBJECT_ID,
+  SUI_TYPE_ARG,
+  TransactionBlock,
+} from "@mysten/sui.js";
+import { SuiCoinObject } from "../sui/types";
 
 export async function getAllowanceEth(
   tokenBridgeAddress: string,
@@ -1015,3 +1021,62 @@ export function transferFromAptos(
     createNonce().readUInt32LE(0)
   );
 }
+
+export function transferFromSui(
+  coreBridgeStateObjectId: string,
+  tokenBridgePackageId: string,
+  tokenBridgeStateObjectId: string,
+  coins: SuiCoinObject[],
+  coinType: string, // such as 0x2::sui::SUI
+  amount: bigint,
+  recipientChain: ChainId | ChainName,
+  recipient: Uint8Array,
+  feeAmount: bigint = BigInt(0),
+  relayerFee: bigint = BigInt(0)
+) {
+  const [primaryCoin, ...mergeCoins] = coins.filter(
+    (coin) => coin.type === coinType
+  );
+  if (primaryCoin === undefined) {
+    throw new Error(
+      `Coins array doesn't contain any coins of type ${coinType}`
+    );
+  }
+  const tx = new TransactionBlock();
+  const [transferCoin] = (() => {
+    if (coinType === SUI_TYPE_ARG) {
+      return tx.splitCoins(tx.gas, [tx.pure(amount)]);
+    } else {
+      const primaryCoinInput = tx.object(primaryCoin.objectId);
+      if (mergeCoins.length) {
+        tx.mergeCoins(
+          primaryCoinInput,
+          mergeCoins.map((coin) => tx.object(coin.objectId))
+        );
+      }
+      return tx.splitCoins(primaryCoinInput, [tx.pure(amount)]);
+    }
+  })();
+  const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure(feeAmount)]);
+  const [, dust] = tx.moveCall({
+    target: `${tokenBridgePackageId}::transfer_tokens::transfer_tokens`,
+    arguments: [
+      tx.object(tokenBridgeStateObjectId),
+      tx.object(coreBridgeStateObjectId),
+      transferCoin,
+      feeCoin,
+      tx.pure(coalesceChainId(recipientChain)),
+      tx.pure([...recipient]),
+      tx.pure(relayerFee),
+      tx.pure(createNonce().readUInt32LE()),
+      tx.object(SUI_CLOCK_OBJECT_ID),
+    ],
+    typeArguments: [coinType],
+  });
+  tx.moveCall({
+    target: `${tokenBridgePackageId}::coin_utils::return_nonzero`,
+    arguments: [dust],
+    typeArguments: [coinType],
+  });
+  return tx;
+}

+ 12 - 3
sdk/js/src/utils/array.ts

@@ -34,6 +34,8 @@ import {
 } from "./consts";
 import { hashLookup } from "./near";
 import { getExternalAddressFromType, isValidAptosType } from "./aptos";
+import { isValidSuiAddress } from "@mysten/sui.js";
+import { isValidSuiType } from "../sui";
 
 /**
  *
@@ -66,7 +68,7 @@ export const uint8ArrayToHex = (a: Uint8Array): string =>
 export const hexToUint8Array = (h: string): Uint8Array => {
   if (h.startsWith("0x")) h = h.slice(2);
   return new Uint8Array(Buffer.from(h, "hex"));
-}
+};
 
 /**
  *
@@ -246,7 +248,12 @@ export const tryNativeToHexString = (
   } else if (chainId === CHAIN_ID_OSMOSIS) {
     throw Error("hexToNativeString: Osmosis not supported yet.");
   } else if (chainId === CHAIN_ID_SUI) {
-    throw Error("hexToNativeString: Sui not supported yet.");
+    if (!isValidSuiType(address) && isValidSuiAddress(address)) {
+      return uint8ArrayToHex(
+        zeroPad(arrayify(address, { allowMissingPrefix: true }), 32)
+      );
+    }
+    throw Error("hexToNativeString: Sui types not supported yet.");
   } else if (chainId === CHAIN_ID_BTC) {
     throw Error("hexToNativeString: Btc not supported yet.");
   } else if (chainId === CHAIN_ID_APTOS) {
@@ -254,7 +261,9 @@ export const tryNativeToHexString = (
       return getExternalAddressFromType(address);
     }
 
-    return uint8ArrayToHex(zeroPad(arrayify(address, { allowMissingPrefix:true }), 32));
+    return uint8ArrayToHex(
+      zeroPad(arrayify(address, { allowMissingPrefix: true }), 32)
+    );
   } else if (chainId === CHAIN_ID_UNSET) {
     throw Error("hexToNativeString: Chain id unset");
   } else {

+ 12 - 22
sdk/js/src/utils/consts.ts

@@ -514,9 +514,21 @@ const DEVNET = {
       "0x46da3d4c569388af61f951bdd1153f4c875f90c2991f6b2d0a38e2161a40852c",
   },
   sui: {
+<<<<<<< HEAD
     core: "0xb80e44b7c40b874f0162d2440d9f79468132e911c62591eba52fb65a1c9835bb",
+||||||| parent of b909483a (sdk/js: Added transferFromSui and other functions)
+    core: "0xb293dbf970046f0157ec881d46e3ff274ca73430b1ad597d8dbd98c0e54f904a",
+=======
+    core: "0x50d49cf0c8f0ab33b0c4ad1693a2617f6b4fe4dac3e6e2d0ce6e9fbe83795b51", // wormhole module State object ID
+>>>>>>> b909483a (sdk/js: Added transferFromSui and other functions)
     token_bridge:
+<<<<<<< HEAD
       "0x68da393248d51fbe2bd7456414adefdf7eac9ef91b32b561c178f01c906ae80e",
+||||||| parent of b909483a (sdk/js: Added transferFromSui and other functions)
+      "0x7e35f61f38e4f9fedb25cd40100a694f6174d49bd2bdf91d152118c02781aed1",
+=======
+      "0x63406070af4b2ba9ba6a7c47b04ef0fb6d7529c8fa80fe2abe701d8b392cfd3f", // token_bridge module State object ID
+>>>>>>> b909483a (sdk/js: Added transferFromSui and other functions)
     nft_bridge: undefined,
   },
   moonbeam: {
@@ -829,28 +841,6 @@ export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112";
 export const WSOL_DECIMALS = 9;
 export const MAX_VAA_DECIMALS = 8;
 
-/**
- * On Sui, we must hardcode both the package ID as well as the object IDs of
- * the State objects created when we initialize the core and token bridges.
- */
-export const SUI_OBJECT_IDS = {
-  MAINNET: {
-    core_state: undefined,
-    token_bridge_state: undefined,
-  },
-  TESTNET: {
-    core_state: undefined,
-    token_bridge_state: undefined,
-  },
-  DEVNET: {
-    core_state:
-      "0xc1867890b51a1fe873ce34fc4ebc6d87e1ebe30b340d9adccf77e38cf8f2453b",
-    token_bridge_state:
-      "0xfbbe04379273ad9e09463c47c678542a8bf95a3c8e765f42d1adf5a8f54ea9bc",
-  },
-};
-export type SuiAddresses = typeof SUI_OBJECT_IDS;
-
 export const APTOS_DEPLOYER_ADDRESS =
   "0108bc32f7de18a5f6e1e7d6ee7aff9f5fc858d0d87ac0da94dd8d2a5d267d6b";
 export const APTOS_DEPLOYER_ADDRESS_DEVNET =

+ 1 - 1
sui/Dockerfile

@@ -1,4 +1,4 @@
-FROM ghcr.io/wormhole-foundation/sui:0.32.0@sha256:a07bc5fccb81c56f27d38aa4422a16aafe71949f0dd8ed791908b68f22badab4 as sui
+FROM ghcr.io/wormhole-foundation/sui:0.31.0_2 as sui
 
 RUN dnf -y install make git npm
 

+ 1 - 0
wormchain/contracts/tools/deploy_wormchain.ts

@@ -232,6 +232,7 @@ async function main() {
         near: String(process.env.REGISTER_NEAR_TOKEN_BRIDGE_VAA),
         terra2: String(process.env.REGISTER_TERRA2_TOKEN_BRIDGE_VAA),
         aptos: String(process.env.REGISTER_APTOS_TOKEN_BRIDGE_VAA),
+        sui: String(process.env.REGISTER_SUI_TOKEN_BRIDGE_VAA),
     };
 
     const instantiateMsg = {};