소스 검색

sdk/js: Remove Solana WASM Dependencies (#1375)

* sdk/js: Remove Solana WASM Dependencies

* sdk/js: Uptick @solana/web3.js and @solana/spl-token versions

* solana: Add IDLs for Wormhole, Token Bridge and NFT Bridge

* sdk/js: Remove Solana WASM to Use IDL Coders

* sdk/js: Remove src/migration

* sdk/js: Add CPI Account Getters

* testing: Add solana-test-validator SDK tests

* sdk/js: add CHANGELOG.md

* sdk/js: remove await

* sdk/js: fix getIsTransferCompletedAptos

* sdk/js: aptos integration test waits for tx

* sdk/js: remove commented out code

* sdk/js: fix inferred type

Co-authored-by: A5 Pickle <a5-pickle@users.noreply.github.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
A5 Pickle 3 년 전
부모
커밋
e109024e99
100개의 변경된 파일6124개의 추가작업 그리고 1004개의 파일을 삭제
  1. 5 1
      sdk/js/.gitignore
  2. 21 0
      sdk/js/CHANGELOG.md
  3. 367 225
      sdk/js/package-lock.json
  4. 9 3
      sdk/js/package.json
  5. 42 0
      sdk/js/scripts/compileAnchorIdls.js
  6. 0 3
      sdk/js/src/algorand/__tests__/unit.ts
  7. 12 6
      sdk/js/src/bridge/getClaimAddress.ts
  8. 6 7
      sdk/js/src/bridge/getEmitterAddress.ts
  9. 4 16
      sdk/js/src/bridge/getSignedVAAHash.ts
  10. 0 1
      sdk/js/src/cosmwasm/query.testnet.test.ts
  11. 3 4
      sdk/js/src/index.ts
  12. 0 40
      sdk/js/src/migration/addLiquidity.ts
  13. 0 6
      sdk/js/src/migration/authorityAddress.ts
  14. 0 40
      sdk/js/src/migration/claimShares.ts
  15. 0 20
      sdk/js/src/migration/createPool.ts
  16. 0 9
      sdk/js/src/migration/fromCustodyAddress.ts
  17. 0 40
      sdk/js/src/migration/migrateTokens.ts
  18. 0 6
      sdk/js/src/migration/parsePool.ts
  19. 0 10
      sdk/js/src/migration/poolAddress.ts
  20. 0 40
      sdk/js/src/migration/removeLiquidity.ts
  21. 0 9
      sdk/js/src/migration/shareMintAddress.ts
  22. 0 9
      sdk/js/src/migration/toCustodyAddress.ts
  23. 205 0
      sdk/js/src/mock/governance.ts
  24. 5 0
      sdk/js/src/mock/index.ts
  25. 17 0
      sdk/js/src/mock/misc.ts
  26. 87 0
      sdk/js/src/mock/nftBridge.ts
  27. 209 0
      sdk/js/src/mock/tokenBridge.ts
  28. 129 0
      sdk/js/src/mock/wormhole.ts
  29. 6 16
      sdk/js/src/nft_bridge/__tests__/integration.ts
  30. 23 23
      sdk/js/src/nft_bridge/getForeignAsset.ts
  31. 17 13
      sdk/js/src/nft_bridge/getIsTransferCompleted.ts
  32. 19 20
      sdk/js/src/nft_bridge/getIsWrappedAsset.ts
  33. 43 44
      sdk/js/src/nft_bridge/getOriginalAsset.ts
  34. 53 59
      sdk/js/src/nft_bridge/redeem.ts
  35. 85 67
      sdk/js/src/nft_bridge/transfer.ts
  36. 97 0
      sdk/js/src/solana/anchor/common.ts
  37. 10 0
      sdk/js/src/solana/anchor/error.ts
  38. 204 0
      sdk/js/src/solana/anchor/idl.ts
  39. 3 0
      sdk/js/src/solana/anchor/index.ts
  40. 0 25
      sdk/js/src/solana/getBridgeFeeIx.ts
  41. 12 6
      sdk/js/src/solana/index.ts
  42. 18 0
      sdk/js/src/solana/nftBridge/accounts/config.ts
  43. 13 0
      sdk/js/src/solana/nftBridge/accounts/index.ts
  44. 83 0
      sdk/js/src/solana/nftBridge/accounts/wrapped.ts
  45. 40 0
      sdk/js/src/solana/nftBridge/coder/accounts.ts
  46. 12 0
      sdk/js/src/solana/nftBridge/coder/events.ts
  47. 24 0
      sdk/js/src/solana/nftBridge/coder/index.ts
  48. 130 0
      sdk/js/src/solana/nftBridge/coder/instruction.ts
  49. 12 0
      sdk/js/src/solana/nftBridge/coder/state.ts
  50. 12 0
      sdk/js/src/solana/nftBridge/coder/types.ts
  51. 3 0
      sdk/js/src/solana/nftBridge/index.ts
  52. 15 0
      sdk/js/src/solana/nftBridge/instructions/approve.ts
  53. 107 0
      sdk/js/src/solana/nftBridge/instructions/completeNative.ts
  54. 117 0
      sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts
  55. 103 0
      sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts
  56. 161 0
      sdk/js/src/solana/nftBridge/instructions/governance.ts
  57. 8 0
      sdk/js/src/solana/nftBridge/instructions/index.ts
  58. 47 0
      sdk/js/src/solana/nftBridge/instructions/initialize.ts
  59. 122 0
      sdk/js/src/solana/nftBridge/instructions/transferNative.ts
  60. 137 0
      sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts
  61. 43 0
      sdk/js/src/solana/nftBridge/program.ts
  62. 0 212
      sdk/js/src/solana/postVaa.ts
  63. 0 23
      sdk/js/src/solana/rust.ts
  64. 171 0
      sdk/js/src/solana/sendAndConfirmPostVaa.ts
  65. 42 0
      sdk/js/src/solana/tokenBridge/accounts/config.ts
  66. 9 0
      sdk/js/src/solana/tokenBridge/accounts/custody.ts
  67. 70 0
      sdk/js/src/solana/tokenBridge/accounts/endpoint.ts
  68. 7 0
      sdk/js/src/solana/tokenBridge/accounts/index.ts
  69. 20 0
      sdk/js/src/solana/tokenBridge/accounts/signer.ts
  70. 14 0
      sdk/js/src/solana/tokenBridge/accounts/transferWithPayload.ts
  71. 87 0
      sdk/js/src/solana/tokenBridge/accounts/wrapped.ts
  72. 40 0
      sdk/js/src/solana/tokenBridge/coder/accounts.ts
  73. 12 0
      sdk/js/src/solana/tokenBridge/coder/events.ts
  74. 24 0
      sdk/js/src/solana/tokenBridge/coder/index.ts
  75. 254 0
      sdk/js/src/solana/tokenBridge/coder/instruction.ts
  76. 12 0
      sdk/js/src/solana/tokenBridge/coder/state.ts
  77. 12 0
      sdk/js/src/solana/tokenBridge/coder/types.ts
  78. 477 0
      sdk/js/src/solana/tokenBridge/cpi.ts
  79. 4 0
      sdk/js/src/solana/tokenBridge/index.ts
  80. 17 0
      sdk/js/src/solana/tokenBridge/instructions/approve.ts
  81. 97 0
      sdk/js/src/solana/tokenBridge/instructions/attestToken.ts
  82. 105 0
      sdk/js/src/solana/tokenBridge/instructions/completeNative.ts
  83. 110 0
      sdk/js/src/solana/tokenBridge/instructions/completeWrapped.ts
  84. 107 0
      sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts
  85. 161 0
      sdk/js/src/solana/tokenBridge/instructions/governance.ts
  86. 11 0
      sdk/js/src/solana/tokenBridge/instructions/index.ts
  87. 47 0
      sdk/js/src/solana/tokenBridge/instructions/initialize.ts
  88. 118 0
      sdk/js/src/solana/tokenBridge/instructions/transferNative.ts
  89. 125 0
      sdk/js/src/solana/tokenBridge/instructions/transferNativeWithPayload.ts
  90. 129 0
      sdk/js/src/solana/tokenBridge/instructions/transferWrapped.ts
  91. 136 0
      sdk/js/src/solana/tokenBridge/instructions/transferWrappedWithPayload.ts
  92. 33 0
      sdk/js/src/solana/tokenBridge/program.ts
  93. 69 0
      sdk/js/src/solana/utils/account.ts
  94. 23 0
      sdk/js/src/solana/utils/bpfLoaderUpgradeable.ts
  95. 12 0
      sdk/js/src/solana/utils/connection.ts
  96. 6 0
      sdk/js/src/solana/utils/index.ts
  97. 124 0
      sdk/js/src/solana/utils/secp256k1.ts
  98. 331 0
      sdk/js/src/solana/utils/splMetadata.ts
  99. 208 0
      sdk/js/src/solana/utils/transaction.ts
  100. 0 1
      sdk/js/src/solana/wasm.ts

+ 5 - 1
sdk/js/.gitignore

@@ -30,4 +30,8 @@ yarn-error.log*
 /src/proto
 
 # build
-/lib
+/lib
+
+# solana idl
+/src/anchor-idl
+/src/solana/types

+ 21 - 0
sdk/js/CHANGELOG.md

@@ -2,10 +2,30 @@
 
 ## 0.9.0
 
+### Added
+
+Methods to create transaction instructions for Wormhole (Core Bridge), Token Bridge and NFT Bridge
+
+Methods to generate PDAs for Wormhole (Core Bridge), Token Bridge and NFT Bridge
+
+Methods to deserialize account data for Wormhole (Core Bridge), Token Bridge and NFT Bridge
+
+Other Solana utility objects and methods
+
+VAA (Verified Wormhole Message) deserializers
+
+Optional Confirmation arguments for account retrieval and wherever they are relevant
+
+Mock objects to be used in local integration tests (e.g. MockGuardians)
+
 ### Changed
 
 Use FQTs in Aptos SDK
 
+### Removed
+
+Dependency: @certusone/wormhole-sdk-wasm
+
 ## 0.8.0
 
 ### Added
@@ -17,6 +37,7 @@ Aptos support
 Wormchain rename
 
 ## 0.7.2
+
 ### Added
 
 XPLA mainnet support and functions

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 367 - 225
sdk/js/package-lock.json


+ 9 - 3
sdk/js/package.json

@@ -12,7 +12,8 @@
   "scripts": {
     "build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
     "build-abis": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts/abi src/abi/Wormhole.abi.json",
-    "build-deps": "npm run build-abis && npm run build-contracts",
+    "build-idl": "node scripts/compileAnchorIdls.js",
+    "build-deps": "npm run build-abis && npm run build-contracts && npm run build-idl",
     "build-lib": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node scripts/copyEthersTypes.js",
     "build-all": "npm run build-deps && npm run build-lib",
     "test": "jest --config jestconfig.json --verbose",
@@ -43,9 +44,11 @@
     "@injectivelabs/tx-ts": "^1.0.22",
     "@openzeppelin/contracts": "^4.2.0",
     "@typechain/ethers-v5": "^7.0.1",
+    "@types/elliptic": "^6.4.14",
     "@types/jest": "^27.0.2",
     "@types/long": "^4.0.1",
     "@types/node": "^16.6.1",
+    "@types/node-fetch": "^2.6.2",
     "@types/react": "^17.0.19",
     "copy-dir": "^1.3.0",
     "ethers": "^5.6.8",
@@ -61,14 +64,17 @@
     "@certusone/wormhole-sdk-proto-web": "^0.0.5",
     "@certusone/wormhole-sdk-wasm": "^0.0.1",
     "@injectivelabs/sdk-ts": "^1.0.75",
-    "@solana/spl-token": "^0.1.8",
-    "@solana/web3.js": "^1.24.0",
+    "@project-serum/anchor": "^0.25.0",
+    "@solana/spl-token": "^0.3.5",
+    "@solana/web3.js": "^1.66.2",
     "@terra-money/terra.js": "^3.1.3",
     "@xpla/xpla.js": "^0.2.1",
     "algosdk": "^1.15.0",
     "aptos": "^1.3.16",
     "axios": "^0.24.0",
     "bech32": "^2.0.0",
+    "binary-parser": "^2.2.1",
+    "elliptic": "^6.5.4",
     "js-base64": "^3.6.1",
     "near-api-js": "^1.0.0"
   }

+ 42 - 0
sdk/js/scripts/compileAnchorIdls.js

@@ -0,0 +1,42 @@
+const fs = require("fs");
+
+const SRC_IDL = __dirname + "/../../../solana/idl";
+const DST_IDL = __dirname + "/../src/anchor-idl";
+const TS = __dirname + "/../src/solana/types";
+
+const programs = {
+    "wormhole.json": "Wormhole",
+    "token_bridge.json": "TokenBridge",
+    "nft_bridge.json": "NftBridge",
+};
+
+function main() {
+    if (!fs.existsSync(DST_IDL)) {
+        fs.mkdirSync(DST_IDL);
+    }
+
+    if (!fs.existsSync(TS)) {
+        fs.mkdirSync(TS);
+    }
+
+    for (const basename of fs.readdirSync(SRC_IDL)) {
+        const idl = DST_IDL + "/" + basename;
+        fs.copyFileSync(SRC_IDL + "/" + basename, idl);
+
+        const targetTypescript = TS + "/" + snakeToCamel(basename).replace("json", "ts");
+
+        const programType = programs[basename];
+        fs.writeFileSync(targetTypescript, `export type ${programType} = `);
+        fs.appendFileSync(targetTypescript, fs.readFileSync(idl));
+    }
+}
+
+const snakeToCamel = str =>
+  str.toLowerCase().replace(/([-_][a-z])/g, group =>
+    group
+      .toUpperCase()
+      .replace('-', '')
+      .replace('_', '')
+  );
+
+main();

+ 0 - 3
sdk/js/src/algorand/__tests__/unit.ts

@@ -4,7 +4,6 @@ import algosdk, {
   decodeAddress,
   getApplicationAddress,
 } from "algosdk";
-import { setDefaultWasm } from "../../solana/wasm";
 import { hexToUint8Array, uint8ArrayToHex } from "../../utils";
 import {
   accountExists,
@@ -23,8 +22,6 @@ import { PopulateData, TmplSig } from "../TmplSig";
 const CORE_ID = BigInt(4);
 const TOKEN_BRIDGE_ID = BigInt(6);
 
-setDefaultWasm("node");
-
 jest.setTimeout(60000);
 
 describe("Unit Tests", () => {

+ 12 - 6
sdk/js/src/bridge/getClaimAddress.ts

@@ -1,10 +1,16 @@
-import { PublicKey } from "@solana/web3.js";
-import { importCoreWasm } from "../solana/wasm";
+import { PublicKeyInitData } from "@solana/web3.js";
+import { deriveClaimKey } from "../solana/wormhole";
+import { parseVaa, SignedVaa } from "../vaa/wormhole";
 
 export async function getClaimAddressSolana(
-  programAddress: string,
-  signedVAA: Uint8Array
+  programAddress: PublicKeyInitData,
+  signedVaa: SignedVaa
 ) {
-  const { claim_address } = await importCoreWasm();
-  return new PublicKey(claim_address(programAddress, signedVAA));
+  const parsed = parseVaa(signedVaa);
+  return deriveClaimKey(
+    programAddress,
+    parsed.emitterAddress,
+    parsed.emitterChain,
+    parsed.sequence
+  );
 }

+ 6 - 7
sdk/js/src/bridge/getEmitterAddress.ts

@@ -1,4 +1,4 @@
-import { PublicKey } from "@solana/web3.js";
+import { PublicKeyInitData } from "@solana/web3.js";
 import { decodeAddress, getApplicationAddress } from "algosdk";
 import { bech32 } from "bech32";
 import {
@@ -8,7 +8,7 @@ import {
   Hexable,
   zeroPad,
 } from "ethers/lib/utils";
-import { importTokenWasm } from "../solana/wasm";
+import { deriveWormholeEmitterKey } from "../solana/wormhole";
 import { uint8ArrayToHex } from "../utils";
 
 export function getEmitterAddressEth(
@@ -17,11 +17,10 @@ export function getEmitterAddressEth(
   return Buffer.from(zeroPad(arrayify(contractAddress), 32)).toString("hex");
 }
 
-export async function getEmitterAddressSolana(programAddress: string) {
-  const { emitter_address } = await importTokenWasm();
-  return Buffer.from(
-    zeroPad(new PublicKey(emitter_address(programAddress)).toBytes(), 32)
-  ).toString("hex");
+export async function getEmitterAddressSolana(
+  programAddress: PublicKeyInitData
+) {
+  return deriveWormholeEmitterKey(programAddress).toBuffer().toString("hex");
 }
 
 export async function getEmitterAddressTerra(programAddress: string) {

+ 4 - 16
sdk/js/src/bridge/getSignedVAAHash.ts

@@ -1,18 +1,6 @@
-import { importCoreWasm } from "../solana/wasm";
-import { ethers } from "ethers";
-import { uint8ArrayToHex } from "..";
+import { keccak256 } from "../utils";
+import { parseVaa, SignedVaa } from "../vaa/wormhole";
 
-export async function getSignedVAAHash(signedVAA: Uint8Array) {
-  const { parse_vaa } = await importCoreWasm();
-  const parsedVAA = parse_vaa(signedVAA);
-  const body = [
-    ethers.utils.defaultAbiCoder.encode(["uint32"], [parsedVAA.timestamp]).substring(2 + (64 - 8)),
-    ethers.utils.defaultAbiCoder.encode(["uint32"], [parsedVAA.nonce]).substring(2 + (64 - 8)),
-    ethers.utils.defaultAbiCoder.encode(["uint16"], [parsedVAA.emitter_chain]).substring(2 + (64 - 4)),
-    ethers.utils.defaultAbiCoder.encode(["bytes32"], [parsedVAA.emitter_address]).substring(2),
-    ethers.utils.defaultAbiCoder.encode(["uint64"], [parsedVAA.sequence]).substring(2 + (64 - 16)),
-    ethers.utils.defaultAbiCoder.encode(["uint8"], [parsedVAA.consistency_level]).substring(2 + (64 - 2)),
-    uint8ArrayToHex(parsedVAA.payload),
-  ];
-  return ethers.utils.solidityKeccak256(["bytes"], [ethers.utils.solidityKeccak256(["bytes"], ["0x" + body.join("")])]);
+export function getSignedVAAHash(signedVaa: SignedVaa): string {
+  return `0x${keccak256(parseVaa(signedVaa).hash).toString("hex")}`;
 }

+ 0 - 1
sdk/js/src/cosmwasm/query.testnet.test.ts

@@ -1,4 +1,3 @@
-require("dotenv").config({ path: ".env" });
 import { getNetworkInfo, Network } from "@injectivelabs/networks";
 import {
   ChainGrpcWasmApi,

+ 3 - 4
sdk/js/src/index.ts

@@ -1,12 +1,9 @@
-export * from "./cosmos";
-export * from "./ethers-contracts";
-export * from "./solana";
-export * from "./terra";
 export * from "./rpc";
 export * from "./utils";
 export * from "./bridge";
 export * from "./token_bridge";
 export * from "./cosmwasm";
+export * from "./vaa";
 
 export * as cosmos from "./cosmos";
 export * as ethers_contracts from "./ethers-contracts";
@@ -18,3 +15,5 @@ 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 { postVaaSolana, postVaaSolanaWithRetry } from "./solana";

+ 0 - 40
sdk/js/src/migration/addLiquidity.ts

@@ -1,40 +0,0 @@
-import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
-import { ixFromRust } from "../solana";
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function addLiquidity(
-  connection: Connection,
-  payerAddress: string,
-  program_id: string,
-  from_mint: string,
-  to_mint: string,
-  liquidity_token_account: string,
-  lp_share_token_account: string,
-  amount: BigInt
-) {
-  const { authority_address, add_liquidity } = await importMigrationWasm();
-  const approvalIx = Token.createApproveInstruction(
-    TOKEN_PROGRAM_ID,
-    new PublicKey(liquidity_token_account),
-    new PublicKey(authority_address(program_id)),
-    new PublicKey(payerAddress),
-    [],
-    new u64(amount.toString(16), 16)
-  );
-  const ix = ixFromRust(
-    add_liquidity(
-      program_id,
-      from_mint,
-      to_mint,
-      liquidity_token_account,
-      lp_share_token_account,
-      amount.valueOf()
-    )
-  );
-  const transaction = new Transaction().add(approvalIx, ix);
-  const { blockhash } = await connection.getRecentBlockhash();
-  transaction.recentBlockhash = blockhash;
-  transaction.feePayer = new PublicKey(payerAddress);
-  return transaction;
-}

+ 0 - 6
sdk/js/src/migration/authorityAddress.ts

@@ -1,6 +0,0 @@
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function authorityAddress(program_id: string) {
-  const { authority_address } = await importMigrationWasm();
-  return authority_address(program_id);
-}

+ 0 - 40
sdk/js/src/migration/claimShares.ts

@@ -1,40 +0,0 @@
-import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
-import { ixFromRust } from "../solana";
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function claimShares(
-  connection: Connection,
-  payerAddress: string,
-  program_id: string,
-  from_mint: string,
-  to_mint: string,
-  output_token_account: string,
-  lp_share_token_account: string,
-  amount: BigInt
-) {
-  const { authority_address, claim_shares } = await importMigrationWasm();
-  const approvalIx = Token.createApproveInstruction(
-    TOKEN_PROGRAM_ID,
-    new PublicKey(lp_share_token_account),
-    new PublicKey(authority_address(program_id)),
-    new PublicKey(payerAddress),
-    [],
-    new u64(amount.toString(16), 16)
-  );
-  const ix = ixFromRust(
-    claim_shares(
-      program_id,
-      from_mint,
-      to_mint,
-      output_token_account,
-      lp_share_token_account,
-      amount.valueOf()
-    )
-  );
-  const transaction = new Transaction().add(approvalIx, ix);
-  const { blockhash } = await connection.getRecentBlockhash();
-  transaction.recentBlockhash = blockhash;
-  transaction.feePayer = new PublicKey(payerAddress);
-  return transaction;
-}

+ 0 - 20
sdk/js/src/migration/createPool.ts

@@ -1,20 +0,0 @@
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
-import { ixFromRust } from "../solana";
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function createPool(
-  connection: Connection,
-  payerAddress: string,
-  program_id: string,
-  payer: string,
-  from_mint: string,
-  to_mint: string
-) {
-  const { create_pool } = await importMigrationWasm();
-  const ix = ixFromRust(create_pool(program_id, payer, from_mint, to_mint));
-  const transaction = new Transaction().add(ix);
-  const { blockhash } = await connection.getRecentBlockhash();
-  transaction.recentBlockhash = blockhash;
-  transaction.feePayer = new PublicKey(payerAddress);
-  return transaction;
-}

+ 0 - 9
sdk/js/src/migration/fromCustodyAddress.ts

@@ -1,9 +0,0 @@
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function fromCustodyAddress(
-  program_id: string,
-  pool: string
-) {
-  const { from_custody_address } = await importMigrationWasm();
-  return from_custody_address(program_id, pool);
-}

+ 0 - 40
sdk/js/src/migration/migrateTokens.ts

@@ -1,40 +0,0 @@
-import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
-import { ixFromRust } from "../solana";
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function migrateTokens(
-  connection: Connection,
-  payerAddress: string,
-  program_id: string,
-  from_mint: string,
-  to_mint: string,
-  input_token_account: string,
-  output_token_account: string,
-  amount: BigInt
-) {
-  const { authority_address, migrate_tokens } = await importMigrationWasm();
-  const approvalIx = Token.createApproveInstruction(
-    TOKEN_PROGRAM_ID,
-    new PublicKey(input_token_account),
-    new PublicKey(authority_address(program_id)),
-    new PublicKey(payerAddress),
-    [],
-    new u64(amount.toString(16), 16)
-  );
-  const ix = ixFromRust(
-    migrate_tokens(
-      program_id,
-      from_mint,
-      to_mint,
-      input_token_account,
-      output_token_account,
-      amount.valueOf()
-    )
-  );
-  const transaction = new Transaction().add(approvalIx, ix);
-  const { blockhash } = await connection.getRecentBlockhash();
-  transaction.recentBlockhash = blockhash;
-  transaction.feePayer = new PublicKey(payerAddress);
-  return transaction;
-}

+ 0 - 6
sdk/js/src/migration/parsePool.ts

@@ -1,6 +0,0 @@
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function parsePool(data: Uint8Array) {
-  const { parse_pool } = await importMigrationWasm();
-  return parse_pool(data);
-}

+ 0 - 10
sdk/js/src/migration/poolAddress.ts

@@ -1,10 +0,0 @@
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function poolAddress(
-  program_id: string,
-  from_mint: string,
-  to_mint: string
-) {
-  const { pool_address } = await importMigrationWasm();
-  return pool_address(program_id, from_mint, to_mint);
-}

+ 0 - 40
sdk/js/src/migration/removeLiquidity.ts

@@ -1,40 +0,0 @@
-import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
-import { ixFromRust } from "../solana";
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function removeLiquidity(
-  connection: Connection,
-  payerAddress: string,
-  program_id: string,
-  from_mint: string,
-  to_mint: string,
-  liquidity_token_account: string,
-  lp_share_token_account: string,
-  amount: BigInt
-) {
-  const { authority_address, remove_liquidity } = await importMigrationWasm();
-  const approvalIx = Token.createApproveInstruction(
-    TOKEN_PROGRAM_ID,
-    new PublicKey(lp_share_token_account),
-    new PublicKey(authority_address(program_id)),
-    new PublicKey(payerAddress),
-    [],
-    new u64(amount.toString(16), 16)
-  );
-  const ix = ixFromRust(
-    remove_liquidity(
-      program_id,
-      from_mint,
-      to_mint,
-      liquidity_token_account,
-      lp_share_token_account,
-      amount.valueOf()
-    )
-  );
-  const transaction = new Transaction().add(approvalIx, ix);
-  const { blockhash } = await connection.getRecentBlockhash();
-  transaction.recentBlockhash = blockhash;
-  transaction.feePayer = new PublicKey(payerAddress);
-  return transaction;
-}

+ 0 - 9
sdk/js/src/migration/shareMintAddress.ts

@@ -1,9 +0,0 @@
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function shareMintAddress(
-  program_id: string,
-  pool: string
-) {
-  const { share_mint_address } = await importMigrationWasm();
-  return share_mint_address(program_id, pool);
-}

+ 0 - 9
sdk/js/src/migration/toCustodyAddress.ts

@@ -1,9 +0,0 @@
-import { importMigrationWasm } from "../solana/wasm";
-
-export default async function toCustodyAddress(
-  program_id: string,
-  pool: string
-) {
-  const { to_custody_address } = await importMigrationWasm();
-  return to_custody_address(program_id, pool);
-}

+ 205 - 0
sdk/js/src/mock/governance.ts

@@ -0,0 +1,205 @@
+import { BN } from "@project-serum/anchor";
+import { ChainId, tryNativeToHexString } from "../utils";
+import { MockEmitter } from "./wormhole";
+
+const ETHEREUM_KEY_LENGTH = 20;
+
+export class GovernanceEmitter extends MockEmitter {
+  constructor(emitterAddress: string, startSequence?: number) {
+    super(emitterAddress, 1, startSequence);
+  }
+
+  publishGovernanceMessage(
+    timestamp: number,
+    module: string,
+    payload: Buffer,
+    action: number,
+    chain: number,
+    uptickSequence: boolean = true
+  ) {
+    const serialized = Buffer.alloc(35 + payload.length);
+
+    const moduleBytes = Buffer.alloc(32);
+    moduleBytes.write(module, 32 - module.length);
+    serialized.write(moduleBytes.toString("hex"), 0, "hex");
+    serialized.writeUInt8(action, 32); // action
+    serialized.writeUInt16BE(chain, 33);
+    serialized.write(payload.toString("hex"), 35, "hex");
+    return this.publishMessage(0, serialized, 1, timestamp, uptickSequence);
+  }
+
+  publishWormholeSetMessageFee(
+    timestamp: number,
+    chain: number,
+    amount: bigint,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(32);
+    const amountBytes = new BN(amount.toString()).toBuffer();
+    payload.write(amountBytes.toString("hex"), 32 - amountBytes.length, "hex");
+    return this.publishGovernanceMessage(
+      timestamp,
+      "Core",
+      payload,
+      3,
+      chain,
+      uptickSequence
+    );
+  }
+
+  publishWormholeTransferFees(
+    timestamp: number,
+    chain: number,
+    amount: bigint,
+    recipient: Buffer,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(64);
+    const amountBytes = new BN(amount.toString()).toBuffer();
+    payload.write(amountBytes.toString("hex"), 32 - amountBytes.length, "hex");
+    payload.write(recipient.toString("hex"), 32, "hex");
+    return this.publishGovernanceMessage(
+      timestamp,
+      "Core",
+      payload,
+      4,
+      chain,
+      uptickSequence
+    );
+  }
+
+  publishWormholeGuardianSetUpgrade(
+    timestamp: number,
+    newGuardianSetIndex: number,
+    publicKeys: Buffer[],
+    uptickSequence: boolean = true
+  ) {
+    const numKeys = publicKeys.length;
+    const payload = Buffer.alloc(5 + ETHEREUM_KEY_LENGTH * numKeys);
+    payload.writeUInt32BE(newGuardianSetIndex, 0);
+    payload.writeUInt8(numKeys, 4);
+    for (let i = 0; i < numKeys; ++i) {
+      const publicKey = publicKeys.at(i);
+      if (publicKey == undefined) {
+        throw Error("publicKey == undefined");
+      }
+      payload.write(
+        publicKey.toString("hex"),
+        5 + ETHEREUM_KEY_LENGTH * i,
+        "hex"
+      );
+    }
+    return this.publishGovernanceMessage(
+      timestamp,
+      "Core",
+      payload,
+      2,
+      0,
+      uptickSequence
+    );
+  }
+
+  publishWormholeUpgradeContract(
+    timestamp: number,
+    chain: number,
+    newContract: string,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(32);
+    payload.write(
+      tryNativeToHexString(newContract, chain as ChainId),
+      0,
+      "hex"
+    );
+    return this.publishGovernanceMessage(
+      timestamp,
+      "Core",
+      payload,
+      1,
+      chain,
+      uptickSequence
+    );
+  }
+
+  publishTokenBridgeRegisterChain(
+    timestamp: number,
+    chain: number,
+    address: string,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(34);
+    payload.writeUInt16BE(chain, 0);
+    payload.write(tryNativeToHexString(address, chain as ChainId), 2, "hex");
+    return this.publishGovernanceMessage(
+      timestamp,
+      "TokenBridge",
+      payload,
+      1,
+      0,
+      uptickSequence
+    );
+  }
+
+  publishTokenBridgeUpgradeContract(
+    timestamp: number,
+    chain: number,
+    newContract: string,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(32);
+    payload.write(
+      tryNativeToHexString(newContract, this.chain as ChainId),
+      0,
+      "hex"
+    );
+    return this.publishGovernanceMessage(
+      timestamp,
+      "TokenBridge",
+      payload,
+      2,
+      chain,
+      uptickSequence
+    );
+  }
+
+  publishNftBridgeRegisterChain(
+    timestamp: number,
+    chain: number,
+    address: string,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(34);
+    payload.writeUInt16BE(chain, 0);
+    payload.write(tryNativeToHexString(address, chain as ChainId), 2, "hex");
+    return this.publishGovernanceMessage(
+      timestamp,
+      "NFTBridge",
+      payload,
+      1,
+      0,
+      uptickSequence
+    );
+  }
+
+  publishNftBridgeUpgradeContract(
+    timestamp: number,
+    chain: number,
+    newContract: string,
+    uptickSequence: boolean = true
+  ) {
+    const payload = Buffer.alloc(32);
+    payload.write(
+      tryNativeToHexString(newContract, this.chain as ChainId),
+      0,
+      "hex"
+    );
+    return this.publishGovernanceMessage(
+      timestamp,
+      "NFTBridge",
+      payload,
+      2,
+      chain,
+      uptickSequence
+    );
+  }
+}

+ 5 - 0
sdk/js/src/mock/index.ts

@@ -0,0 +1,5 @@
+export * from "./governance";
+export * from "./misc";
+export * from "./nftBridge";
+export * from "./tokenBridge";
+export * from "./wormhole";

+ 17 - 0
sdk/js/src/mock/misc.ts

@@ -0,0 +1,17 @@
+import { keccak256 } from "../utils";
+import * as elliptic from "elliptic";
+
+export function ethPrivateToPublic(key: string) {
+  const ecdsa = new elliptic.ec("secp256k1");
+  const publicKey = ecdsa.keyFromPrivate(key).getPublic("hex");
+  return keccak256(Buffer.from(publicKey, "hex").subarray(1)).subarray(12);
+}
+
+export function ethSignWithPrivate(privateKey: string, hash: Buffer) {
+  if (hash.length != 32) {
+    throw new Error("hash.length != 32");
+  }
+  const ecdsa = new elliptic.ec("secp256k1");
+  const key = ecdsa.keyFromPrivate(privateKey);
+  return key.sign(hash, { canonical: true });
+}

+ 87 - 0
sdk/js/src/mock/nftBridge.ts

@@ -0,0 +1,87 @@
+import { NodePrivilegedServiceChainGovernorReleasePendingVAADesc } from "@certusone/wormhole-sdk-proto-web/lib/cjs/node/v1/node";
+import { BN } from "@project-serum/anchor";
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { ChainId, tryNativeToHexString } from "../utils";
+import { MockEmitter } from "./wormhole";
+
+export class MockNftBridge extends MockEmitter {
+  consistencyLevel: number;
+
+  constructor(emitterAddress: string, chain: number, consistencyLevel: number) {
+    super(emitterAddress, chain);
+    this.consistencyLevel = consistencyLevel;
+  }
+
+  publishNftBridgeMessage(
+    serialized: Buffer,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    return this.publishMessage(
+      nonce == undefined ? 0 : nonce,
+      serialized,
+      this.consistencyLevel,
+      timestamp,
+      uptickSequence
+    );
+  }
+
+  publishTransferNft(
+    tokenAddress: string,
+    tokenChain: number,
+    name: string,
+    symbol: string,
+    tokenId: bigint,
+    uri: string,
+    recipientChain: number,
+    recipient: string,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    if (uri.length > 200) {
+      throw new Error("uri.length > 200");
+    }
+    const serialized = Buffer.alloc(166 + uri.length);
+    serialized.writeUInt8(1, 0);
+    serialized.write(tokenAddress, 1, "hex");
+    serialized.writeUInt16BE(tokenChain, 33);
+    // truncate to 32 characters
+    symbol = symbol.substring(0, 32);
+    serialized.write(symbol, 35);
+    // truncate to 32 characters
+    name = name.substring(0, 32);
+    serialized.write(name, 67);
+    const tokenIdBytes = new BN(tokenId.toString()).toBuffer();
+    serialized.write(
+      tokenIdBytes.toString("hex"),
+      131 - tokenIdBytes.length,
+      "hex"
+    );
+    serialized.writeUInt8(uri.length, 131);
+    serialized.write(uri, 132);
+    const uriEnd = 132 + uri.length;
+    serialized.write(recipient, uriEnd, "hex");
+    serialized.writeUInt16BE(recipientChain, uriEnd + 32);
+    return this.publishNftBridgeMessage(
+      serialized,
+      nonce,
+      timestamp,
+      uptickSequence
+    );
+  }
+}
+
+export class MockEthereumNftBridge extends MockNftBridge {
+  constructor(emitterAddress: string) {
+    const chain = 2;
+    super(tryNativeToHexString(emitterAddress, chain as ChainId), chain, 15);
+  }
+}
+
+export class MockSolanaNftBridge extends MockNftBridge {
+  constructor(emitterAddress: PublicKeyInitData) {
+    super(new PublicKey(emitterAddress).toBuffer().toString("hex"), 1, 32);
+  }
+}

+ 209 - 0
sdk/js/src/mock/tokenBridge.ts

@@ -0,0 +1,209 @@
+import { BN } from "@project-serum/anchor";
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { ChainId, tryNativeToHexString } from "../utils";
+import { MockEmitter } from "./wormhole";
+
+export class MockTokenBridge extends MockEmitter {
+  consistencyLevel: number;
+
+  constructor(emitterAddress: string, chain: number, consistencyLevel: number) {
+    super(emitterAddress, chain);
+    this.consistencyLevel = consistencyLevel;
+  }
+
+  publishTokenBridgeMessage(
+    serialized: Buffer,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    return this.publishMessage(
+      nonce == undefined ? 0 : nonce,
+      serialized,
+      this.consistencyLevel,
+      timestamp,
+      uptickSequence
+    );
+  }
+
+  publishAttestMeta(
+    tokenAddress: string,
+    decimals: number,
+    symbol: string,
+    name: string,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    const serialized = Buffer.alloc(100);
+    serialized.writeUInt8(2, 0);
+    const hexlified = Buffer.from(tokenAddress, "hex");
+    if (hexlified.length != 32) {
+      throw new Error("tokenAddress must be 32 bytes");
+    }
+    serialized.write(hexlified.toString("hex"), 1, "hex");
+    serialized.writeUInt16BE(this.chain, 33);
+    serialized.writeUInt8(decimals, 35);
+    // truncate to 32 characters
+    symbol = symbol.substring(0, 32);
+    serialized.write(symbol, 68 - symbol.length);
+    // truncate to 32 characters
+    name = name.substring(0, 32);
+    serialized.write(name, 100 - name.length);
+    return this.publishTokenBridgeMessage(
+      serialized,
+      nonce,
+      timestamp,
+      uptickSequence
+    );
+  }
+
+  serializeTransferOnly(
+    withPayload: boolean,
+    tokenAddress: string,
+    tokenChain: number,
+    amount: bigint,
+    recipientChain: number,
+    recipient: string,
+    fee?: bigint,
+    fromAddress?: Buffer
+  ) {
+    const serialized = Buffer.alloc(133);
+    serialized.writeUInt8(1, 0);
+    const amountBytes = new BN(amount.toString()).toBuffer();
+    serialized.write(
+      amountBytes.toString("hex"),
+      33 - amountBytes.length,
+      "hex"
+    );
+    serialized.write(tokenAddress, 33, "hex");
+    serialized.writeUInt16BE(tokenChain, 65);
+    serialized.write(recipient, 67, "hex");
+    serialized.writeUInt16BE(recipientChain, 99);
+    if (withPayload) {
+      if (fromAddress === undefined) {
+        throw new Error("fromAddress === undefined");
+      }
+      serialized.write(fromAddress.toString("hex"), 101, "hex");
+    } else {
+      if (fee === undefined) {
+        throw new Error("fee === undefined");
+      }
+      const feeBytes = new BN(fee.toString()).toBuffer();
+      serialized.write(feeBytes.toString("hex"), 133 - feeBytes.length, "hex");
+    }
+    return serialized;
+  }
+
+  publishTransferTokens(
+    tokenAddress: string,
+    tokenChain: number,
+    amount: bigint,
+    recipientChain: number,
+    recipient: string,
+    fee: bigint,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    return this.publishTokenBridgeMessage(
+      this.serializeTransferOnly(
+        false, // withPayload
+        tokenAddress,
+        tokenChain,
+        amount,
+        recipientChain,
+        recipient,
+        fee
+      ),
+      nonce,
+      timestamp,
+      uptickSequence
+    );
+  }
+
+  publishTransferTokensWithPayload(
+    tokenAddress: string,
+    tokenChain: number,
+    amount: bigint,
+    recipientChain: number,
+    recipient: string,
+    fromAddress: Buffer,
+    payload: Buffer,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    return this.publishTokenBridgeMessage(
+      Buffer.concat([
+        this.serializeTransferOnly(
+          true, // withPayload
+          tokenAddress,
+          tokenChain,
+          amount,
+          recipientChain,
+          recipient,
+          undefined, // fee
+          fromAddress
+        ),
+        payload,
+      ]),
+      nonce,
+      timestamp,
+      uptickSequence
+    );
+  }
+}
+
+export class MockEthereumTokenBridge extends MockTokenBridge {
+  constructor(emitterAddress: string) {
+    const chain = 2;
+    super(tryNativeToHexString(emitterAddress, chain as ChainId), chain, 15);
+  }
+
+  publishAttestMeta(
+    tokenAddress: string,
+    decimals: number,
+    symbol: string,
+    name: string,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    return super.publishAttestMeta(
+      tryNativeToHexString(tokenAddress, this.chain as ChainId),
+      decimals,
+      symbol == undefined ? "" : symbol,
+      name == undefined ? "" : name,
+      nonce,
+      timestamp,
+      uptickSequence
+    );
+  }
+}
+
+export class MockSolanaTokenBridge extends MockTokenBridge {
+  constructor(emitterAddress: PublicKeyInitData) {
+    super(new PublicKey(emitterAddress).toBuffer().toString("hex"), 1, 32);
+  }
+
+  publishAttestMeta(
+    mint: PublicKeyInitData,
+    decimals: number,
+    symbol?: string,
+    name?: string,
+    nonce?: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    return super.publishAttestMeta(
+      new PublicKey(mint).toBuffer().toString("hex"),
+      decimals,
+      symbol == undefined ? "" : symbol,
+      name == undefined ? "" : name,
+      nonce,
+      timestamp,
+      uptickSequence
+    );
+  }
+}

+ 129 - 0
sdk/js/src/mock/wormhole.ts

@@ -0,0 +1,129 @@
+import { keccak256 } from "../utils";
+import { ethPrivateToPublic, ethSignWithPrivate } from "./misc";
+
+const SIGNATURE_PAYLOAD_LEN = 66;
+
+interface Guardian {
+  index: number;
+  key: string;
+}
+
+export class MockGuardians {
+  setIndex: number;
+  signers: Guardian[];
+
+  constructor(setIndex: number, keys: string[]) {
+    this.setIndex = setIndex;
+    this.signers = keys.map((key, index): Guardian => {
+      return { index, key };
+    });
+  }
+
+  getPublicKeys() {
+    return this.signers.map((guardian) => ethPrivateToPublic(guardian.key));
+  }
+
+  updateGuardianSetIndex(setIndex: number) {
+    this.setIndex = setIndex;
+  }
+
+  addSignatures(message: Buffer, guardianIndices: number[]) {
+    if (guardianIndices.length == 0) {
+      throw Error("guardianIndices.length == 0");
+    }
+    const signers = this.signers.filter((signer) =>
+      guardianIndices.includes(signer.index)
+    );
+
+    const sigStart = 6;
+    const numSigners = signers.length;
+
+    const signedVaa = Buffer.alloc(
+      sigStart + SIGNATURE_PAYLOAD_LEN * numSigners + message.length
+    );
+    signedVaa.write(
+      message.toString("hex"),
+      sigStart + SIGNATURE_PAYLOAD_LEN * numSigners,
+      "hex"
+    );
+
+    signedVaa.writeUInt8(1, 0);
+    signedVaa.writeUInt32BE(this.setIndex, 1);
+    signedVaa.writeUInt8(numSigners, 5);
+
+    // signatures
+    const hash = keccak256(keccak256(message));
+
+    for (let i = 0; i < numSigners; ++i) {
+      const signer = signers.at(i);
+      if (signer == undefined) {
+        throw Error("signer == undefined");
+      }
+      const signature = ethSignWithPrivate(signer.key, hash);
+
+      const start = sigStart + i * SIGNATURE_PAYLOAD_LEN;
+      signedVaa.writeUInt8(signer.index, start);
+      signedVaa.write(
+        signature.r.toString(16).padStart(64, "0"),
+        start + 1,
+        "hex"
+      );
+      signedVaa.write(
+        signature.s.toString(16).padStart(64, "0"),
+        start + 33,
+        "hex"
+      );
+      signedVaa.writeUInt8(signature.recoveryParam!, start + 65);
+    }
+
+    return signedVaa;
+  }
+}
+
+export class MockEmitter {
+  chain: number;
+  address: Buffer;
+
+  sequence: number;
+
+  constructor(emitterAddress: string, chain: number, startSequence?: number) {
+    this.chain = chain;
+    const address = Buffer.from(emitterAddress, "hex");
+    if (address.length != 32) {
+      throw Error("emitterAddress.length != 32");
+    }
+    this.address = address;
+
+    this.sequence = startSequence == undefined ? 0 : startSequence;
+  }
+
+  publishMessage(
+    nonce: number,
+    payload: Buffer,
+    consistencyLevel: number,
+    timestamp?: number,
+    uptickSequence: boolean = true
+  ) {
+    if (uptickSequence) {
+      ++this.sequence;
+    }
+
+    const message = Buffer.alloc(51 + payload.length);
+
+    message.writeUInt32BE(timestamp == undefined ? 0 : timestamp, 0);
+    message.writeUInt32BE(nonce, 4);
+    message.writeUInt16BE(this.chain, 8);
+    message.write(this.address.toString("hex"), 10, "hex");
+    message.writeBigUInt64BE(BigInt(this.sequence), 42);
+    message.writeUInt8(consistencyLevel, 50);
+    message.write(payload.toString("hex"), 51, "hex");
+
+    return message;
+  }
+}
+
+export class MockEthereumEmitter extends MockEmitter {
+  constructor(emitterAddress: string, chain?: number) {
+    super(emitterAddress, chain == undefined ? 2 : chain);
+  }
+}

+ 6 - 16
sdk/js/src/nft_bridge/__tests__/integration.ts

@@ -11,7 +11,7 @@ import {
 } from "@jest/globals";
 import {
   ASSOCIATED_TOKEN_PROGRAM_ID,
-  Token,
+  getAssociatedTokenAddress,
   TOKEN_PROGRAM_ID,
 } from "@solana/spl-token";
 import { BigNumber, BigNumberish, ethers } from "ethers";
@@ -28,10 +28,8 @@ import {
   parseSequenceFromLogSolana,
   getEmitterAddressSolana,
   CHAIN_ID_SOLANA,
-  parseNFTPayload,
 } from "../..";
 import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
-import { importCoreWasm, setDefaultWasm } from "../../solana/wasm";
 import {
   ETH_NODE_URL,
   ETH_PRIVATE_KEY,
@@ -59,10 +57,10 @@ import {
 import { postVaaSolanaWithRetry } from "../../solana";
 import { tryNativeToUint8Array } from "../../utils";
 import { arrayify } from "ethers/lib/utils";
+import { parseVaa } from "../../vaa/wormhole";
+import { parseNftTransferVaa } from "../../vaa";
 const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json");
 
-setDefaultWasm("node");
-
 jest.setTimeout(60000);
 
 type Address = string;
@@ -158,11 +156,7 @@ describe("Integration Tests", () => {
   test("Send Solana SPL to Ethereum and back", (done) => {
     (async () => {
       try {
-        const { parse_vaa } = await importCoreWasm();
-
-        const fromAddress = await Token.getAssociatedTokenAddress(
-          ASSOCIATED_TOKEN_PROGRAM_ID,
-          TOKEN_PROGRAM_ID,
+        const fromAddress = await getAssociatedTokenAddress(
           new PublicKey(TEST_SOLANA_TOKEN),
           keypair.publicKey
         );
@@ -177,9 +171,7 @@ describe("Integration Tests", () => {
         let signedVAA = await waitUntilSolanaTxObserved(transaction1);
 
         // we get the solana token id from the VAA
-        const { tokenId } = parseNFTPayload(
-          Buffer.from(new Uint8Array(parse_vaa(signedVAA).payload))
-        );
+        const { tokenId } = parseNftTransferVaa(signedVAA);
 
         await _redeemOnEth(signedVAA);
         const eth_addr = await nft_bridge.getForeignAssetEth(
@@ -200,9 +192,7 @@ describe("Integration Tests", () => {
         );
         signedVAA = await waitUntilEthTxObserved(transaction3);
 
-        const { name, symbol } = parseNFTPayload(
-          Buffer.from(new Uint8Array(parse_vaa(signedVAA).payload))
-        );
+        const { name, symbol } = parseNftTransferVaa(signedVAA);
 
         // if the names match up here, it means all the spl caches work
         expect(name).toBe("Not a PUNK🎸");

+ 23 - 23
sdk/js/src/nft_bridge/getForeignAsset.ts

@@ -1,28 +1,30 @@
-import { PublicKey } from "@solana/web3.js";
+import { BN } from "@project-serum/anchor";
+import { PublicKeyInitData } from "@solana/web3.js";
 import { LCDClient } from "@terra-money/terra.js";
 import { ethers } from "ethers";
+import { isBytes } from "ethers/lib/utils";
 import { fromUint8Array } from "js-base64";
 import { CHAIN_ID_SOLANA } from "..";
 import { NFTBridge__factory } from "../ethers-contracts";
-import { importNftWasm } from "../solana/wasm";
+import { deriveWrappedMintKey } from "../solana/nftBridge";
 import { ChainId, ChainName, coalesceChainId } 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 nftBridgeAddress
  * @param provider
  * @param originChain
  * @param originAsset zero pad to 32 bytes
  * @returns
  */
 export async function getForeignAssetEth(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   provider: ethers.Signer | ethers.providers.Provider,
   originChain: ChainId | ChainName,
   originAsset: Uint8Array
 ): Promise<string | null> {
   const originChainId = coalesceChainId(originChain);
-  const tokenBridge = NFTBridge__factory.connect(tokenBridgeAddress, provider);
+  const tokenBridge = NFTBridge__factory.connect(nftBridgeAddress, provider);
   try {
     if (originChainId === CHAIN_ID_SOLANA) {
       // All NFTs from Solana are minted to the same address, the originAsset is encoded as the tokenId as
@@ -41,14 +43,14 @@ export async function getForeignAssetEth(
 
 /**
  * Returns a foreign asset address on Terra for a provided native chain and asset address
- * @param tokenBridgeAddress
+ * @param nftBridgeAddress
  * @param client
  * @param originChain
  * @param originAsset
  * @returns
  */
 export async function getForeignAssetTerra(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   client: LCDClient,
   originChain: ChainId,
   originAsset: Uint8Array
@@ -60,7 +62,7 @@ export async function getForeignAssetTerra(
         ? "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="
         : fromUint8Array(originAsset);
     const result: { address: string } = await client.wasm.contractQuery(
-      tokenBridgeAddress,
+      nftBridgeAddress,
       {
         wrapped_registry: {
           chain: originChainId,
@@ -76,26 +78,24 @@ export async function getForeignAssetTerra(
 
 /**
  * Returns a foreign asset address on Solana for a provided native chain and asset address
- * @param tokenBridgeAddress
+ * @param nftBridgeAddress
  * @param originChain
  * @param originAsset zero pad to 32 bytes
  * @returns
  */
-export async function getForeignAssetSol(
-  tokenBridgeAddress: string,
+export async function getForeignAssetSolana(
+  nftBridgeAddress: PublicKeyInitData,
   originChain: ChainId | ChainName,
-  originAsset: Uint8Array,
-  tokenId: Uint8Array
+  originAsset: string | Uint8Array | Buffer,
+  tokenId: Uint8Array | Buffer | bigint
 ): Promise<string> {
-  const originChainId = coalesceChainId(originChain);
-  const { wrapped_address } = await importNftWasm();
-  const wrappedAddress = wrapped_address(
-    tokenBridgeAddress,
-    originAsset,
-    originChainId,
-    tokenId
-  );
-  const wrappedAddressPK = new PublicKey(wrappedAddress);
   // we don't require NFT accounts to exist, so don't check them.
-  return wrappedAddressPK.toString();
+  return deriveWrappedMintKey(
+    nftBridgeAddress,
+    coalesceChainId(originChain) as number,
+    originAsset,
+    isBytes(tokenId) ? BigInt(new BN(tokenId).toString()) : tokenId
+  ).toString();
 }
+
+export const getForeignAssetSol = getForeignAssetSolana;

+ 17 - 13
sdk/js/src/nft_bridge/getIsTransferCompleted.ts

@@ -1,12 +1,13 @@
 import { ethers } from "ethers";
 import { NFTBridge__factory } from "../ethers-contracts";
 import { getSignedVAAHash } from "../bridge";
-import { importCoreWasm } from "../solana/wasm";
-import { Connection, PublicKey } from "@solana/web3.js";
+import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js";
 import { LCDClient } from "@terra-money/terra.js";
 import axios from "axios";
 import { redeemOnTerra } from ".";
 import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
+import { getClaim } from "../solana/wormhole";
+import { parseVaa, SignedVaa } from "../vaa/wormhole";
 
 export async function getIsTransferCompletedEth(
   nftBridgeAddress: string,
@@ -14,7 +15,7 @@ export async function getIsTransferCompletedEth(
   signedVAA: Uint8Array
 ) {
   const nftBridge = NFTBridge__factory.connect(nftBridgeAddress, provider);
-  const signedVAAHash = await getSignedVAAHash(signedVAA);
+  const signedVAAHash = getSignedVAAHash(signedVAA);
   return await nftBridge.isTransferCompleted(signedVAAHash);
 }
 
@@ -57,15 +58,18 @@ export async function getIsTransferCompletedTerra(
 }
 
 export async function getIsTransferCompletedSolana(
-  nftBridgeAddress: string,
-  signedVAA: Uint8Array,
-  connection: Connection
+  nftBridgeAddress: PublicKeyInitData,
+  signedVAA: SignedVaa,
+  connection: Connection,
+  commitment?: Commitment
 ) {
-  const { claim_address } = await importCoreWasm();
-  const claimAddress = await claim_address(nftBridgeAddress, signedVAA);
-  const claimInfo = await connection.getAccountInfo(
-    new PublicKey(claimAddress),
-    "confirmed"
-  );
-  return !!claimInfo;
+  const parsed = parseVaa(signedVAA);
+  return getClaim(
+    connection,
+    nftBridgeAddress,
+    parsed.emitterAddress,
+    parsed.emitterChain,
+    parsed.sequence,
+    commitment
+  ).catch((e) => false);
 }

+ 19 - 20
sdk/js/src/nft_bridge/getIsWrappedAsset.ts

@@ -1,46 +1,45 @@
-import { Connection, PublicKey } from "@solana/web3.js";
+import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js";
 import { ethers } from "ethers";
 import { Bridge__factory } from "../ethers-contracts";
-import { importNftWasm } from "../solana/wasm";
+import { getWrappedMeta } from "../solana/nftBridge";
 
 /**
  * Returns whether or not an asset address on Ethereum is a wormhole wrapped asset
- * @param tokenBridgeAddress
+ * @param nftBridgeAddress
  * @param provider
  * @param assetAddress
  * @returns
  */
 export async function getIsWrappedAssetEth(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   provider: ethers.Signer | ethers.providers.Provider,
   assetAddress: string
 ) {
   if (!assetAddress) return false;
-  const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
+  const tokenBridge = Bridge__factory.connect(nftBridgeAddress, provider);
   return await tokenBridge.isWrappedAsset(assetAddress);
 }
 
 /**
  * Returns whether or not an asset on Solana is a wormhole wrapped asset
  * @param connection
- * @param tokenBridgeAddress
+ * @param nftBridgeAddress
  * @param mintAddress
+ * @param [commitment]
  * @returns
  */
-export async function getIsWrappedAssetSol(
+export async function getIsWrappedAssetSolana(
   connection: Connection,
-  tokenBridgeAddress: string,
-  mintAddress: string
+  nftBridgeAddress: PublicKeyInitData,
+  mintAddress: PublicKeyInitData,
+  commitment?: Commitment
 ) {
-  if (!mintAddress) return false;
-  const { wrapped_meta_address } = await importNftWasm();
-  const wrappedMetaAddress = wrapped_meta_address(
-    tokenBridgeAddress,
-    new PublicKey(mintAddress).toBytes()
-  );
-  const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
-  const wrappedMetaAccountInfo = await connection.getAccountInfo(
-    wrappedMetaAddressPK
-  );
-  return !!wrappedMetaAccountInfo;
+  if (!mintAddress) {
+    return false;
+  }
+  return getWrappedMeta(connection, nftBridgeAddress, mintAddress, commitment)
+    .catch((_) => null)
+    .then((meta) => meta != null);
 }
+
+export const getIsWrappedAssetSol = getIsWrappedAssetSolana;

+ 43 - 44
sdk/js/src/nft_bridge/getOriginalAsset.ts

@@ -1,10 +1,16 @@
-import { Connection, PublicKey } from "@solana/web3.js";
+import {
+  Commitment,
+  Connection,
+  PublicKey,
+  PublicKeyInitData,
+} from "@solana/web3.js";
 import { LCDClient } from "@terra-money/terra.js";
 import { BigNumber, ethers } from "ethers";
 import { arrayify, zeroPad } from "ethers/lib/utils";
-import { canonicalAddress, WormholeWrappedInfo } from "..";
+import { WormholeWrappedInfo } from "..";
+import { canonicalAddress } from "../cosmos";
 import { TokenImplementation__factory } from "../ethers-contracts";
-import { importNftWasm } from "../solana/wasm";
+import { getWrappedMeta } from "../solana/nftBridge";
 import {
   ChainId,
   ChainName,
@@ -24,20 +30,20 @@ export interface WormholeWrappedNFTInfo {
 
 /**
  * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
- * @param tokenBridgeAddress
+ * @param nftBridgeAddress
  * @param provider
  * @param wrappedAddress
  * @returns
  */
 export async function getOriginalAssetEth(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   provider: ethers.Signer | ethers.providers.Provider,
   wrappedAddress: string,
   tokenId: string,
   lookupChain: ChainId | ChainName
 ): Promise<WormholeWrappedNFTInfo> {
   const isWrapped = await getIsWrappedAssetEth(
-    tokenBridgeAddress,
+    nftBridgeAddress,
     provider,
     wrappedAddress
   );
@@ -69,56 +75,49 @@ export async function getOriginalAssetEth(
 /**
  * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
  * @param connection
- * @param tokenBridgeAddress
+ * @param nftBridgeAddress
  * @param mintAddress
+ * @param [commitment]
  * @returns
  */
-export async function getOriginalAssetSol(
+export async function getOriginalAssetSolana(
   connection: Connection,
-  tokenBridgeAddress: string,
-  mintAddress: string
+  nftBridgeAddress: PublicKeyInitData,
+  mintAddress: PublicKeyInitData,
+  commitment?: Commitment
 ): Promise<WormholeWrappedNFTInfo> {
-  if (mintAddress) {
-    // TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
-    const { parse_wrapped_meta, wrapped_meta_address } = await importNftWasm();
-    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);
-      const token_id_arr = parsed.token_id as BigUint64Array;
-      const token_id_bytes = [];
-      for (let elem of token_id_arr.reverse()) {
-        token_id_bytes.push(...bigToUint8Array(elem));
-      }
-      const token_id = BigNumber.from(token_id_bytes).toString();
-      return {
-        isWrapped: true,
-        chainId: parsed.chain,
-        assetAddress: parsed.token_address,
-        tokenId: token_id,
-      };
-    }
-  }
   try {
+    const mint = new PublicKey(mintAddress);
+
+    return getWrappedMeta(connection, nftBridgeAddress, mintAddress, commitment)
+      .catch((_) => null)
+      .then((meta) => {
+        if (meta === null) {
+          return {
+            isWrapped: false,
+            chainId: CHAIN_ID_SOLANA,
+            assetAddress: mint.toBytes(),
+          };
+        } else {
+          return {
+            isWrapped: true,
+            chainId: meta.chain as ChainId,
+            assetAddress: Uint8Array.from(meta.tokenAddress),
+            tokenId: meta.tokenId.toString(),
+          };
+        }
+      });
+  } catch (_) {
     return {
       isWrapped: false,
       chainId: CHAIN_ID_SOLANA,
-      assetAddress: new PublicKey(mintAddress).toBytes(),
+      assetAddress: new Uint8Array(32),
     };
-  } catch (e) {}
-  return {
-    isWrapped: false,
-    chainId: CHAIN_ID_SOLANA,
-    assetAddress: new Uint8Array(32),
-  };
+  }
 }
 
+export const getOriginalAssetSol = getOriginalAssetSolana;
+
 // Derived from https://www.jackieli.dev/posts/bigint-to-uint8array/
 const big0 = BigInt(0);
 const big1 = BigInt(1);

+ 53 - 59
sdk/js/src/nft_bridge/redeem.ts

@@ -1,19 +1,29 @@
-import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import {
+  Commitment,
+  Connection,
+  PublicKey,
+  PublicKeyInitData,
+  Transaction,
+} from "@solana/web3.js";
 import { MsgExecuteContract } from "@terra-money/terra.js";
 import { ethers, Overrides } from "ethers";
 import { fromUint8Array } from "js-base64";
 import { CHAIN_ID_SOLANA } from "..";
 import { Bridge__factory } from "../ethers-contracts";
-import { ixFromRust } from "../solana";
-import { importCoreWasm, importNftWasm } from "../solana/wasm";
+import {
+  createCompleteTransferNativeInstruction,
+  createCompleteTransferWrappedInstruction,
+  createCompleteWrappedMetaInstruction,
+} from "../solana/nftBridge";
+import { parseNftTransferVaa, parseVaa, SignedVaa } from "../vaa";
 
 export async function redeemOnEth(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   signer: ethers.Signer,
   signedVAA: Uint8Array,
   overrides: Overrides & { from?: string | Promise<string> } = {}
 ): Promise<ethers.ContractReceipt> {
-  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const bridge = Bridge__factory.connect(nftBridgeAddress, signer);
   const v = await bridge.completeTransfer(signedVAA, overrides);
   const receipt = await v.wait();
   return receipt;
@@ -22,52 +32,33 @@ export async function redeemOnEth(
 export async function isNFTVAASolanaNative(
   signedVAA: Uint8Array
 ): Promise<boolean> {
-  const { parse_vaa } = await importCoreWasm();
-  const parsedVAA = parse_vaa(signedVAA);
-  const isSolanaNative =
-    Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(33) ===
-    CHAIN_ID_SOLANA;
-  return isSolanaNative;
+  return parseVaa(signedVAA).payload.readUInt16BE(33) === CHAIN_ID_SOLANA;
 }
 
 export async function redeemOnSolana(
   connection: Connection,
-  bridgeAddress: string,
-  tokenBridgeAddress: string,
-  payerAddress: string,
-  signedVAA: Uint8Array
+  bridgeAddress: PublicKeyInitData,
+  nftBridgeAddress: PublicKeyInitData,
+  payerAddress: PublicKeyInitData,
+  signedVaa: SignedVaa,
+  toAuthorityAddress?: PublicKeyInitData,
+  commitment?: Commitment
 ): Promise<Transaction> {
-  const isSolanaNative = await isNFTVAASolanaNative(signedVAA);
-  const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
-    await importNftWasm();
-  const ixs = [];
-  if (isSolanaNative) {
-    ixs.push(
-      ixFromRust(
-        complete_transfer_native_ix(
-          tokenBridgeAddress,
-          bridgeAddress,
-          payerAddress,
-          payerAddress, //TODO: allow for a different address than payer
-          signedVAA
-        )
-      )
-    );
-  } else {
-    ixs.push(
-      ixFromRust(
-        complete_transfer_wrapped_ix(
-          tokenBridgeAddress,
-          bridgeAddress,
-          payerAddress,
-          payerAddress, //TODO: allow for a different address than payer
-          signedVAA
-        )
-      )
-    );
-  }
-  const transaction = new Transaction().add(...ixs);
-  const { blockhash } = await connection.getRecentBlockhash();
+  const parsed = parseNftTransferVaa(signedVaa);
+  const createCompleteTransferInstruction =
+    parsed.tokenChain == CHAIN_ID_SOLANA
+      ? createCompleteTransferNativeInstruction
+      : createCompleteTransferWrappedInstruction;
+  const transaction = new Transaction().add(
+    createCompleteTransferInstruction(
+      nftBridgeAddress,
+      bridgeAddress,
+      payerAddress,
+      parsed,
+      toAuthorityAddress
+    )
+  );
+  const { blockhash } = await connection.getLatestBlockhash(commitment);
   transaction.recentBlockhash = blockhash;
   transaction.feePayer = new PublicKey(payerAddress);
   return transaction;
@@ -75,33 +66,36 @@ export async function redeemOnSolana(
 
 export async function createMetaOnSolana(
   connection: Connection,
-  bridgeAddress: string,
-  tokenBridgeAddress: string,
-  payerAddress: string,
-  signedVAA: Uint8Array
+  bridgeAddress: PublicKeyInitData,
+  nftBridgeAddress: PublicKeyInitData,
+  payerAddress: PublicKeyInitData,
+  signedVaa: SignedVaa,
+  commitment?: Commitment
 ): Promise<Transaction> {
-  const { complete_transfer_wrapped_meta_ix } = await importNftWasm();
-  const ix = ixFromRust(
-    complete_transfer_wrapped_meta_ix(
-      tokenBridgeAddress,
+  const parsed = parseNftTransferVaa(signedVaa);
+  if (parsed.tokenChain == CHAIN_ID_SOLANA) {
+    return Promise.reject("parsed.tokenChain == CHAIN_ID_SOLANA");
+  }
+  const transaction = new Transaction().add(
+    createCompleteWrappedMetaInstruction(
+      nftBridgeAddress,
       bridgeAddress,
       payerAddress,
-      signedVAA
+      parsed
     )
   );
-  const transaction = new Transaction().add(ix);
-  const { blockhash } = await connection.getRecentBlockhash();
+  const { blockhash } = await connection.getLatestBlockhash(commitment);
   transaction.recentBlockhash = blockhash;
   transaction.feePayer = new PublicKey(payerAddress);
   return transaction;
 }
 
 export async function redeemOnTerra(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   walletAddress: string,
   signedVAA: Uint8Array
 ): Promise<MsgExecuteContract> {
-  return new MsgExecuteContract(walletAddress, tokenBridgeAddress, {
+  return new MsgExecuteContract(walletAddress, nftBridgeAddress, {
     submit_vaa: {
       data: fromUint8Array(signedVAA),
     },

+ 85 - 67
sdk/js/src/nft_bridge/transfer.ts

@@ -1,17 +1,36 @@
-import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
-import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
+import { createApproveInstruction } from "@solana/spl-token";
+import {
+  Commitment,
+  Connection,
+  Keypair,
+  PublicKey,
+  PublicKeyInitData,
+  Transaction,
+} from "@solana/web3.js";
 import { MsgExecuteContract } from "@terra-money/terra.js";
 import { ethers, Overrides } from "ethers";
+import { BN } from "@project-serum/anchor";
 import {
   NFTBridge__factory,
   NFTImplementation__factory,
 } from "../ethers-contracts";
-import { getBridgeFeeIx, ixFromRust } from "../solana";
-import { importNftWasm } from "../solana/wasm";
-import { ChainId, ChainName, CHAIN_ID_SOLANA, coalesceChainId, createNonce } from "../utils";
+import { createBridgeFeeTransferInstruction } from "../solana";
+import {
+  createApproveAuthoritySignerInstruction,
+  createTransferNativeInstruction,
+  createTransferWrappedInstruction,
+} from "../solana/nftBridge";
+import {
+  ChainId,
+  ChainName,
+  CHAIN_ID_SOLANA,
+  coalesceChainId,
+  createNonce,
+} from "../utils";
+import { isBytes } from "ethers/lib/utils";
 
 export async function transferFromEth(
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   signer: ethers.Signer,
   tokenAddress: string,
   tokenID: ethers.BigNumberish,
@@ -19,11 +38,11 @@ export async function transferFromEth(
   recipientAddress: Uint8Array,
   overrides: Overrides & { from?: string | Promise<string> } = {}
 ): Promise<ethers.ContractReceipt> {
-  const recipientChainId = coalesceChainId(recipientChain)
+  const recipientChainId = coalesceChainId(recipientChain);
   //TODO: should we check if token attestation exists on the target chain
   const token = NFTImplementation__factory.connect(tokenAddress, signer);
-  await (await token.approve(tokenBridgeAddress, tokenID, overrides)).wait();
-  const bridge = NFTBridge__factory.connect(tokenBridgeAddress, signer);
+  await (await token.approve(nftBridgeAddress, tokenID, overrides)).wait();
+  const bridge = NFTBridge__factory.connect(nftBridgeAddress, signer);
   const v = await bridge.transferNFT(
     tokenAddress,
     tokenID,
@@ -38,90 +57,89 @@ export async function transferFromEth(
 
 export async function transferFromSolana(
   connection: Connection,
-  bridgeAddress: string,
-  tokenBridgeAddress: string,
-  payerAddress: string,
-  fromAddress: string,
-  mintAddress: string,
-  targetAddress: Uint8Array,
+  bridgeAddress: PublicKeyInitData,
+  nftBridgeAddress: PublicKeyInitData,
+  payerAddress: PublicKeyInitData,
+  fromAddress: PublicKeyInitData,
+  mintAddress: PublicKeyInitData,
+  targetAddress: Uint8Array | Buffer,
   targetChain: ChainId | ChainName,
-  originAddress?: Uint8Array,
+  originAddress?: Uint8Array | Buffer,
   originChain?: ChainId | ChainName,
-  originTokenId?: Uint8Array
+  originTokenId?: Uint8Array | Buffer | number | bigint,
+  commitment?: Commitment
 ): Promise<Transaction> {
-  const originChainId: ChainId | undefined = originChain ? coalesceChainId(originChain) : undefined
+  const originChainId: ChainId | undefined = originChain
+    ? coalesceChainId(originChain)
+    : undefined;
   const nonce = createNonce().readUInt32LE(0);
-  const transferIx = await getBridgeFeeIx(
+  const transferIx = await createBridgeFeeTransferInstruction(
     connection,
     bridgeAddress,
     payerAddress
   );
-  const {
-    transfer_native_ix,
-    transfer_wrapped_ix,
-    approval_authority_address,
-  } = await importNftWasm();
-  const approvalIx = Token.createApproveInstruction(
-    TOKEN_PROGRAM_ID,
-    new PublicKey(fromAddress),
-    new PublicKey(approval_authority_address(tokenBridgeAddress)),
-    new PublicKey(payerAddress),
-    [],
-    Number(1)
+  const approvalIx = createApproveAuthoritySignerInstruction(
+    nftBridgeAddress,
+    fromAddress,
+    payerAddress
   );
-  let messageKey = Keypair.generate();
+  let message = Keypair.generate();
   const isSolanaNative =
     originChain === undefined || originChain === CHAIN_ID_SOLANA;
   if (!isSolanaNative && (!originAddress || !originTokenId)) {
-    throw new Error(
+    return Promise.reject(
       "originAddress and originTokenId are required when specifying originChain"
     );
   }
-  const ix = ixFromRust(
-    isSolanaNative
-      ? transfer_native_ix(
-          tokenBridgeAddress,
-          bridgeAddress,
-          payerAddress,
-          messageKey.publicKey.toString(),
-          fromAddress,
-          mintAddress,
-          nonce,
-          targetAddress,
-          coalesceChainId(targetChain)
-        )
-      : transfer_wrapped_ix(
-          tokenBridgeAddress,
-          bridgeAddress,
-          payerAddress,
-          messageKey.publicKey.toString(),
-          fromAddress,
-          payerAddress,
-          originChainId as number, // checked by isSolanaNative
-          originAddress as Uint8Array, // checked by throw
-          originTokenId as Uint8Array, // checked by throw
-          nonce,
-          targetAddress,
-          coalesceChainId(targetChain)
-        )
+  const nftBridgeTransferIx = isSolanaNative
+    ? createTransferNativeInstruction(
+        nftBridgeAddress,
+        bridgeAddress,
+        payerAddress,
+        message.publicKey,
+        fromAddress,
+        mintAddress,
+        nonce,
+        targetAddress,
+        coalesceChainId(targetChain)
+      )
+    : createTransferWrappedInstruction(
+        nftBridgeAddress,
+        bridgeAddress,
+        payerAddress,
+        message.publicKey,
+        fromAddress,
+        payerAddress,
+        originChainId!,
+        originAddress!,
+        isBytes(originTokenId)
+          ? BigInt(new BN(originTokenId).toString())
+          : originTokenId!,
+        nonce,
+        targetAddress,
+        coalesceChainId(targetChain)
+      );
+  const transaction = new Transaction().add(
+    transferIx,
+    approvalIx,
+    nftBridgeTransferIx
   );
-  const transaction = new Transaction().add(transferIx, approvalIx, ix);
-  const { blockhash } = await connection.getRecentBlockhash();
+  const { blockhash } = await connection.getLatestBlockhash(commitment);
   transaction.recentBlockhash = blockhash;
   transaction.feePayer = new PublicKey(payerAddress);
-  transaction.partialSign(messageKey);
+  transaction.partialSign(message);
   return transaction;
 }
 
 export async function transferFromTerra(
   walletAddress: string,
-  tokenBridgeAddress: string,
+  nftBridgeAddress: string,
   tokenAddress: string,
   tokenID: string,
   recipientChain: ChainId | ChainName,
   recipientAddress: Uint8Array
 ): Promise<MsgExecuteContract[]> {
-  const recipientChainId = coalesceChainId(recipientChain)
+  const recipientChainId = coalesceChainId(recipientChain);
   const nonce = Math.round(Math.random() * 100000);
   return [
     new MsgExecuteContract(
@@ -129,7 +147,7 @@ export async function transferFromTerra(
       tokenAddress,
       {
         approve: {
-          spender: tokenBridgeAddress,
+          spender: nftBridgeAddress,
           token_id: tokenID,
         },
       },
@@ -137,7 +155,7 @@ export async function transferFromTerra(
     ),
     new MsgExecuteContract(
       walletAddress,
-      tokenBridgeAddress,
+      nftBridgeAddress,
       {
         initiate_transfer: {
           contract_addr: tokenAddress,

+ 97 - 0
sdk/js/src/solana/anchor/common.ts

@@ -0,0 +1,97 @@
+// Borrowed from coral-xyz/anchor
+//
+// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/coder/common.ts
+
+import { Idl, IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "./idl";
+import { IdlError } from "./error";
+
+export function accountSize(idl: Idl, idlAccount: IdlTypeDef): number {
+  if (idlAccount.type.kind === "enum") {
+    let variantSizes = idlAccount.type.variants.map(
+      (variant: IdlEnumVariant) => {
+        if (variant.fields === undefined) {
+          return 0;
+        }
+        return variant.fields
+          .map((f: IdlField | IdlType) => {
+            if (!(typeof f === "object" && "name" in f)) {
+              throw new Error("Tuple enum variants not yet implemented.");
+            }
+            return typeSize(idl, f.type);
+          })
+          .reduce((a: number, b: number) => a + b);
+      }
+    );
+    return Math.max(...variantSizes) + 1;
+  }
+  if (idlAccount.type.fields === undefined) {
+    return 0;
+  }
+  return idlAccount.type.fields
+    .map((f) => typeSize(idl, f.type))
+    .reduce((a, b) => a + b, 0);
+}
+
+// Returns the size of the type in bytes. For variable length types, just return
+// 1. Users should override this value in such cases.
+function typeSize(idl: Idl, ty: IdlType): number {
+  switch (ty) {
+    case "bool":
+      return 1;
+    case "u8":
+      return 1;
+    case "i8":
+      return 1;
+    case "i16":
+      return 2;
+    case "u16":
+      return 2;
+    case "u32":
+      return 4;
+    case "i32":
+      return 4;
+    case "f32":
+      return 4;
+    case "u64":
+      return 8;
+    case "i64":
+      return 8;
+    case "f64":
+      return 8;
+    case "u128":
+      return 16;
+    case "i128":
+      return 16;
+    case "bytes":
+      return 1;
+    case "string":
+      return 1;
+    case "publicKey":
+      return 32;
+    default:
+      if ("vec" in ty) {
+        return 1;
+      }
+      if ("option" in ty) {
+        return 1 + typeSize(idl, ty.option);
+      }
+      if ("coption" in ty) {
+        return 4 + typeSize(idl, ty.coption);
+      }
+      if ("defined" in ty) {
+        const filtered = idl.types?.filter((t) => t.name === ty.defined) ?? [];
+        if (filtered.length !== 1) {
+          throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
+        }
+        let typeDef = filtered[0];
+
+        return accountSize(idl, typeDef);
+      }
+      if ("array" in ty) {
+        let arrayTy = ty.array[0];
+        let arraySize = ty.array[1];
+        return typeSize(idl, arrayTy) * arraySize;
+      }
+      throw new Error(`Invalid type ${JSON.stringify(ty)}`);
+  }
+}

+ 10 - 0
sdk/js/src/solana/anchor/error.ts

@@ -0,0 +1,10 @@
+// Borrowed from coral-xyz/anchor
+//
+// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/error.ts
+
+export class IdlError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "IdlError";
+  }
+}

+ 204 - 0
sdk/js/src/solana/anchor/idl.ts

@@ -0,0 +1,204 @@
+// Borrowed from coral-xyz/anchor
+//
+// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/idl.ts
+
+import { Buffer } from "buffer";
+import { PublicKey } from "@solana/web3.js";
+import * as borsh from "@project-serum/borsh";
+
+export type Idl = {
+  version: string;
+  name: string;
+  docs?: string[];
+  instructions: IdlInstruction[];
+  state?: IdlState;
+  accounts?: IdlAccountDef[];
+  types?: IdlTypeDef[];
+  events?: IdlEvent[];
+  errors?: IdlErrorCode[];
+  constants?: IdlConstant[];
+  metadata?: IdlMetadata;
+};
+
+export type IdlMetadata = any;
+
+export type IdlConstant = {
+  name: string;
+  type: IdlType;
+  value: string;
+};
+
+export type IdlEvent = {
+  name: string;
+  fields: IdlEventField[];
+};
+
+export type IdlEventField = {
+  name: string;
+  type: IdlType;
+  index: boolean;
+};
+
+export type IdlInstruction = {
+  name: string;
+  docs?: string[];
+  accounts: IdlAccountItem[];
+  args: IdlField[];
+  returns?: IdlType;
+};
+
+export type IdlState = {
+  struct: IdlTypeDef;
+  methods: IdlStateMethod[];
+};
+
+export type IdlStateMethod = IdlInstruction;
+
+export type IdlAccountItem = IdlAccount | IdlAccounts;
+
+export type IdlAccount = {
+  name: string;
+  isMut: boolean;
+  isSigner: boolean;
+  docs?: string[];
+  pda?: IdlPda;
+};
+
+export type IdlPda = {
+  seeds: IdlSeed[];
+  programId?: IdlSeed;
+};
+
+export type IdlSeed = any; // TODO
+
+// A nested/recursive version of IdlAccount.
+export type IdlAccounts = {
+  name: string;
+  docs?: string[];
+  accounts: IdlAccountItem[];
+};
+
+export type IdlField = {
+  name: string;
+  docs?: string[];
+  type: IdlType;
+};
+
+export type IdlTypeDef = {
+  name: string;
+  docs?: string[];
+  type: IdlTypeDefTy;
+};
+
+export type IdlAccountDef = {
+  name: string;
+  docs?: string[];
+  type: IdlTypeDefTyStruct;
+};
+
+export type IdlTypeDefTyStruct = {
+  kind: "struct";
+  fields: IdlTypeDefStruct;
+};
+
+export type IdlTypeDefTyEnum = {
+  kind: "enum";
+  variants: IdlEnumVariant[];
+};
+
+export type IdlTypeDefTy = IdlTypeDefTyEnum | IdlTypeDefTyStruct;
+
+export type IdlTypeDefStruct = Array<IdlField>;
+
+export type IdlType =
+  | "bool"
+  | "u8"
+  | "i8"
+  | "u16"
+  | "i16"
+  | "u32"
+  | "i32"
+  | "f32"
+  | "u64"
+  | "i64"
+  | "f64"
+  | "u128"
+  | "i128"
+  | "bytes"
+  | "string"
+  | "publicKey"
+  | IdlTypeDefined
+  | IdlTypeOption
+  | IdlTypeCOption
+  | IdlTypeVec
+  | IdlTypeArray;
+
+// User defined type.
+export type IdlTypeDefined = {
+  defined: string;
+};
+
+export type IdlTypeOption = {
+  option: IdlType;
+};
+
+export type IdlTypeCOption = {
+  coption: IdlType;
+};
+
+export type IdlTypeVec = {
+  vec: IdlType;
+};
+
+export type IdlTypeArray = {
+  array: [idlType: IdlType, size: number];
+};
+
+export type IdlEnumVariant = {
+  name: string;
+  fields?: IdlEnumFields;
+};
+
+export type IdlEnumFields = IdlEnumFieldsNamed | IdlEnumFieldsTuple;
+
+export type IdlEnumFieldsNamed = IdlField[];
+
+export type IdlEnumFieldsTuple = IdlType[];
+
+export type IdlErrorCode = {
+  code: number;
+  name: string;
+  msg?: string;
+};
+
+// Deterministic IDL address as a function of the program id.
+export async function idlAddress(programId: PublicKey): Promise<PublicKey> {
+  const base = (await PublicKey.findProgramAddress([], programId))[0];
+  return await PublicKey.createWithSeed(base, seed(), programId);
+}
+
+// Seed for generating the idlAddress.
+export function seed(): string {
+  return "anchor:idl";
+}
+
+// The on-chain account of the IDL.
+export interface IdlProgramAccount {
+  authority: PublicKey;
+  data: Buffer;
+}
+
+const IDL_ACCOUNT_LAYOUT: borsh.Layout<IdlProgramAccount> = borsh.struct([
+  borsh.publicKey("authority"),
+  borsh.vecU8("data"),
+]);
+
+export function decodeIdlAccount(data: Buffer): IdlProgramAccount {
+  return IDL_ACCOUNT_LAYOUT.decode(data);
+}
+
+export function encodeIdlAccount(acc: IdlProgramAccount): Buffer {
+  const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+  const len = IDL_ACCOUNT_LAYOUT.encode(acc, buffer);
+  return buffer.slice(0, len);
+}

+ 3 - 0
sdk/js/src/solana/anchor/index.ts

@@ -0,0 +1,3 @@
+export * from "./common";
+export * from "./error";
+export * from "./idl";

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

@@ -1,25 +0,0 @@
-import { Connection, PublicKey, SystemProgram } from "@solana/web3.js";
-import { importCoreWasm } from "./wasm";
-
-export async function getBridgeFeeIx(
-  connection: Connection,
-  bridgeAddress: string,
-  payerAddress: string
-) {
-  const bridge = await importCoreWasm();
-  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;
-}

+ 12 - 6
sdk/js/src/solana/index.ts

@@ -1,9 +1,15 @@
-export * from "./getBridgeFeeIx";
+export * from "./utils";
+
 export {
-  createPostVaaInstruction as createPostVaaInstructionSolana,
-  createVerifySignaturesInstructions as createVerifySignaturesInstructionsSolana,
   postVaa as postVaaSolana,
   postVaaWithRetry as postVaaSolanaWithRetry,
-} from "./postVaa";
-export * from "./rust";
-export * from "./wasm";
+} from "./sendAndConfirmPostVaa";
+export {
+  createVerifySignaturesInstructions as createVerifySignaturesInstructionsSolana,
+  createPostVaaInstruction as createPostVaaInstructionSolana,
+  createBridgeFeeTransferInstruction,
+  getPostMessageAccounts as getWormholeCpiAccounts,
+} from "./wormhole";
+
+export * from "./wormhole/cpi";
+export * from "./tokenBridge/cpi";

+ 18 - 0
sdk/js/src/solana/nftBridge/accounts/config.ts

@@ -0,0 +1,18 @@
+import { Connection, Commitment, PublicKeyInitData } from "@solana/web3.js";
+import {
+  deriveTokenBridgeConfigKey,
+  getTokenBridgeConfig,
+  TokenBridgeConfig,
+} from "../../tokenBridge";
+
+export const deriveNftBridgeConfigKey = deriveTokenBridgeConfigKey;
+
+export async function getNftBridgeConfig(
+  connection: Connection,
+  nftBridgeProgramId: PublicKeyInitData,
+  commitment?: Commitment
+): Promise<NftBridgeConfig> {
+  return getTokenBridgeConfig(connection, nftBridgeProgramId, commitment);
+}
+
+export class NftBridgeConfig extends TokenBridgeConfig {}

+ 13 - 0
sdk/js/src/solana/nftBridge/accounts/index.ts

@@ -0,0 +1,13 @@
+export * from "./config";
+export * from "./wrapped";
+
+export {
+  EndpointRegistration,
+  deriveAuthoritySignerKey,
+  deriveCustodyKey,
+  deriveCustodySignerKey,
+  deriveEndpointKey,
+  deriveMintAuthorityKey,
+  deriveUpgradeAuthorityKey,
+  getEndpointRegistration,
+} from "../../tokenBridge";

+ 83 - 0
sdk/js/src/solana/nftBridge/accounts/wrapped.ts

@@ -0,0 +1,83 @@
+import { BN } from "@project-serum/anchor";
+import {
+  Connection,
+  PublicKey,
+  Commitment,
+  PublicKeyInitData,
+} from "@solana/web3.js";
+import {
+  ChainId,
+  CHAIN_ID_SOLANA,
+  tryNativeToUint8Array,
+} from "../../../utils";
+import { deriveAddress, getAccountData } from "../../utils";
+import { deriveWrappedMetaKey } from "../../tokenBridge";
+
+export { deriveWrappedMetaKey } from "../../tokenBridge";
+
+export function deriveWrappedMintKey(
+  tokenBridgeProgramId: PublicKeyInitData,
+  tokenChain: number | ChainId,
+  tokenAddress: Buffer | Uint8Array | string,
+  tokenId: bigint | number
+): PublicKey {
+  if (tokenChain == CHAIN_ID_SOLANA) {
+    throw new Error(
+      "tokenChain == CHAIN_ID_SOLANA does not have wrapped mint key"
+    );
+  }
+  if (typeof tokenAddress == "string") {
+    tokenAddress = tryNativeToUint8Array(tokenAddress, tokenChain as ChainId);
+  }
+  return deriveAddress(
+    [
+      Buffer.from("wrapped"),
+      (() => {
+        const buf = Buffer.alloc(2);
+        buf.writeUInt16BE(tokenChain as number);
+        return buf;
+      })(),
+      tokenAddress,
+      new BN(tokenId.toString()).toBuffer("be", 32),
+    ],
+    tokenBridgeProgramId
+  );
+}
+
+export async function getWrappedMeta(
+  connection: Connection,
+  tokenBridgeProgramId: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  commitment?: Commitment
+): Promise<WrappedMeta> {
+  return connection
+    .getAccountInfo(
+      deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+      commitment
+    )
+    .then((info) => WrappedMeta.deserialize(getAccountData(info)));
+}
+
+export class WrappedMeta {
+  chain: number;
+  tokenAddress: Buffer;
+  tokenId: bigint;
+
+  constructor(chain: number, tokenAddress: Buffer, tokenId: bigint) {
+    this.chain = chain;
+    this.tokenAddress = tokenAddress;
+    this.tokenId = tokenId;
+  }
+
+  static deserialize(data: Buffer): WrappedMeta {
+    if (data.length != 66) {
+      throw new Error("data.length != 66");
+    }
+    const chain = data.readUInt16LE(0);
+    const tokenAddress = data.subarray(2, 34);
+    const tokenId = BigInt(
+      new BN(data.subarray(34, 66), undefined, "le").toString()
+    );
+    return new WrappedMeta(chain, tokenAddress, tokenId);
+  }
+}

+ 40 - 0
sdk/js/src/solana/nftBridge/coder/accounts.ts

@@ -0,0 +1,40 @@
+import { AccountsCoder, Idl } from "@project-serum/anchor";
+import { accountSize, IdlTypeDef } from "../../anchor";
+
+export class NftBridgeAccountsCoder<A extends string = string>
+  implements AccountsCoder
+{
+  constructor(private idl: Idl) {}
+
+  public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
+    switch (accountName) {
+      default: {
+        throw new Error(`Invalid account name: ${accountName}`);
+      }
+    }
+  }
+
+  public decode<T = any>(accountName: A, ix: Buffer): T {
+    return this.decodeUnchecked(accountName, ix);
+  }
+
+  public decodeUnchecked<T = any>(accountName: A, ix: Buffer): T {
+    switch (accountName) {
+      default: {
+        throw new Error(`Invalid account name: ${accountName}`);
+      }
+    }
+  }
+
+  public memcmp(accountName: A, _appendData?: Buffer): any {
+    switch (accountName) {
+      default: {
+        throw new Error(`Invalid account name: ${accountName}`);
+      }
+    }
+  }
+
+  public size(idlAccount: IdlTypeDef): number {
+    return accountSize(this.idl, idlAccount) ?? 0;
+  }
+}

+ 12 - 0
sdk/js/src/solana/nftBridge/coder/events.ts

@@ -0,0 +1,12 @@
+import { EventCoder, Event, Idl } from "@project-serum/anchor";
+import { IdlEvent } from "../../anchor";
+
+export class NftBridgeEventsCoder implements EventCoder {
+  constructor(_idl: Idl) {}
+
+  decode<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
+    _log: string
+  ): Event<E, T> | null {
+    throw new Error("NFT Bridge program does not have events");
+  }
+}

+ 24 - 0
sdk/js/src/solana/nftBridge/coder/index.ts

@@ -0,0 +1,24 @@
+import { Coder, Idl } from "@project-serum/anchor";
+import { NftBridgeAccountsCoder } from "./accounts";
+import { NftBridgeEventsCoder } from "./events";
+import { NftBridgeInstructionCoder } from "./instruction";
+import { NftBridgeStateCoder } from "./state";
+import { NftBridgeTypesCoder } from "./types";
+
+export { NftBridgeInstruction } from "./instruction";
+
+export class NftBridgeCoder implements Coder {
+  readonly instruction: NftBridgeInstructionCoder;
+  readonly accounts: NftBridgeAccountsCoder;
+  readonly state: NftBridgeStateCoder;
+  readonly events: NftBridgeEventsCoder;
+  readonly types: NftBridgeTypesCoder;
+
+  constructor(idl: Idl) {
+    this.instruction = new NftBridgeInstructionCoder(idl);
+    this.accounts = new NftBridgeAccountsCoder(idl);
+    this.state = new NftBridgeStateCoder(idl);
+    this.events = new NftBridgeEventsCoder(idl);
+    this.types = new NftBridgeTypesCoder(idl);
+  }
+}

+ 130 - 0
sdk/js/src/solana/nftBridge/coder/instruction.ts

@@ -0,0 +1,130 @@
+import { Idl, InstructionCoder } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+
+export class NftBridgeInstructionCoder implements InstructionCoder {
+  constructor(_: Idl) {}
+
+  encode(ixName: string, ix: any): Buffer {
+    switch (ixName) {
+      case "initialize": {
+        return encodeInitialize(ix);
+      }
+      case "completeNative": {
+        return encodeCompleteNative(ix);
+      }
+      case "completeWrapped": {
+        return encodeCompleteWrapped(ix);
+      }
+      case "completeWrappedMeta": {
+        return encodeCompleteWrappedMeta(ix);
+      }
+      case "transferWrapped": {
+        return encodeTransferWrapped(ix);
+      }
+      case "transferNative": {
+        return encodeTransferNative(ix);
+      }
+      case "registerChain": {
+        return encodeRegisterChain(ix);
+      }
+      case "upgradeContract": {
+        return encodeUpgradeContract(ix);
+      }
+      default: {
+        throw new Error(`Invalid instruction: ${ixName}`);
+      }
+    }
+  }
+
+  encodeState(_ixName: string, _ix: any): Buffer {
+    throw new Error("NFT Bridge program does not have state");
+  }
+}
+
+/** Solitaire enum of existing the NFT Bridge's instructions.
+ *
+ * https://github.com/certusone/wormhole/blob/dev.v2/solana/modules/nft_bridge/program/src/lib.rs#L74
+ */
+export enum NftBridgeInstruction {
+  Initialize,
+  CompleteNative,
+  CompleteWrapped,
+  CompleteWrappedMeta,
+  TransferWrapped,
+  TransferNative,
+  RegisterChain,
+  UpgradeContract,
+}
+
+function encodeNftBridgeInstructionData(
+  instructionType: NftBridgeInstruction,
+  data?: Buffer
+): Buffer {
+  const dataLen = data === undefined ? 0 : data.length;
+  const instructionData = Buffer.alloc(1 + dataLen);
+  instructionData.writeUInt8(instructionType, 0);
+  if (dataLen > 0) {
+    instructionData.write(data!.toString("hex"), 1, "hex");
+  }
+  return instructionData;
+}
+
+function encodeInitialize({ wormhole }: any): Buffer {
+  const serialized = Buffer.alloc(32);
+  serialized.write(
+    new PublicKey(wormhole).toBuffer().toString("hex"),
+    0,
+    "hex"
+  );
+  return encodeNftBridgeInstructionData(
+    NftBridgeInstruction.Initialize,
+    serialized
+  );
+}
+
+function encodeCompleteNative({}: any) {
+  return encodeNftBridgeInstructionData(NftBridgeInstruction.CompleteNative);
+}
+
+function encodeCompleteWrapped({}: any) {
+  return encodeNftBridgeInstructionData(NftBridgeInstruction.CompleteWrapped);
+}
+
+function encodeCompleteWrappedMeta({}: any) {
+  return encodeNftBridgeInstructionData(
+    NftBridgeInstruction.CompleteWrappedMeta
+  );
+}
+
+function encodeTransferData({ nonce, targetAddress, targetChain }: any) {
+  if (!Buffer.isBuffer(targetAddress)) {
+    throw new Error("targetAddress must be Buffer");
+  }
+  const serialized = Buffer.alloc(38);
+  serialized.writeUInt32LE(nonce, 0);
+  serialized.write(targetAddress.toString("hex"), 4, "hex");
+  serialized.writeUInt16LE(targetChain, 36);
+  return serialized;
+}
+
+function encodeTransferWrapped({ nonce, targetAddress, targetChain }: any) {
+  return encodeNftBridgeInstructionData(
+    NftBridgeInstruction.TransferWrapped,
+    encodeTransferData({ nonce, targetAddress, targetChain })
+  );
+}
+
+function encodeTransferNative({ nonce, targetAddress, targetChain }: any) {
+  return encodeNftBridgeInstructionData(
+    NftBridgeInstruction.TransferNative,
+    encodeTransferData({ nonce, targetAddress, targetChain })
+  );
+}
+
+function encodeRegisterChain({}: any) {
+  return encodeNftBridgeInstructionData(NftBridgeInstruction.RegisterChain);
+}
+
+function encodeUpgradeContract({}: any) {
+  return encodeNftBridgeInstructionData(NftBridgeInstruction.UpgradeContract);
+}

+ 12 - 0
sdk/js/src/solana/nftBridge/coder/state.ts

@@ -0,0 +1,12 @@
+import { Idl, StateCoder } from "@project-serum/anchor";
+
+export class NftBridgeStateCoder implements StateCoder {
+  constructor(_idl: Idl) {}
+
+  encode<T = any>(_name: string, _account: T): Promise<Buffer> {
+    throw new Error("NFT Bridge program does not have state");
+  }
+  decode<T = any>(_ix: Buffer): T {
+    throw new Error("NFT Bridge program does not have state");
+  }
+}

+ 12 - 0
sdk/js/src/solana/nftBridge/coder/types.ts

@@ -0,0 +1,12 @@
+import { Idl, TypesCoder } from "@project-serum/anchor";
+
+export class NftBridgeTypesCoder implements TypesCoder {
+  constructor(_idl: Idl) {}
+
+  encode<T = any>(_name: string, _type: T): Buffer {
+    throw new Error("NFT Bridge program does not have user-defined types");
+  }
+  decode<T = any>(_name: string, _typeData: Buffer): T {
+    throw new Error("NFT Bridge program does not have user-defined types");
+  }
+}

+ 3 - 0
sdk/js/src/solana/nftBridge/index.ts

@@ -0,0 +1,3 @@
+export * from "./accounts";
+export * from "./instructions";
+export * from "./program";

+ 15 - 0
sdk/js/src/solana/nftBridge/instructions/approve.ts

@@ -0,0 +1,15 @@
+import { PublicKeyInitData } from "@solana/web3.js";
+import { createApproveAuthoritySignerInstruction as _createApproveAuthoritySignerInstruction } from "../../tokenBridge";
+
+export function createApproveAuthoritySignerInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  tokenAccount: PublicKeyInitData,
+  owner: PublicKeyInitData
+) {
+  return _createApproveAuthoritySignerInstruction(
+    nftBridgeProgramId,
+    tokenAccount,
+    owner,
+    1
+  );
+}

+ 107 - 0
sdk/js/src/solana/nftBridge/instructions/completeNative.ts

@@ -0,0 +1,107 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import {
+  createReadOnlyNftBridgeProgramInterface,
+  tokenIdToMint,
+} from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveNftBridgeConfigKey,
+  deriveCustodyKey,
+  deriveCustodySignerKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedNftTransferVaa,
+  parseNftTransferVaa,
+  SignedVaa,
+} from "../../../vaa";
+
+export function createCompleteTransferNativeInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftTransferVaa,
+  toAuthority?: PublicKeyInitData
+): TransactionInstruction {
+  const methods =
+    createReadOnlyNftBridgeProgramInterface(
+      nftBridgeProgramId
+    ).methods.completeNative();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getCompleteTransferNativeAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa,
+      toAuthority
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface CompleteTransferNativeAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  endpoint: PublicKey;
+  to: PublicKey;
+  toAuthority: PublicKey;
+  custody: PublicKey;
+  mint: PublicKey;
+  custodySigner: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getCompleteTransferNativeAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftTransferVaa,
+  toAuthority?: PublicKeyInitData
+): CompleteTransferNativeAccounts {
+  const parsed = isBytes(vaa) ? parseNftTransferVaa(vaa) : vaa;
+  // the mint key is encoded in the tokenId when it was transferred out
+  const mint = tokenIdToMint(parsed.tokenId);
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      nftBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    endpoint: deriveEndpointKey(
+      nftBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    to: new PublicKey(parsed.to),
+    toAuthority: new PublicKey(toAuthority === undefined ? payer : toAuthority),
+    custody: deriveCustodyKey(nftBridgeProgramId, mint),
+    mint,
+    custodySigner: deriveCustodySignerKey(nftBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 117 - 0
sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts

@@ -0,0 +1,117 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveNftBridgeConfigKey,
+  deriveWrappedMintKey,
+  deriveWrappedMetaKey,
+  deriveMintAuthorityKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedNftTransferVaa,
+  parseNftTransferVaa,
+  SignedVaa,
+} from "../../../vaa";
+import { SplTokenMetadataProgram } from "../../utils";
+
+export function createCompleteTransferWrappedInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftTransferVaa,
+  toAuthority?: PublicKeyInitData
+): TransactionInstruction {
+  const methods =
+    createReadOnlyNftBridgeProgramInterface(
+      nftBridgeProgramId
+    ).methods.completeWrapped();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getCompleteTransferWrappedAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa,
+      toAuthority
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface CompleteTransferWrappedAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  endpoint: PublicKey;
+  to: PublicKey;
+  toAuthority: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  mintAuthority: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  splMetadataProgram: PublicKey;
+  associatedTokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getCompleteTransferWrappedAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftTransferVaa,
+  toAuthority?: PublicKeyInitData
+): CompleteTransferWrappedAccounts {
+  const parsed = isBytes(vaa) ? parseNftTransferVaa(vaa) : vaa;
+  const mint = deriveWrappedMintKey(
+    nftBridgeProgramId,
+    parsed.tokenChain,
+    parsed.tokenAddress,
+    parsed.tokenId
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      nftBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    endpoint: deriveEndpointKey(
+      nftBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    to: new PublicKey(parsed.to),
+    toAuthority: new PublicKey(toAuthority === undefined ? payer : toAuthority),
+    mint,
+    wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
+    mintAuthority: deriveMintAuthorityKey(nftBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    splMetadataProgram: SplTokenMetadataProgram.programId,
+    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 103 - 0
sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts

@@ -0,0 +1,103 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveNftBridgeConfigKey,
+  deriveWrappedMintKey,
+  deriveWrappedMetaKey,
+  deriveMintAuthorityKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedNftTransferVaa,
+  parseNftTransferVaa,
+  SignedVaa,
+} from "../../../vaa";
+import {
+  deriveSplTokenMetadataKey,
+  SplTokenMetadataProgram,
+} from "../../utils";
+
+export function createCompleteWrappedMetaInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftTransferVaa
+): TransactionInstruction {
+  const methods =
+    createReadOnlyNftBridgeProgramInterface(
+      nftBridgeProgramId
+    ).methods.completeWrappedMeta();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getCompleteWrappedMetaAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface CompleteWrappedMetaAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  vaa: PublicKey;
+  endpoint: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  splMetadata: PublicKey;
+  mintAuthority: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  splMetadataProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getCompleteWrappedMetaAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftTransferVaa
+): CompleteWrappedMetaAccounts {
+  const parsed = isBytes(vaa) ? parseNftTransferVaa(vaa) : vaa;
+  const mint = deriveWrappedMintKey(
+    nftBridgeProgramId,
+    parsed.tokenChain,
+    parsed.tokenAddress,
+    parsed.tokenId
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    endpoint: deriveEndpointKey(
+      nftBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    mint,
+    wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
+    splMetadata: deriveSplTokenMetadataKey(mint),
+    mintAuthority: deriveMintAuthorityKey(nftBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    splMetadataProgram: SplTokenMetadataProgram.programId,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 161 - 0
sdk/js/src/solana/nftBridge/instructions/governance.ts

@@ -0,0 +1,161 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_CLOCK_PUBKEY,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveNftBridgeConfigKey,
+  deriveUpgradeAuthorityKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedNftBridgeRegisterChainVaa,
+  ParsedNftBridgeUpgradeContractVaa,
+  parseNftBridgeRegisterChainVaa,
+  parseNftBridgeUpgradeContractVaa,
+  SignedVaa,
+} from "../../../vaa";
+import { BpfLoaderUpgradeable, deriveUpgradeableProgramKey } from "../../utils";
+
+export function createRegisterChainInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftBridgeRegisterChainVaa
+): TransactionInstruction {
+  const methods =
+    createReadOnlyNftBridgeProgramInterface(
+      nftBridgeProgramId
+    ).methods.registerChain();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getRegisterChainAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface RegisterChainAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  endpoint: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getRegisterChainAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftBridgeRegisterChainVaa
+): RegisterChainAccounts {
+  const parsed = isBytes(vaa) ? parseNftBridgeRegisterChainVaa(vaa) : vaa;
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    endpoint: deriveEndpointKey(
+      nftBridgeProgramId,
+      parsed.foreignChain,
+      parsed.foreignAddress
+    ),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      nftBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}
+
+export function createUpgradeContractInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftBridgeUpgradeContractVaa,
+  spill?: PublicKeyInitData
+): TransactionInstruction {
+  const methods =
+    createReadOnlyNftBridgeProgramInterface(
+      nftBridgeProgramId
+    ).methods.upgradeContract();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getUpgradeContractAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa,
+      spill
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface UpgradeContractAccounts {
+  payer: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  upgradeAuthority: PublicKey;
+  spill: PublicKey;
+  implementation: PublicKey;
+  programData: PublicKey;
+  nftBridgeProgram: PublicKey;
+  rent: PublicKey;
+  clock: PublicKey;
+  bpfLoaderUpgradeable: PublicKey;
+  systemProgram: PublicKey;
+}
+
+export function getUpgradeContractAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedNftBridgeUpgradeContractVaa,
+  spill?: PublicKeyInitData
+): UpgradeContractAccounts {
+  const parsed = isBytes(vaa) ? parseNftBridgeUpgradeContractVaa(vaa) : vaa;
+  return {
+    payer: new PublicKey(payer),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      nftBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    upgradeAuthority: deriveUpgradeAuthorityKey(nftBridgeProgramId),
+    spill: new PublicKey(spill === undefined ? payer : spill),
+    implementation: new PublicKey(parsed.newContract),
+    programData: deriveUpgradeableProgramKey(nftBridgeProgramId),
+    nftBridgeProgram: new PublicKey(nftBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    clock: SYSVAR_CLOCK_PUBKEY,
+    bpfLoaderUpgradeable: BpfLoaderUpgradeable.programId,
+    systemProgram: SystemProgram.programId,
+  };
+}

+ 8 - 0
sdk/js/src/solana/nftBridge/instructions/index.ts

@@ -0,0 +1,8 @@
+export * from "./approve";
+export * from "./completeNative";
+export * from "./completeWrapped";
+export * from "./completeWrappedMeta";
+export * from "./initialize";
+export * from "./governance";
+export * from "./transferNative";
+export * from "./transferWrapped";

+ 47 - 0
sdk/js/src/solana/nftBridge/instructions/initialize.ts

@@ -0,0 +1,47 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { deriveNftBridgeConfigKey } from "../accounts";
+
+export function createInitializeInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData
+): TransactionInstruction {
+  const methods = createReadOnlyNftBridgeProgramInterface(
+    nftBridgeProgramId
+  ).methods.initialize(wormholeProgramId as any);
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getInitializeAccounts(nftBridgeProgramId, payer) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface InitializeAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+}
+
+export function getInitializeAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData
+): InitializeAccounts {
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+  };
+}

+ 122 - 0
sdk/js/src/solana/nftBridge/instructions/transferNative.ts

@@ -0,0 +1,122 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { getPostMessageAccounts } from "../../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveCustodySignerKey,
+  deriveNftBridgeConfigKey,
+  deriveCustodyKey,
+} from "../accounts";
+import {
+  deriveSplTokenMetadataKey,
+  SplTokenMetadataProgram,
+} from "../../utils";
+
+export function createTransferNativeInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  nonce: number,
+  targetAddress: Buffer | Uint8Array,
+  targetChain: number
+): TransactionInstruction {
+  const methods = createReadOnlyNftBridgeProgramInterface(
+    nftBridgeProgramId
+  ).methods.transferNative(
+    nonce,
+    Buffer.from(targetAddress) as any,
+    targetChain
+  );
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getTransferNativeAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      message,
+      from,
+      mint
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface TransferNativeAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  from: PublicKey;
+  mint: PublicKey;
+  splMetadata: PublicKey;
+  custody: PublicKey;
+  authoritySigner: PublicKey;
+  custodySigner: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  splMetadataProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getTransferNativeAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  mint: PublicKeyInitData
+): TransferNativeAccounts {
+  const {
+    bridge: wormholeBridge,
+    message: wormholeMessage,
+    emitter: wormholeEmitter,
+    sequence: wormholeSequence,
+    feeCollector: wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageAccounts(
+    wormholeProgramId,
+    payer,
+    nftBridgeProgramId,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    from: new PublicKey(from),
+    mint: new PublicKey(mint),
+    splMetadata: deriveSplTokenMetadataKey(mint),
+    custody: deriveCustodyKey(nftBridgeProgramId, mint),
+    authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId),
+    custodySigner: deriveCustodySignerKey(nftBridgeProgramId),
+    wormholeBridge,
+    wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    splMetadataProgram: SplTokenMetadataProgram.programId,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 137 - 0
sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts

@@ -0,0 +1,137 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { getPostMessageAccounts } from "../../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveNftBridgeConfigKey,
+  deriveWrappedMetaKey,
+  deriveWrappedMintKey,
+} from "../accounts";
+import {
+  deriveSplTokenMetadataKey,
+  SplTokenMetadataProgram,
+} from "../../utils";
+
+export function createTransferWrappedInstruction(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  fromOwner: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array,
+  tokenId: bigint | number,
+  nonce: number,
+  targetAddress: Buffer | Uint8Array,
+  targetChain: number
+): TransactionInstruction {
+  const methods = createReadOnlyNftBridgeProgramInterface(
+    nftBridgeProgramId
+  ).methods.transferWrapped(
+    nonce,
+    Buffer.from(targetAddress) as any,
+    targetChain
+  );
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getTransferWrappedAccounts(
+      nftBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      message,
+      from,
+      fromOwner,
+      tokenChain,
+      tokenAddress,
+      tokenId
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface TransferWrappedAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  from: PublicKey;
+  fromOwner: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  splMetadata: PublicKey;
+  authoritySigner: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  splMetadataProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getTransferWrappedAccounts(
+  nftBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  fromOwner: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array,
+  tokenId: bigint | number
+): TransferWrappedAccounts {
+  const mint = deriveWrappedMintKey(
+    nftBridgeProgramId,
+    tokenChain,
+    tokenAddress,
+    tokenId
+  );
+  const {
+    bridge: wormholeBridge,
+    message: wormholeMessage,
+    emitter: wormholeEmitter,
+    sequence: wormholeSequence,
+    feeCollector: wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageAccounts(
+    wormholeProgramId,
+    payer,
+    nftBridgeProgramId,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveNftBridgeConfigKey(nftBridgeProgramId),
+    from: new PublicKey(from),
+    fromOwner: new PublicKey(fromOwner),
+    mint,
+    wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
+    splMetadata: deriveSplTokenMetadataKey(mint),
+    authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId),
+    wormholeBridge,
+    wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    splMetadataProgram: SplTokenMetadataProgram.programId,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 43 - 0
sdk/js/src/solana/nftBridge/program.ts

@@ -0,0 +1,43 @@
+import { Connection, PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { BN, Program, Provider } from "@project-serum/anchor";
+import { createReadOnlyProvider } from "../utils";
+import { NftBridgeCoder } from "./coder";
+import { NftBridge } from "../types/nftBridge";
+
+import IDL from "../../anchor-idl/nft_bridge.json";
+
+export const NFT_TRANSFER_NATIVE_TOKEN_ADDRESS = Buffer.alloc(32, 1);
+
+export function createNftBridgeProgramInterface(
+  programId: PublicKeyInitData,
+  provider?: Provider
+): Program<NftBridge> {
+  return new Program<NftBridge>(
+    IDL as NftBridge,
+    new PublicKey(programId),
+    provider === undefined ? ({ connection: null } as any) : provider,
+    coder()
+  );
+}
+
+export function createReadOnlyNftBridgeProgramInterface(
+  programId: PublicKeyInitData,
+  connection?: Connection
+): Program<NftBridge> {
+  return createNftBridgeProgramInterface(
+    programId,
+    createReadOnlyProvider(connection)
+  );
+}
+
+export function coder(): NftBridgeCoder {
+  return new NftBridgeCoder(IDL as NftBridge);
+}
+
+export function tokenIdToMint(tokenId: bigint) {
+  return new PublicKey(new BN(tokenId.toString()).toBuffer());
+}
+
+export function mintToTokenId(mint: PublicKeyInitData) {
+  return BigInt(new BN(new PublicKey(mint).toBuffer()).toString());
+}

+ 0 - 212
sdk/js/src/solana/postVaa.ts

@@ -1,212 +0,0 @@
-import {
-  Connection,
-  Keypair,
-  PublicKey,
-  Transaction,
-  TransactionInstruction,
-} from "@solana/web3.js";
-import { chunks } from "..";
-import { sendAndConfirmTransactionsWithRetry } from "../utils/solana";
-import { ixFromRust } from "./rust";
-import { importCoreWasm } from "./wasm";
-
-export async function postVaaWithRetry(
-  connection: Connection,
-  signTransaction: (transaction: Transaction) => Promise<Transaction>,
-  bridge_id: string,
-  payer: string,
-  vaa: Buffer,
-  maxRetries: number
-) {
-  const unsignedTransactions: Transaction[] = [];
-  const signature_set = Keypair.generate();
-  const instructions = await createVerifySignaturesInstructions(
-    connection,
-    bridge_id,
-    payer,
-    vaa,
-    signature_set
-  );
-  const finalInstruction = await createPostVaaInstruction(
-    bridge_id,
-    payer,
-    vaa,
-    signature_set
-  );
-  if (!finalInstruction) {
-    return Promise.reject("Failed to construct the transaction.");
-  }
-
-  //The verify signatures instructions can be batched into groups of 2 safely,
-  //reducing the total number of transactions.
-  const batchableChunks = chunks(instructions, 2);
-  batchableChunks.forEach((chunk) => {
-    let transaction;
-    if (chunk.length === 1) {
-      transaction = new Transaction().add(chunk[0]);
-    } else {
-      transaction = new Transaction().add(chunk[0], chunk[1]);
-    }
-    unsignedTransactions.push(transaction);
-  });
-
-  //the postVaa instruction can only execute after the verifySignature transactions have
-  //successfully completed.
-  const finalTransaction = new Transaction().add(finalInstruction);
-
-  //The signature_set keypair also needs to sign the verifySignature transactions, thus a wrapper is needed.
-  const partialSignWrapper = (transaction: Transaction) => {
-    transaction.partialSign(signature_set);
-    return signTransaction(transaction);
-  };
-
-  await sendAndConfirmTransactionsWithRetry(
-    connection,
-    partialSignWrapper,
-    payer,
-    unsignedTransactions,
-    maxRetries
-  );
-  //While the signature_set is used to create the final instruction, it doesn't need to sign it.
-  await sendAndConfirmTransactionsWithRetry(
-    connection,
-    signTransaction,
-    payer,
-    [finalTransaction],
-    maxRetries
-  );
-
-  return Promise.resolve();
-}
-
-/*
-This returns an array of instructions required to verify the signatures of a VAA, and upload it to the blockchain.
-signature_set should be a new keypair, and also needs to partial sign the transaction when these instructions are submitted.
-*/
-export async function createVerifySignaturesInstructions(
-  connection: Connection,
-  bridge_id: string,
-  payer: string,
-  vaa: Buffer,
-  signature_set: Keypair
-): Promise<TransactionInstruction[]> {
-  const output: TransactionInstruction[] = [];
-  const {
-    guardian_set_address,
-    parse_guardian_set,
-    parse_vaa,
-    verify_signatures_ix,
-  } = await importCoreWasm();
-  const { guardian_set_index } = parse_vaa(new Uint8Array(vaa));
-  let guardian_addr = new PublicKey(
-    guardian_set_address(bridge_id, guardian_set_index)
-  );
-  let acc = await connection.getAccountInfo(guardian_addr);
-  if (acc?.data === undefined) {
-    return output;
-  }
-  let guardian_data = parse_guardian_set(new Uint8Array(acc?.data));
-
-  let txs = verify_signatures_ix(
-    bridge_id,
-    payer,
-    guardian_set_index,
-    guardian_data,
-    signature_set.publicKey.toString(),
-    vaa
-  );
-  // Add transfer instruction to transaction
-  for (let tx of txs) {
-    let ixs: Array<TransactionInstruction> = tx.map((v: any) => {
-      return ixFromRust(v);
-    });
-    output.push(ixs[0], ixs[1]);
-  }
-  return output;
-}
-
-/*
-This will return the postVaaInstruction. This should only be executed after the verifySignaturesInstructions have been executed.
-signatureSetKeypair should be the same keypair used for verifySignaturesInstructions, but does not need to partialSign the transaction
-when this instruction is submitted.
-*/
-export async function createPostVaaInstruction(
-  bridge_id: string,
-  payer: string,
-  vaa: Buffer,
-  signatureSetKeypair: Keypair
-): Promise<TransactionInstruction> {
-  const { post_vaa_ix } = await importCoreWasm();
-  return ixFromRust(
-    post_vaa_ix(bridge_id, payer, signatureSetKeypair.publicKey.toString(), vaa)
-  );
-}
-
-/*
-  @deprecated
-  Instead, either use postVaaWithRetry or create, sign, and send the verifySignaturesInstructions & postVaaInstruction yourself.
-  
-  This function is equivalent to a postVaaWithRetry with a maxRetries of 0.
-*/
-export async function postVaa(
-  connection: Connection,
-  signTransaction: (transaction: Transaction) => Promise<Transaction>,
-  bridge_id: string,
-  payer: string,
-  vaa: Buffer
-) {
-  const {
-    guardian_set_address,
-    parse_guardian_set,
-    parse_vaa,
-    post_vaa_ix,
-    verify_signatures_ix,
-  } = await importCoreWasm();
-  const { guardian_set_index } = parse_vaa(new Uint8Array(vaa));
-  let guardian_addr = new PublicKey(
-    guardian_set_address(bridge_id, guardian_set_index)
-  );
-  let acc = await connection.getAccountInfo(guardian_addr);
-  if (acc?.data === undefined) {
-    return;
-  }
-  let guardian_data = parse_guardian_set(new Uint8Array(acc?.data));
-
-  let signature_set = Keypair.generate();
-  let txs = verify_signatures_ix(
-    bridge_id,
-    payer,
-    guardian_set_index,
-    guardian_data,
-    signature_set.publicKey.toString(),
-    vaa
-  );
-  // Add transfer instruction to transaction
-  for (let tx of txs) {
-    let ixs: Array<TransactionInstruction> = tx.map((v: any) => {
-      return ixFromRust(v);
-    });
-    let transaction = new Transaction().add(...ixs);
-    const { blockhash } = await connection.getRecentBlockhash();
-    transaction.recentBlockhash = blockhash;
-    transaction.feePayer = new PublicKey(payer);
-    transaction.partialSign(signature_set);
-
-    // Sign transaction, broadcast, and confirm
-    const signed = await signTransaction(transaction);
-    const txid = await connection.sendRawTransaction(signed.serialize());
-    await connection.confirmTransaction(txid);
-  }
-
-  let ix = ixFromRust(
-    post_vaa_ix(bridge_id, payer, signature_set.publicKey.toString(), vaa)
-  );
-  let transaction = new Transaction().add(ix);
-  const { blockhash } = await connection.getRecentBlockhash();
-  transaction.recentBlockhash = blockhash;
-  transaction.feePayer = new PublicKey(payer);
-
-  const signed = await signTransaction(transaction);
-  const txid = await connection.sendRawTransaction(signed.serialize());
-  await connection.confirmTransaction(txid);
-}

+ 0 - 23
sdk/js/src/solana/rust.ts

@@ -1,23 +0,0 @@
-import {
-  AccountMeta,
-  PublicKey,
-  TransactionInstruction,
-} from "@solana/web3.js";
-// begin from clients\solana\main.ts
-export function ixFromRust(data: any): TransactionInstruction {
-  const keys: AccountMeta[] = data.accounts.map(accountMetaFromRust);
-  return new TransactionInstruction({
-    programId: new PublicKey(data.program_id),
-    data: Buffer.from(data.data),
-    keys,
-  });
-}
-
-function accountMetaFromRust(meta: any): AccountMeta {
-  return {
-    pubkey: new PublicKey(meta.pubkey),
-    isSigner: meta.is_signer,
-    isWritable: meta.is_writable,
-  };
-}
-// end from clients\solana\main.ts

+ 171 - 0
sdk/js/src/solana/sendAndConfirmPostVaa.ts

@@ -0,0 +1,171 @@
+import {
+  Commitment,
+  ConfirmOptions,
+  Connection,
+  Keypair,
+  PublicKeyInitData,
+  Transaction,
+} from "@solana/web3.js";
+import {
+  signSendAndConfirmTransaction,
+  SignTransaction,
+  sendAndConfirmTransactionsWithRetry,
+  modifySignTransaction,
+  TransactionSignatureAndResponse,
+  PreparedTransactions,
+} from "./utils";
+import {
+  createPostVaaInstruction,
+  createVerifySignaturesInstructions,
+} from "./wormhole";
+import { isBytes, ParsedVaa, parseVaa, SignedVaa } from "../vaa/wormhole";
+
+export async function postVaaWithRetry(
+  connection: Connection,
+  signTransaction: SignTransaction,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: Buffer,
+  maxRetries?: number,
+  commitment?: Commitment
+): Promise<TransactionSignatureAndResponse[]> {
+  const { unsignedTransactions, signers } =
+    await createPostSignedVaaTransactions(
+      connection,
+      wormholeProgramId,
+      payer,
+      vaa,
+      commitment
+    );
+
+  const postVaaTransaction = unsignedTransactions.pop()!;
+
+  const responses = await sendAndConfirmTransactionsWithRetry(
+    connection,
+    modifySignTransaction(signTransaction, ...signers),
+    payer.toString(),
+    unsignedTransactions,
+    maxRetries
+  );
+  //While the signature_set is used to create the final instruction, it doesn't need to sign it.
+  responses.push(
+    ...(await sendAndConfirmTransactionsWithRetry(
+      connection,
+      signTransaction,
+      payer.toString(),
+      [postVaaTransaction],
+      maxRetries
+    ))
+  );
+  return responses;
+}
+
+export async function postVaa(
+  connection: Connection,
+  signTransaction: SignTransaction,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: Buffer,
+  options?: ConfirmOptions,
+  asyncVerifySignatures: boolean = true
+): Promise<TransactionSignatureAndResponse[]> {
+  const { unsignedTransactions, signers } =
+    await createPostSignedVaaTransactions(
+      connection,
+      wormholeProgramId,
+      payer,
+      vaa,
+      options?.commitment
+    );
+
+  const postVaaTransaction = unsignedTransactions.pop()!;
+
+  const verifySignatures = async (transaction: Transaction) =>
+    signSendAndConfirmTransaction(
+      connection,
+      payer,
+      modifySignTransaction(signTransaction, ...signers),
+      transaction,
+      options
+    );
+
+  const output: TransactionSignatureAndResponse[] = [];
+  if (asyncVerifySignatures) {
+    const verified = await Promise.all(
+      unsignedTransactions.map(async (transaction) =>
+        verifySignatures(transaction)
+      )
+    );
+    output.push(...verified);
+  } else {
+    for (const transaction of unsignedTransactions) {
+      output.push(await verifySignatures(transaction));
+    }
+  }
+  output.push(
+    await signSendAndConfirmTransaction(
+      connection,
+      payer,
+      signTransaction,
+      postVaaTransaction,
+      options
+    )
+  );
+  return output;
+}
+
+/** Send transactions for `verify_signatures` and `post_vaa` instructions.
+ *
+ * Using a signed VAA, execute transactions generated by {@link verifySignatures} and
+ * {@link postVaa}. At most 4 transactions are sent (up to 3 from signature verification
+ * and 1 to post VAA data to an account).
+ *
+ * @param {Connection} connection - Solana web3 connection
+ * @param {PublicKeyInitData} wormholeProgramId - wormhole program address
+ * @param {web3.Keypair} payer - transaction signer address
+ * @param {Buffer} signedVaa - bytes of signed VAA
+ * @param {Commitment} [options] - Solana commitment
+ *
+ */
+export async function createPostSignedVaaTransactions(
+  connection: Connection,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedVaa,
+  commitment?: Commitment
+): Promise<PreparedTransactions> {
+  const parsed = isBytes(vaa) ? parseVaa(vaa) : vaa;
+  const signatureSet = Keypair.generate();
+
+  const verifySignaturesInstructions = await createVerifySignaturesInstructions(
+    connection,
+    wormholeProgramId,
+    payer,
+    parsed,
+    signatureSet.publicKey,
+    commitment
+  );
+
+  const unsignedTransactions: Transaction[] = [];
+  for (let i = 0; i < verifySignaturesInstructions.length; i += 2) {
+    unsignedTransactions.push(
+      new Transaction().add(...verifySignaturesInstructions.slice(i, i + 2))
+    );
+  }
+
+  unsignedTransactions.push(
+    new Transaction().add(
+      createPostVaaInstruction(
+        wormholeProgramId,
+        payer,
+        parsed,
+        signatureSet.publicKey
+      )
+    )
+  );
+
+  return {
+    unsignedTransactions,
+    signers: [signatureSet],
+  };
+}

+ 42 - 0
sdk/js/src/solana/tokenBridge/accounts/config.ts

@@ -0,0 +1,42 @@
+import {
+  Connection,
+  PublicKey,
+  Commitment,
+  PublicKeyInitData,
+} from "@solana/web3.js";
+import { deriveAddress, getAccountData } from "../../utils";
+
+export function deriveTokenBridgeConfigKey(
+  tokenBridgeProgramId: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([Buffer.from("config")], tokenBridgeProgramId);
+}
+
+export async function getTokenBridgeConfig(
+  connection: Connection,
+  tokenBridgeProgramId: PublicKeyInitData,
+  commitment?: Commitment
+): Promise<TokenBridgeConfig> {
+  return connection
+    .getAccountInfo(
+      deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+      commitment
+    )
+    .then((info) => TokenBridgeConfig.deserialize(getAccountData(info)));
+}
+
+export class TokenBridgeConfig {
+  wormhole: PublicKey;
+
+  constructor(wormholeProgramId: Buffer) {
+    this.wormhole = new PublicKey(wormholeProgramId);
+  }
+
+  static deserialize(data: Buffer): TokenBridgeConfig {
+    if (data.length != 32) {
+      throw new Error("data.length != 32");
+    }
+    const wormholeProgramId = data.subarray(0, 32);
+    return new TokenBridgeConfig(wormholeProgramId);
+  }
+}

+ 9 - 0
sdk/js/src/solana/tokenBridge/accounts/custody.ts

@@ -0,0 +1,9 @@
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { deriveAddress } from "../../utils";
+
+export function deriveCustodyKey(
+  tokenBridgeProgramId: PublicKeyInitData,
+  mint: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([new PublicKey(mint).toBuffer()], tokenBridgeProgramId);
+}

+ 70 - 0
sdk/js/src/solana/tokenBridge/accounts/endpoint.ts

@@ -0,0 +1,70 @@
+import {
+  Connection,
+  PublicKey,
+  Commitment,
+  PublicKeyInitData,
+} from "@solana/web3.js";
+import {
+  ChainId,
+  CHAIN_ID_SOLANA,
+  tryNativeToUint8Array,
+} from "../../../utils";
+import { deriveAddress, getAccountData } from "../../utils";
+
+export function deriveEndpointKey(
+  tokenBridgeProgramId: PublicKeyInitData,
+  emitterChain: number | ChainId,
+  emitterAddress: Buffer | Uint8Array | string
+): PublicKey {
+  if (emitterChain == CHAIN_ID_SOLANA) {
+    throw new Error(
+      "emitterChain == CHAIN_ID_SOLANA cannot exist as foreign token bridge emitter"
+    );
+  }
+  if (typeof emitterAddress == "string") {
+    emitterAddress = tryNativeToUint8Array(
+      emitterAddress,
+      emitterChain as ChainId
+    );
+  }
+  return deriveAddress(
+    [
+      (() => {
+        const buf = Buffer.alloc(2);
+        buf.writeUInt16BE(emitterChain as number);
+        return buf;
+      })(),
+      emitterAddress,
+    ],
+    tokenBridgeProgramId
+  );
+}
+
+export async function getEndpointRegistration(
+  connection: Connection,
+  endpointKey: PublicKeyInitData,
+  commitment?: Commitment
+): Promise<EndpointRegistration> {
+  return connection
+    .getAccountInfo(new PublicKey(endpointKey), commitment)
+    .then((info) => EndpointRegistration.deserialize(getAccountData(info)));
+}
+
+export class EndpointRegistration {
+  chain: ChainId;
+  contract: Buffer;
+
+  constructor(chain: number, contract: Buffer) {
+    this.chain = chain as ChainId;
+    this.contract = contract;
+  }
+
+  static deserialize(data: Buffer): EndpointRegistration {
+    if (data.length != 34) {
+      throw new Error("data.length != 34");
+    }
+    const chain = data.readUInt16LE(0);
+    const contract = data.subarray(2, 34);
+    return new EndpointRegistration(chain, contract);
+  }
+}

+ 7 - 0
sdk/js/src/solana/tokenBridge/accounts/index.ts

@@ -0,0 +1,7 @@
+export * from "./config";
+export * from "./custody";
+export * from "./endpoint";
+export * from "./transferWithPayload";
+export * from "./signer";
+export * from "./wrapped";
+export { deriveUpgradeAuthorityKey } from "../../wormhole";

+ 20 - 0
sdk/js/src/solana/tokenBridge/accounts/signer.ts

@@ -0,0 +1,20 @@
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { deriveAddress } from "../../utils";
+
+export function deriveAuthoritySignerKey(
+  tokenBridgeProgramId: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([Buffer.from("authority_signer")], tokenBridgeProgramId);
+}
+
+export function deriveCustodySignerKey(
+  tokenBridgeProgramId: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([Buffer.from("custody_signer")], tokenBridgeProgramId);
+}
+
+export function deriveMintAuthorityKey(
+  tokenBridgeProgramId: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([Buffer.from("mint_signer")], tokenBridgeProgramId);
+}

+ 14 - 0
sdk/js/src/solana/tokenBridge/accounts/transferWithPayload.ts

@@ -0,0 +1,14 @@
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { deriveAddress } from "../../utils";
+
+export function deriveSenderAccountKey(
+  cpiProgramId: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([Buffer.from("sender")], cpiProgramId);
+}
+
+export function deriveRedeemerAccountKey(
+  cpiProgramId: PublicKeyInitData
+): PublicKey {
+  return deriveAddress([Buffer.from("redeemer")], cpiProgramId);
+}

+ 87 - 0
sdk/js/src/solana/tokenBridge/accounts/wrapped.ts

@@ -0,0 +1,87 @@
+import {
+  Connection,
+  PublicKey,
+  Commitment,
+  PublicKeyInitData,
+} from "@solana/web3.js";
+import {
+  ChainId,
+  CHAIN_ID_SOLANA,
+  tryNativeToUint8Array,
+} from "../../../utils";
+import { deriveAddress, getAccountData } from "../../utils";
+
+export { deriveSplTokenMetadataKey } from "../../utils/splMetadata";
+
+export function deriveWrappedMintKey(
+  tokenBridgeProgramId: PublicKeyInitData,
+  tokenChain: number | ChainId,
+  tokenAddress: Buffer | Uint8Array | string
+): PublicKey {
+  if (tokenChain == CHAIN_ID_SOLANA) {
+    throw new Error(
+      "tokenChain == CHAIN_ID_SOLANA does not have wrapped mint key"
+    );
+  }
+  if (typeof tokenAddress == "string") {
+    tokenAddress = tryNativeToUint8Array(tokenAddress, tokenChain as ChainId);
+  }
+  return deriveAddress(
+    [
+      Buffer.from("wrapped"),
+      (() => {
+        const buf = Buffer.alloc(2);
+        buf.writeUInt16BE(tokenChain as number);
+        return buf;
+      })(),
+      tokenAddress,
+    ],
+    tokenBridgeProgramId
+  );
+}
+
+export function deriveWrappedMetaKey(
+  tokenBridgeProgramId: PublicKeyInitData,
+  mint: PublicKeyInitData
+): PublicKey {
+  return deriveAddress(
+    [Buffer.from("meta"), new PublicKey(mint).toBuffer()],
+    tokenBridgeProgramId
+  );
+}
+
+export async function getWrappedMeta(
+  connection: Connection,
+  tokenBridgeProgramId: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  commitment?: Commitment
+): Promise<WrappedMeta> {
+  return connection
+    .getAccountInfo(
+      deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+      commitment
+    )
+    .then((info) => WrappedMeta.deserialize(getAccountData(info)));
+}
+
+export class WrappedMeta {
+  chain: number;
+  tokenAddress: Buffer;
+  originalDecimals: number;
+
+  constructor(chain: number, tokenAddress: Buffer, originalDecimals: number) {
+    this.chain = chain;
+    this.tokenAddress = tokenAddress;
+    this.originalDecimals = originalDecimals;
+  }
+
+  static deserialize(data: Buffer): WrappedMeta {
+    if (data.length != 35) {
+      throw new Error("data.length != 35");
+    }
+    const chain = data.readUInt16LE(0);
+    const tokenAddress = data.subarray(2, 34);
+    const originalDecimals = data.readUInt8(34);
+    return new WrappedMeta(chain, tokenAddress, originalDecimals);
+  }
+}

+ 40 - 0
sdk/js/src/solana/tokenBridge/coder/accounts.ts

@@ -0,0 +1,40 @@
+import { AccountsCoder, Idl } from "@project-serum/anchor";
+import { accountSize, IdlTypeDef } from "../../anchor";
+
+export class TokenBridgeAccountsCoder<A extends string = string>
+  implements AccountsCoder
+{
+  constructor(private idl: Idl) {}
+
+  public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
+    switch (accountName) {
+      default: {
+        throw new Error(`Invalid account name: ${accountName}`);
+      }
+    }
+  }
+
+  public decode<T = any>(accountName: A, ix: Buffer): T {
+    return this.decodeUnchecked(accountName, ix);
+  }
+
+  public decodeUnchecked<T = any>(accountName: A, ix: Buffer): T {
+    switch (accountName) {
+      default: {
+        throw new Error(`Invalid account name: ${accountName}`);
+      }
+    }
+  }
+
+  public memcmp(accountName: A, _appendData?: Buffer): any {
+    switch (accountName) {
+      default: {
+        throw new Error(`Invalid account name: ${accountName}`);
+      }
+    }
+  }
+
+  public size(idlAccount: IdlTypeDef): number {
+    return accountSize(this.idl, idlAccount) ?? 0;
+  }
+}

+ 12 - 0
sdk/js/src/solana/tokenBridge/coder/events.ts

@@ -0,0 +1,12 @@
+import { EventCoder, Event, Idl } from "@project-serum/anchor";
+import { IdlEvent } from "../../anchor";
+
+export class TokenBridgeEventsCoder implements EventCoder {
+  constructor(_idl: Idl) {}
+
+  decode<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
+    _log: string
+  ): Event<E, T> | null {
+    throw new Error("Token Bridge program does not have events");
+  }
+}

+ 24 - 0
sdk/js/src/solana/tokenBridge/coder/index.ts

@@ -0,0 +1,24 @@
+import { Coder, Idl } from "@project-serum/anchor";
+import { TokenBridgeAccountsCoder } from "./accounts";
+import { TokenBridgeEventsCoder } from "./events";
+import { TokenBridgeInstructionCoder } from "./instruction";
+import { TokenBridgeStateCoder } from "./state";
+import { TokenBridgeTypesCoder } from "./types";
+
+export { TokenBridgeInstruction } from "./instruction";
+
+export class TokenBridgeCoder implements Coder {
+  readonly instruction: TokenBridgeInstructionCoder;
+  readonly accounts: TokenBridgeAccountsCoder;
+  readonly state: TokenBridgeStateCoder;
+  readonly events: TokenBridgeEventsCoder;
+  readonly types: TokenBridgeTypesCoder;
+
+  constructor(idl: Idl) {
+    this.instruction = new TokenBridgeInstructionCoder(idl);
+    this.accounts = new TokenBridgeAccountsCoder(idl);
+    this.state = new TokenBridgeStateCoder(idl);
+    this.events = new TokenBridgeEventsCoder(idl);
+    this.types = new TokenBridgeTypesCoder(idl);
+  }
+}

+ 254 - 0
sdk/js/src/solana/tokenBridge/coder/instruction.ts

@@ -0,0 +1,254 @@
+import { Idl, InstructionCoder } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+
+export class TokenBridgeInstructionCoder implements InstructionCoder {
+  constructor(_: Idl) {}
+
+  encode(ixName: string, ix: any): Buffer {
+    switch (ixName) {
+      case "initialize": {
+        return encodeInitialize(ix);
+      }
+      case "attestToken": {
+        return encodeAttestToken(ix);
+      }
+      case "completeNative": {
+        return encodeCompleteNative(ix);
+      }
+      case "completeWrapped": {
+        return encodeCompleteWrapped(ix);
+      }
+      case "transferWrapped": {
+        return encodeTransferWrapped(ix);
+      }
+      case "transferNative": {
+        return encodeTransferNative(ix);
+      }
+      case "registerChain": {
+        return encodeRegisterChain(ix);
+      }
+      case "createWrapped": {
+        return encodeCreateWrapped(ix);
+      }
+      case "upgradeContract": {
+        return encodeUpgradeContract(ix);
+      }
+      case "transferWrappedWithPayload": {
+        return encodeTransferWrappedWithPayload(ix);
+      }
+      case "transferNativeWithPayload": {
+        return encodeTransferNativeWithPayload(ix);
+      }
+      default: {
+        throw new Error(`Invalid instruction: ${ixName}`);
+      }
+    }
+  }
+
+  encodeState(_ixName: string, _ix: any): Buffer {
+    throw new Error("Token Bridge program does not have state");
+  }
+}
+
+/** Solitaire enum of existing the Token Bridge's instructions.
+ *
+ * https://github.com/certusone/wormhole/blob/dev.v2/solana/modules/token_bridge/program/src/lib.rs#L100
+ */
+export enum TokenBridgeInstruction {
+  Initialize,
+  AttestToken,
+  CompleteNative,
+  CompleteWrapped,
+  TransferWrapped,
+  TransferNative,
+  RegisterChain,
+  CreateWrapped,
+  UpgradeContract,
+  CompleteNativeWithPayload,
+  CompleteWrappedWithPayload,
+  TransferWrappedWithPayload,
+  TransferNativeWithPayload,
+}
+
+function encodeTokenBridgeInstructionData(
+  instructionType: TokenBridgeInstruction,
+  data?: Buffer
+): Buffer {
+  const dataLen = data === undefined ? 0 : data.length;
+  const instructionData = Buffer.alloc(1 + dataLen);
+  instructionData.writeUInt8(instructionType, 0);
+  if (dataLen > 0) {
+    instructionData.write(data!.toString("hex"), 1, "hex");
+  }
+  return instructionData;
+}
+
+function encodeInitialize({ wormhole }: any): Buffer {
+  const serialized = Buffer.alloc(32);
+  serialized.write(
+    new PublicKey(wormhole).toBuffer().toString("hex"),
+    0,
+    "hex"
+  );
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.Initialize,
+    serialized
+  );
+}
+
+function encodeAttestToken({ nonce }: any) {
+  const serialized = Buffer.alloc(4);
+  serialized.writeUInt32LE(nonce, 0);
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.AttestToken,
+    serialized
+  );
+}
+
+function encodeCompleteNative({}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.CompleteNative
+  );
+}
+
+function encodeCompleteWrapped({}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.CompleteWrapped
+  );
+}
+
+function encodeTransferData({
+  nonce,
+  amount,
+  fee,
+  targetAddress,
+  targetChain,
+}: any) {
+  if (typeof amount != "bigint") {
+    amount = BigInt(amount);
+  }
+  if (typeof fee != "bigint") {
+    fee = BigInt(fee);
+  }
+  if (!Buffer.isBuffer(targetAddress)) {
+    throw new Error("targetAddress must be Buffer");
+  }
+  const serialized = Buffer.alloc(54);
+  serialized.writeUInt32LE(nonce, 0);
+  serialized.writeBigUInt64LE(amount, 4);
+  serialized.writeBigUInt64LE(fee, 12);
+  serialized.write(targetAddress.toString("hex"), 20, "hex");
+  serialized.writeUInt16LE(targetChain, 52);
+  return serialized;
+}
+
+function encodeTransferWrapped({
+  nonce,
+  amount,
+  fee,
+  targetAddress,
+  targetChain,
+}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.TransferWrapped,
+    encodeTransferData({ nonce, amount, fee, targetAddress, targetChain })
+  );
+}
+
+function encodeTransferNative({
+  nonce,
+  amount,
+  fee,
+  targetAddress,
+  targetChain,
+}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.TransferNative,
+    encodeTransferData({ nonce, amount, fee, targetAddress, targetChain })
+  );
+}
+
+function encodeRegisterChain({}: any) {
+  return encodeTokenBridgeInstructionData(TokenBridgeInstruction.RegisterChain);
+}
+
+function encodeCreateWrapped({}: any) {
+  return encodeTokenBridgeInstructionData(TokenBridgeInstruction.CreateWrapped);
+}
+
+function encodeUpgradeContract({}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.UpgradeContract
+  );
+}
+
+function encodeTransferWithPayloadData({
+  nonce,
+  amount,
+  targetAddress,
+  targetChain,
+  payload,
+}: any) {
+  if (typeof amount != "bigint") {
+    amount = BigInt(amount);
+  }
+  if (!Buffer.isBuffer(targetAddress)) {
+    throw new Error("targetAddress must be Buffer");
+  }
+  if (!Buffer.isBuffer(payload)) {
+    throw new Error("payload must be Buffer");
+  }
+  const serializedWithPayloadLen = Buffer.alloc(50);
+  serializedWithPayloadLen.writeUInt32LE(nonce, 0);
+  serializedWithPayloadLen.writeBigUInt64LE(amount, 4);
+  serializedWithPayloadLen.write(targetAddress.toString("hex"), 12, "hex");
+  serializedWithPayloadLen.writeUInt16LE(targetChain, 44);
+  serializedWithPayloadLen.writeUInt32LE(payload.length, 46);
+  return Buffer.concat([
+    serializedWithPayloadLen,
+    payload,
+    Buffer.alloc(1), // option == None
+  ]);
+}
+
+function encodeTransferWrappedWithPayload({
+  nonce,
+  amount,
+  fee,
+  targetAddress,
+  targetChain,
+  payload,
+}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.TransferWrappedWithPayload,
+    encodeTransferWithPayloadData({
+      nonce,
+      amount,
+      fee,
+      targetAddress,
+      targetChain,
+      payload,
+    })
+  );
+}
+
+function encodeTransferNativeWithPayload({
+  nonce,
+  amount,
+  fee,
+  targetAddress,
+  targetChain,
+  payload,
+}: any) {
+  return encodeTokenBridgeInstructionData(
+    TokenBridgeInstruction.TransferNativeWithPayload,
+    encodeTransferWithPayloadData({
+      nonce,
+      amount,
+      fee,
+      targetAddress,
+      targetChain,
+      payload,
+    })
+  );
+}

+ 12 - 0
sdk/js/src/solana/tokenBridge/coder/state.ts

@@ -0,0 +1,12 @@
+import { Idl, StateCoder } from "@project-serum/anchor";
+
+export class TokenBridgeStateCoder implements StateCoder {
+  constructor(_idl: Idl) {}
+
+  encode<T = any>(_name: string, _account: T): Promise<Buffer> {
+    throw new Error("Token Bridge program does not have state");
+  }
+  decode<T = any>(_ix: Buffer): T {
+    throw new Error("Token Bridge program does not have state");
+  }
+}

+ 12 - 0
sdk/js/src/solana/tokenBridge/coder/types.ts

@@ -0,0 +1,12 @@
+import { Idl, TypesCoder } from "@project-serum/anchor";
+
+export class TokenBridgeTypesCoder implements TypesCoder {
+  constructor(_idl: Idl) {}
+
+  encode<T = any>(_name: string, _type: T): Buffer {
+    throw new Error("Token Bridge program does not have user-defined types");
+  }
+  decode<T = any>(_name: string, _typeData: Buffer): T {
+    throw new Error("Token Bridge program does not have user-defined types");
+  }
+}

+ 477 - 0
sdk/js/src/solana/tokenBridge/cpi.ts

@@ -0,0 +1,477 @@
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+} from "@solana/web3.js";
+import {
+  isBytes,
+  ParsedTokenTransferVaa,
+  parseTokenTransferVaa,
+  SignedVaa,
+} from "../../vaa";
+import {
+  deriveClaimKey,
+  derivePostedVaaKey,
+  getWormholeDerivedAccounts,
+} from "../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveCustodyKey,
+  deriveCustodySignerKey,
+  deriveEndpointKey,
+  deriveMintAuthorityKey,
+  deriveRedeemerAccountKey,
+  deriveSenderAccountKey,
+  deriveTokenBridgeConfigKey,
+  deriveWrappedMetaKey,
+  deriveWrappedMintKey,
+} from "./accounts";
+import {
+  getTransferNativeWithPayloadAccounts,
+  getTransferWrappedWithPayloadAccounts,
+} from "./instructions";
+
+export interface TokenBridgeBaseDerivedAccounts {
+  /**
+   * seeds = ["config"], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeConfig: PublicKey;
+}
+
+export interface TokenBridgeBaseNativeDerivedAccounts
+  extends TokenBridgeBaseDerivedAccounts {
+  /**
+   * seeds = ["custody_signer"], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeCustodySigner: PublicKey;
+}
+
+export interface TokenBridgeBaseSenderDerivedAccounts
+  extends TokenBridgeBaseDerivedAccounts {
+  /**
+   * seeds = ["authority_signer"], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeAuthoritySigner: PublicKey;
+  /**
+   * seeds = ["sender"], seeds::program = cpiProgramId
+   */
+  tokenBridgeSender: PublicKey;
+  /**
+   * seeds = ["Bridge"], seeds::program = wormholeProgram
+   */
+  wormholeBridge: PublicKey;
+  /**
+   * seeds = ["emitter"], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeEmitter: PublicKey;
+  /**
+   * seeds = ["Sequence", tokenBridgeEmitter], seeds::program = wormholeProgram
+   */
+  tokenBridgeSequence: PublicKey;
+  /**
+   * seeds = ["fee_collector"], seeds::program = wormholeProgram
+   */
+  wormholeFeeCollector: PublicKey;
+}
+
+export interface TokenBridgeNativeSenderDerivedAccounts
+  extends TokenBridgeBaseNativeDerivedAccounts,
+    TokenBridgeBaseSenderDerivedAccounts {}
+
+export interface TokenBridgeWrappedSenderDerivedAccounts
+  extends TokenBridgeBaseSenderDerivedAccounts {}
+
+export interface TokenBridgeBaseRedeemerDerivedAccounts
+  extends TokenBridgeBaseDerivedAccounts {
+  /**
+   * seeds = ["redeemer"], seeds::program = cpiProgramId
+   */
+  tokenBridgeRedeemer: PublicKey;
+}
+
+export interface TokenBridgeNativeRedeemerDerivedAccounts
+  extends TokenBridgeBaseNativeDerivedAccounts,
+    TokenBridgeBaseRedeemerDerivedAccounts {}
+
+export interface TokenBridgeWrappedRedeemerDerivedAccounts
+  extends TokenBridgeBaseRedeemerDerivedAccounts {
+  /**
+   * seeds = ["mint_signer"], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeMintAuthority: PublicKey;
+}
+
+export interface TokenBridgeDerivedAccounts
+  extends TokenBridgeNativeSenderDerivedAccounts,
+    TokenBridgeWrappedSenderDerivedAccounts,
+    TokenBridgeNativeRedeemerDerivedAccounts,
+    TokenBridgeWrappedRedeemerDerivedAccounts {}
+
+/**
+ * Generate Token Bridge PDAs.
+ *
+ * @param cpiProgramId
+ * @param tokenBridgeProgramId
+ * @param wormholeProgramId
+ * @returns
+ */
+export function getTokenBridgeDerivedAccounts(
+  cpiProgramId: PublicKeyInitData,
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData
+): TokenBridgeDerivedAccounts {
+  const {
+    wormholeEmitter: tokenBridgeEmitter,
+    wormholeBridge,
+    wormholeFeeCollector,
+    wormholeSequence: tokenBridgeSequence,
+  } = getWormholeDerivedAccounts(tokenBridgeProgramId, wormholeProgramId);
+  return {
+    tokenBridgeConfig: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    tokenBridgeAuthoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId),
+    tokenBridgeCustodySigner: deriveCustodySignerKey(tokenBridgeProgramId),
+    tokenBridgeMintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId),
+    tokenBridgeSender: deriveSenderAccountKey(cpiProgramId),
+    tokenBridgeRedeemer: deriveRedeemerAccountKey(cpiProgramId),
+    wormholeBridge,
+    tokenBridgeEmitter,
+    wormholeFeeCollector,
+    tokenBridgeSequence,
+  };
+}
+
+export interface TransferNativeWithPayloadCpiAccounts
+  extends TokenBridgeNativeSenderDerivedAccounts {
+  payer: PublicKey;
+  /**
+   * seeds = [mint], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeCustody: PublicKey;
+  /**
+   * Token account where tokens reside
+   */
+  fromTokenAccount: PublicKey;
+  mint: PublicKey;
+  wormholeMessage: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+/**
+ * Generate accounts needed to perform `transfer_wrapped_with_payload` instruction
+ * as cross-program invocation.
+ *
+ * @param cpiProgramId
+ * @param tokenBridgeProgramId
+ * @param wormholeProgramId
+ * @param payer
+ * @param message
+ * @param fromTokenAccount
+ * @param mint
+ * @returns
+ */
+export function getTransferNativeWithPayloadCpiAccounts(
+  cpiProgramId: PublicKeyInitData,
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  fromTokenAccount: PublicKeyInitData,
+  mint: PublicKeyInitData
+): TransferNativeWithPayloadCpiAccounts {
+  const accounts = getTransferNativeWithPayloadAccounts(
+    tokenBridgeProgramId,
+    wormholeProgramId,
+    payer,
+    message,
+    fromTokenAccount,
+    mint,
+    cpiProgramId
+  );
+  return {
+    payer: accounts.payer,
+    tokenBridgeConfig: accounts.config,
+    fromTokenAccount: accounts.from,
+    mint: accounts.mint,
+    tokenBridgeCustody: accounts.custody,
+    tokenBridgeAuthoritySigner: accounts.authoritySigner,
+    tokenBridgeCustodySigner: accounts.custodySigner,
+    wormholeBridge: accounts.wormholeBridge,
+    wormholeMessage: accounts.wormholeMessage,
+    tokenBridgeEmitter: accounts.wormholeEmitter,
+    tokenBridgeSequence: accounts.wormholeSequence,
+    wormholeFeeCollector: accounts.wormholeFeeCollector,
+    clock: accounts.clock,
+    tokenBridgeSender: accounts.sender,
+    rent: accounts.rent,
+    systemProgram: accounts.systemProgram,
+    tokenProgram: accounts.tokenProgram,
+    wormholeProgram: accounts.wormholeProgram,
+  };
+}
+
+export interface TransferWrappedWithPayloadCpiAccounts
+  extends TokenBridgeWrappedSenderDerivedAccounts {
+  payer: PublicKey;
+  /**
+   * Token account where tokens reside
+   */
+  fromTokenAccount: PublicKey;
+  /**
+   * Token account owner (usually cpiProgramId)
+   */
+  fromTokenAccountOwner: PublicKey;
+  /**
+   * seeds = ["wrapped", token_chain, token_address], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeWrappedMint: PublicKey;
+  /**
+   * seeds = ["meta", mint], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeWrappedMeta: PublicKey;
+  wormholeMessage: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+/**
+ * Generate accounts needed to perform `transfer_wrapped_with_payload` instruction
+ * as cross-program invocation.
+ *
+ * @param cpiProgramId
+ * @param tokenBridgeProgramId
+ * @param wormholeProgramId
+ * @param payer
+ * @param message
+ * @param fromTokenAccount
+ * @param tokenChain
+ * @param tokenAddress
+ * @param [fromTokenAccountOwner]
+ * @returns
+ */
+export function getTransferWrappedWithPayloadCpiAccounts(
+  cpiProgramId: PublicKeyInitData,
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  fromTokenAccount: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array,
+  fromTokenAccountOwner?: PublicKeyInitData
+): TransferWrappedWithPayloadCpiAccounts {
+  const accounts = getTransferWrappedWithPayloadAccounts(
+    tokenBridgeProgramId,
+    wormholeProgramId,
+    payer,
+    message,
+    fromTokenAccount,
+    fromTokenAccountOwner === undefined ? cpiProgramId : fromTokenAccountOwner,
+    tokenChain,
+    tokenAddress,
+    cpiProgramId
+  );
+  return {
+    payer: accounts.payer,
+    tokenBridgeConfig: accounts.config,
+    fromTokenAccount: accounts.from,
+    fromTokenAccountOwner: accounts.fromOwner,
+    tokenBridgeWrappedMint: accounts.mint,
+    tokenBridgeWrappedMeta: accounts.wrappedMeta,
+    tokenBridgeAuthoritySigner: accounts.authoritySigner,
+    wormholeBridge: accounts.wormholeBridge,
+    wormholeMessage: accounts.wormholeMessage,
+    tokenBridgeEmitter: accounts.wormholeEmitter,
+    tokenBridgeSequence: accounts.wormholeSequence,
+    wormholeFeeCollector: accounts.wormholeFeeCollector,
+    clock: accounts.clock,
+    tokenBridgeSender: accounts.sender,
+    rent: accounts.rent,
+    systemProgram: accounts.systemProgram,
+    tokenProgram: accounts.tokenProgram,
+    wormholeProgram: accounts.wormholeProgram,
+  };
+}
+
+export interface CompleteTransferNativeWithPayloadCpiAccounts
+  extends TokenBridgeNativeRedeemerDerivedAccounts {
+  payer: PublicKey;
+  /**
+   * seeds = ["PostedVAA", vaa_hash], seeds::program = wormholeProgram
+   */
+  vaa: PublicKey;
+  /**
+   * seeds = [emitter_address, emitter_chain, sequence], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeClaim: PublicKey;
+  /**
+   * seeds = [emitter_chain, emitter_address], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeForeignEndpoint: PublicKey;
+  /**
+   * Token account to receive tokens
+   */
+  toTokenAccount: PublicKey;
+  toFeesTokenAccount: PublicKey; // this shouldn't exist?
+  /**
+   * seeds = [mint], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeCustody: PublicKey;
+  mint: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+/**
+ * Generate accounts needed to perform `complete_native_with_payload` instruction
+ * as cross-program invocation.
+ *
+ * Note: `toFeesTokenAccount` is the same as `toTokenAccount`. For your program,
+ * you only need to pass your `toTokenAccount` into the complete transfer
+ * instruction for the `toFeesTokenAccount`.
+ *
+ * @param cpiProgramId
+ * @param tokenBridgeProgramId
+ * @param wormholeProgramId
+ * @param payer
+ * @param vaa
+ * @returns
+ */
+export function getCompleteTransferNativeWithPayloadCpiAccounts(
+  cpiProgramId: PublicKeyInitData,
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenTransferVaa
+): CompleteTransferNativeWithPayloadCpiAccounts {
+  const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa;
+  const mint = new PublicKey(parsed.tokenAddress);
+  const toTokenAccount = new PublicKey(parsed.to);
+  return {
+    payer: new PublicKey(payer),
+    tokenBridgeConfig: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    tokenBridgeClaim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    tokenBridgeForeignEndpoint: deriveEndpointKey(
+      tokenBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    toTokenAccount,
+    tokenBridgeRedeemer: deriveRedeemerAccountKey(cpiProgramId),
+    toFeesTokenAccount: toTokenAccount,
+    tokenBridgeCustody: deriveCustodyKey(tokenBridgeProgramId, mint),
+    mint,
+    tokenBridgeCustodySigner: deriveCustodySignerKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}
+
+export interface CompleteTransferWrappedWithPayloadCpiAccounts
+  extends TokenBridgeWrappedRedeemerDerivedAccounts {
+  payer: PublicKey;
+  /**
+   * seeds = ["PostedVAA", vaa_hash], seeds::program = wormholeProgram
+   */
+  vaa: PublicKey;
+  /**
+   * seeds = [emitter_address, emitter_chain, sequence], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeClaim: PublicKey;
+  /**
+   * seeds = [emitter_chain, emitter_address], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeForeignEndpoint: PublicKey;
+  /**
+   * Token account to receive tokens
+   */
+  toTokenAccount: PublicKey;
+  toFeesTokenAccount: PublicKey; // this shouldn't exist?
+  /**
+   * seeds = ["wrapped", token_chain, token_address], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeWrappedMint: PublicKey;
+  /**
+   * seeds = ["meta", mint], seeds::program = tokenBridgeProgram
+   */
+  tokenBridgeWrappedMeta: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+/**
+ * Generate accounts needed to perform `complete_wrapped_with_payload` instruction
+ * as cross-program invocation.
+ *
+ * Note: `toFeesTokenAccount` is the same as `toTokenAccount`. For your program,
+ * you only need to pass your `toTokenAccount` into the complete transfer
+ * instruction for the `toFeesTokenAccount`.
+ *
+ * @param cpiProgramId
+ * @param tokenBridgeProgramId
+ * @param wormholeProgramId
+ * @param payer
+ * @param vaa
+ * @returns
+ */
+export function getCompleteTransferWrappedWithPayloadCpiAccounts(
+  cpiProgramId: PublicKeyInitData,
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenTransferVaa
+): CompleteTransferWrappedWithPayloadCpiAccounts {
+  const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa;
+  const mint = deriveWrappedMintKey(
+    tokenBridgeProgramId,
+    parsed.tokenChain,
+    parsed.tokenAddress
+  );
+  const toTokenAccount = new PublicKey(parsed.to);
+  return {
+    payer: new PublicKey(payer),
+    tokenBridgeConfig: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    tokenBridgeClaim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    tokenBridgeForeignEndpoint: deriveEndpointKey(
+      tokenBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    toTokenAccount,
+    tokenBridgeRedeemer: deriveRedeemerAccountKey(cpiProgramId),
+    toFeesTokenAccount: toTokenAccount,
+    tokenBridgeWrappedMint: mint,
+    tokenBridgeWrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+    tokenBridgeMintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 4 - 0
sdk/js/src/solana/tokenBridge/index.ts

@@ -0,0 +1,4 @@
+export * from "./accounts";
+export * from "./cpi";
+export * from "./instructions";
+export * from "./program";

+ 17 - 0
sdk/js/src/solana/tokenBridge/instructions/approve.ts

@@ -0,0 +1,17 @@
+import { createApproveInstruction } from "@solana/spl-token";
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { deriveAuthoritySignerKey } from "../accounts";
+
+export function createApproveAuthoritySignerInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  tokenAccount: PublicKeyInitData,
+  owner: PublicKeyInitData,
+  amount: number | bigint
+) {
+  return createApproveInstruction(
+    new PublicKey(tokenAccount),
+    deriveAuthoritySignerKey(tokenBridgeProgramId),
+    new PublicKey(owner),
+    amount
+  );
+}

+ 97 - 0
sdk/js/src/solana/tokenBridge/instructions/attestToken.ts

@@ -0,0 +1,97 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { getPostMessageAccounts } from "../../wormhole";
+import {
+  deriveSplTokenMetadataKey,
+  deriveTokenBridgeConfigKey,
+  deriveWrappedMetaKey,
+} from "../accounts";
+
+export function createAttestTokenInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  message: PublicKeyInitData,
+  nonce: number
+): TransactionInstruction {
+  const methods =
+    createReadOnlyTokenBridgeProgramInterface(
+      tokenBridgeProgramId
+    ).methods.attestToken(nonce);
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getAttestTokenAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      mint,
+      message
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface AttestTokenAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  splMetadata: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getAttestTokenAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  message: PublicKeyInitData
+): AttestTokenAccounts {
+  const {
+    bridge: wormholeBridge,
+    emitter: wormholeEmitter,
+    sequence: wormholeSequence,
+    feeCollector: wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageAccounts(
+    wormholeProgramId,
+    payer,
+    tokenBridgeProgramId,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    mint: new PublicKey(mint),
+    wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+    splMetadata: deriveSplTokenMetadataKey(mint),
+    wormholeBridge,
+    wormholeMessage: new PublicKey(message),
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 105 - 0
sdk/js/src/solana/tokenBridge/instructions/completeNative.ts

@@ -0,0 +1,105 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveTokenBridgeConfigKey,
+  deriveCustodyKey,
+  deriveCustodySignerKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedTokenTransferVaa,
+  parseTokenTransferVaa,
+  SignedVaa,
+} from "../../../vaa";
+
+export function createCompleteTransferNativeInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenTransferVaa,
+  feeRecipient?: PublicKeyInitData
+): TransactionInstruction {
+  const methods =
+    createReadOnlyTokenBridgeProgramInterface(
+      tokenBridgeProgramId
+    ).methods.completeNative();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getCompleteTransferNativeAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa,
+      feeRecipient
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface CompleteTransferNativeAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  endpoint: PublicKey;
+  to: PublicKey;
+  toFees: PublicKey;
+  custody: PublicKey;
+  mint: PublicKey;
+  custodySigner: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getCompleteTransferNativeAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenTransferVaa,
+  feeRecipient?: PublicKeyInitData
+): CompleteTransferNativeAccounts {
+  const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa;
+  const mint = new PublicKey(parsed.tokenAddress);
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    endpoint: deriveEndpointKey(
+      tokenBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    to: new PublicKey(parsed.to),
+    toFees: new PublicKey(
+      feeRecipient === undefined ? parsed.to : feeRecipient
+    ),
+    custody: deriveCustodyKey(tokenBridgeProgramId, mint),
+    mint,
+    custodySigner: deriveCustodySignerKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 110 - 0
sdk/js/src/solana/tokenBridge/instructions/completeWrapped.ts

@@ -0,0 +1,110 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveTokenBridgeConfigKey,
+  deriveWrappedMintKey,
+  deriveWrappedMetaKey,
+  deriveMintAuthorityKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedTokenTransferVaa,
+  parseTokenTransferVaa,
+  SignedVaa,
+} from "../../../vaa";
+
+export function createCompleteTransferWrappedInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenTransferVaa,
+  feeRecipient?: PublicKeyInitData
+): TransactionInstruction {
+  const methods =
+    createReadOnlyTokenBridgeProgramInterface(
+      tokenBridgeProgramId
+    ).methods.completeWrapped();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getCompleteTransferWrappedAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa,
+      feeRecipient
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface CompleteTransferWrappedAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  endpoint: PublicKey;
+  to: PublicKey;
+  toFees: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  mintAuthority: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getCompleteTransferWrappedAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenTransferVaa,
+  feeRecipient?: PublicKeyInitData
+): CompleteTransferWrappedAccounts {
+  const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa;
+  const mint = deriveWrappedMintKey(
+    tokenBridgeProgramId,
+    parsed.tokenChain,
+    parsed.tokenAddress
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    endpoint: deriveEndpointKey(
+      tokenBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    to: new PublicKey(parsed.to),
+    toFees: new PublicKey(
+      feeRecipient === undefined ? parsed.to : feeRecipient
+    ),
+    mint,
+    wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+    mintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 107 - 0
sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts

@@ -0,0 +1,107 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveMintAuthorityKey,
+  deriveSplTokenMetadataKey,
+  deriveWrappedMetaKey,
+  deriveTokenBridgeConfigKey,
+  deriveWrappedMintKey,
+} from "../accounts";
+import {
+  isBytes,
+  parseAttestMetaVaa,
+  ParsedAttestMetaVaa,
+  SignedVaa,
+} from "../../../vaa";
+import { SplTokenMetadataProgram } from "../../utils";
+
+export function createCreateWrappedInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedAttestMetaVaa
+): TransactionInstruction {
+  const methods =
+    createReadOnlyTokenBridgeProgramInterface(
+      tokenBridgeProgramId
+    ).methods.createWrapped();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getCreateWrappedAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface CreateWrappedAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  endpoint: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  splMetadata: PublicKey;
+  mintAuthority: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  splMetadataProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getCreateWrappedAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedAttestMetaVaa
+): CreateWrappedAccounts {
+  const parsed = isBytes(vaa) ? parseAttestMetaVaa(vaa) : vaa;
+  const mint = deriveWrappedMintKey(
+    tokenBridgeProgramId,
+    parsed.tokenChain,
+    parsed.tokenAddress
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    endpoint: deriveEndpointKey(
+      tokenBridgeProgramId,
+      parsed.emitterChain,
+      parsed.emitterAddress
+    ),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    mint,
+    wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+    splMetadata: deriveSplTokenMetadataKey(mint),
+    mintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    splMetadataProgram: SplTokenMetadataProgram.programId,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 161 - 0
sdk/js/src/solana/tokenBridge/instructions/governance.ts

@@ -0,0 +1,161 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_CLOCK_PUBKEY,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
+import {
+  deriveEndpointKey,
+  deriveTokenBridgeConfigKey,
+  deriveUpgradeAuthorityKey,
+} from "../accounts";
+import {
+  isBytes,
+  ParsedTokenBridgeRegisterChainVaa,
+  ParsedTokenBridgeUpgradeContractVaa,
+  parseTokenBridgeRegisterChainVaa,
+  parseTokenBridgeUpgradeContractVaa,
+  SignedVaa,
+} from "../../../vaa";
+import { BpfLoaderUpgradeable, deriveUpgradeableProgramKey } from "../../utils";
+
+export function createRegisterChainInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenBridgeRegisterChainVaa
+): TransactionInstruction {
+  const methods =
+    createReadOnlyTokenBridgeProgramInterface(
+      tokenBridgeProgramId
+    ).methods.registerChain();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getRegisterChainAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface RegisterChainAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  endpoint: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getRegisterChainAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenBridgeRegisterChainVaa
+): RegisterChainAccounts {
+  const parsed = isBytes(vaa) ? parseTokenBridgeRegisterChainVaa(vaa) : vaa;
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    endpoint: deriveEndpointKey(
+      tokenBridgeProgramId,
+      parsed.foreignChain,
+      parsed.foreignAddress
+    ),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}
+
+export function createUpgradeContractInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenBridgeUpgradeContractVaa,
+  spill?: PublicKeyInitData
+): TransactionInstruction {
+  const methods =
+    createReadOnlyTokenBridgeProgramInterface(
+      tokenBridgeProgramId
+    ).methods.upgradeContract();
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getUpgradeContractAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      vaa,
+      spill
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface UpgradeContractAccounts {
+  payer: PublicKey;
+  vaa: PublicKey;
+  claim: PublicKey;
+  upgradeAuthority: PublicKey;
+  spill: PublicKey;
+  implementation: PublicKey;
+  programData: PublicKey;
+  tokenBridgeProgram: PublicKey;
+  rent: PublicKey;
+  clock: PublicKey;
+  bpfLoaderUpgradeable: PublicKey;
+  systemProgram: PublicKey;
+}
+
+export function getUpgradeContractAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  vaa: SignedVaa | ParsedTokenBridgeUpgradeContractVaa,
+  spill?: PublicKeyInitData
+): UpgradeContractAccounts {
+  const parsed = isBytes(vaa) ? parseTokenBridgeUpgradeContractVaa(vaa) : vaa;
+  return {
+    payer: new PublicKey(payer),
+    vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash),
+    claim: deriveClaimKey(
+      tokenBridgeProgramId,
+      parsed.emitterAddress,
+      parsed.emitterChain,
+      parsed.sequence
+    ),
+    upgradeAuthority: deriveUpgradeAuthorityKey(tokenBridgeProgramId),
+    spill: new PublicKey(spill === undefined ? payer : spill),
+    implementation: new PublicKey(parsed.newContract),
+    programData: deriveUpgradeableProgramKey(tokenBridgeProgramId),
+    tokenBridgeProgram: new PublicKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    clock: SYSVAR_CLOCK_PUBKEY,
+    bpfLoaderUpgradeable: BpfLoaderUpgradeable.programId,
+    systemProgram: SystemProgram.programId,
+  };
+}

+ 11 - 0
sdk/js/src/solana/tokenBridge/instructions/index.ts

@@ -0,0 +1,11 @@
+export * from "./approve";
+export * from "./attestToken";
+export * from "./completeNative";
+export * from "./completeWrapped";
+export * from "./createWrapped";
+export * from "./initialize";
+export * from "./governance";
+export * from "./transferNative";
+export * from "./transferNativeWithPayload";
+export * from "./transferWrapped";
+export * from "./transferWrappedWithPayload";

+ 47 - 0
sdk/js/src/solana/tokenBridge/instructions/initialize.ts

@@ -0,0 +1,47 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { deriveTokenBridgeConfigKey } from "../accounts";
+
+export function createInitializeInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData
+): TransactionInstruction {
+  const methods = createReadOnlyTokenBridgeProgramInterface(
+    tokenBridgeProgramId
+  ).methods.initialize(wormholeProgramId as any);
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getInitializeAccounts(tokenBridgeProgramId, payer) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface InitializeAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+}
+
+export function getInitializeAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData
+): InitializeAccounts {
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    rent: SYSVAR_RENT_PUBKEY,
+    systemProgram: SystemProgram.programId,
+  };
+}

+ 118 - 0
sdk/js/src/solana/tokenBridge/instructions/transferNative.ts

@@ -0,0 +1,118 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { getPostMessageCpiAccounts } from "../../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveCustodySignerKey,
+  deriveTokenBridgeConfigKey,
+  deriveCustodyKey,
+} from "../accounts";
+
+export function createTransferNativeInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  nonce: number,
+  amount: bigint,
+  fee: bigint,
+  targetAddress: Buffer | Uint8Array,
+  targetChain: number
+): TransactionInstruction {
+  const methods = createReadOnlyTokenBridgeProgramInterface(
+    tokenBridgeProgramId
+  ).methods.transferNative(
+    nonce,
+    amount as any,
+    fee as any,
+    Buffer.from(targetAddress) as any,
+    targetChain
+  );
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getTransferNativeAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      message,
+      from,
+      mint
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface TransferNativeAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  from: PublicKey;
+  mint: PublicKey;
+  custody: PublicKey;
+  authoritySigner: PublicKey;
+  custodySigner: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getTransferNativeAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  mint: PublicKeyInitData
+): TransferNativeAccounts {
+  const {
+    wormholeBridge,
+    wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageCpiAccounts(
+    tokenBridgeProgramId,
+    wormholeProgramId,
+    payer,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    from: new PublicKey(from),
+    mint: new PublicKey(mint),
+    custody: deriveCustodyKey(tokenBridgeProgramId, mint),
+    authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId),
+    custodySigner: deriveCustodySignerKey(tokenBridgeProgramId),
+    wormholeBridge,
+    wormholeMessage: wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 125 - 0
sdk/js/src/solana/tokenBridge/instructions/transferNativeWithPayload.ts

@@ -0,0 +1,125 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { getPostMessageCpiAccounts } from "../../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveCustodySignerKey,
+  deriveTokenBridgeConfigKey,
+  deriveCustodyKey,
+  deriveSenderAccountKey,
+} from "../accounts";
+
+export function createTransferNativeWithPayloadInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  nonce: number,
+  amount: bigint,
+  targetAddress: Buffer | Uint8Array,
+  targetChain: number,
+  payload: Buffer | Uint8Array
+): TransactionInstruction {
+  const methods = createReadOnlyTokenBridgeProgramInterface(
+    tokenBridgeProgramId
+  ).methods.transferNativeWithPayload(
+    nonce,
+    amount as any,
+    Buffer.from(targetAddress) as any,
+    targetChain,
+    Buffer.from(payload) as any,
+    null
+  );
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getTransferNativeWithPayloadAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      message,
+      from,
+      mint
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface TransferNativeWithPayloadAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  from: PublicKey;
+  mint: PublicKey;
+  custody: PublicKey;
+  authoritySigner: PublicKey;
+  custodySigner: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  sender: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getTransferNativeWithPayloadAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  mint: PublicKeyInitData,
+  cpiProgramId?: PublicKeyInitData
+): TransferNativeWithPayloadAccounts {
+  const {
+    wormholeBridge,
+    wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageCpiAccounts(
+    tokenBridgeProgramId,
+    wormholeProgramId,
+    payer,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    from: new PublicKey(from),
+    mint: new PublicKey(mint),
+    custody: deriveCustodyKey(tokenBridgeProgramId, mint),
+    authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId),
+    custodySigner: deriveCustodySignerKey(tokenBridgeProgramId),
+    wormholeBridge,
+    wormholeMessage: wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    sender: new PublicKey(
+      cpiProgramId === undefined ? payer : deriveSenderAccountKey(cpiProgramId)
+    ),
+    rent,
+    systemProgram,
+    tokenProgram: TOKEN_PROGRAM_ID,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+  };
+}

+ 129 - 0
sdk/js/src/solana/tokenBridge/instructions/transferWrapped.ts

@@ -0,0 +1,129 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { getPostMessageCpiAccounts } from "../../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveTokenBridgeConfigKey,
+  deriveWrappedMetaKey,
+  deriveWrappedMintKey,
+} from "../accounts";
+
+export function createTransferWrappedInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  fromOwner: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array,
+  nonce: number,
+  amount: bigint,
+  fee: bigint,
+  targetAddress: Buffer | Uint8Array,
+  targetChain: number
+): TransactionInstruction {
+  const methods = createReadOnlyTokenBridgeProgramInterface(
+    tokenBridgeProgramId
+  ).methods.transferWrapped(
+    nonce,
+    amount as any,
+    fee as any,
+    Buffer.from(targetAddress) as any,
+    targetChain
+  );
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getTransferWrappedAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      message,
+      from,
+      fromOwner,
+      tokenChain,
+      tokenAddress
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface TransferWrappedAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  from: PublicKey;
+  fromOwner: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  authoritySigner: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  wormholeProgram: PublicKey;
+  tokenProgram: PublicKey;
+}
+
+export function getTransferWrappedAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  fromOwner: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array
+): TransferWrappedAccounts {
+  const mint = deriveWrappedMintKey(
+    tokenBridgeProgramId,
+    tokenChain,
+    tokenAddress
+  );
+  const {
+    wormholeBridge,
+    wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageCpiAccounts(
+    tokenBridgeProgramId,
+    wormholeProgramId,
+    payer,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    from: new PublicKey(from),
+    fromOwner: new PublicKey(fromOwner),
+    mint: mint,
+    wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+    authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId),
+    wormholeBridge,
+    wormholeMessage: wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+    tokenProgram: TOKEN_PROGRAM_ID,
+  };
+}

+ 136 - 0
sdk/js/src/solana/tokenBridge/instructions/transferWrappedWithPayload.ts

@@ -0,0 +1,136 @@
+import {
+  PublicKey,
+  PublicKeyInitData,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import { getPostMessageCpiAccounts } from "../../wormhole";
+import {
+  deriveAuthoritySignerKey,
+  deriveSenderAccountKey,
+  deriveTokenBridgeConfigKey,
+  deriveWrappedMetaKey,
+  deriveWrappedMintKey,
+} from "../accounts";
+
+export function createTransferWrappedWithPayloadInstruction(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  fromOwner: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array,
+  nonce: number,
+  amount: bigint,
+  targetAddress: Buffer | Uint8Array,
+  targetChain: number,
+  payload: Buffer | Uint8Array
+): TransactionInstruction {
+  const methods = createReadOnlyTokenBridgeProgramInterface(
+    tokenBridgeProgramId
+  ).methods.transferWrappedWithPayload(
+    nonce,
+    amount as any,
+    Buffer.from(targetAddress) as any,
+    targetChain,
+    Buffer.from(payload) as any,
+    null
+  );
+
+  // @ts-ignore
+  return methods._ixFn(...methods._args, {
+    accounts: getTransferWrappedWithPayloadAccounts(
+      tokenBridgeProgramId,
+      wormholeProgramId,
+      payer,
+      message,
+      from,
+      fromOwner,
+      tokenChain,
+      tokenAddress
+    ) as any,
+    signers: undefined,
+    remainingAccounts: undefined,
+    preInstructions: undefined,
+    postInstructions: undefined,
+  });
+}
+
+export interface TransferWrappedWithPayloadAccounts {
+  payer: PublicKey;
+  config: PublicKey;
+  from: PublicKey;
+  fromOwner: PublicKey;
+  mint: PublicKey;
+  wrappedMeta: PublicKey;
+  authoritySigner: PublicKey;
+  wormholeBridge: PublicKey;
+  wormholeMessage: PublicKey;
+  wormholeEmitter: PublicKey;
+  wormholeSequence: PublicKey;
+  wormholeFeeCollector: PublicKey;
+  clock: PublicKey;
+  sender: PublicKey;
+  rent: PublicKey;
+  systemProgram: PublicKey;
+  tokenProgram: PublicKey;
+  wormholeProgram: PublicKey;
+}
+
+export function getTransferWrappedWithPayloadAccounts(
+  tokenBridgeProgramId: PublicKeyInitData,
+  wormholeProgramId: PublicKeyInitData,
+  payer: PublicKeyInitData,
+  message: PublicKeyInitData,
+  from: PublicKeyInitData,
+  fromOwner: PublicKeyInitData,
+  tokenChain: number,
+  tokenAddress: Buffer | Uint8Array,
+  cpiProgramId?: PublicKeyInitData
+): TransferWrappedWithPayloadAccounts {
+  const mint = deriveWrappedMintKey(
+    tokenBridgeProgramId,
+    tokenChain,
+    tokenAddress
+  );
+  const {
+    wormholeBridge,
+    wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    rent,
+    systemProgram,
+  } = getPostMessageCpiAccounts(
+    tokenBridgeProgramId,
+    wormholeProgramId,
+    payer,
+    message
+  );
+  return {
+    payer: new PublicKey(payer),
+    config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
+    from: new PublicKey(from),
+    fromOwner: new PublicKey(fromOwner),
+    mint: mint,
+    wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
+    authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId),
+    wormholeBridge,
+    wormholeMessage: wormholeMessage,
+    wormholeEmitter,
+    wormholeSequence,
+    wormholeFeeCollector,
+    clock,
+    sender: new PublicKey(
+      cpiProgramId === undefined ? payer : deriveSenderAccountKey(cpiProgramId)
+    ),
+    rent,
+    systemProgram,
+    wormholeProgram: new PublicKey(wormholeProgramId),
+    tokenProgram: TOKEN_PROGRAM_ID,
+  };
+}

+ 33 - 0
sdk/js/src/solana/tokenBridge/program.ts

@@ -0,0 +1,33 @@
+import { Connection, PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { Program, Provider } from "@project-serum/anchor";
+import { createReadOnlyProvider } from "../utils";
+import { TokenBridgeCoder } from "./coder";
+import { TokenBridge } from "../types/tokenBridge";
+
+import IDL from "../../anchor-idl/token_bridge.json";
+
+export function createTokenBridgeProgramInterface(
+  programId: PublicKeyInitData,
+  provider?: Provider
+): Program<TokenBridge> {
+  return new Program<TokenBridge>(
+    IDL as TokenBridge,
+    new PublicKey(programId),
+    provider === undefined ? ({ connection: null } as any) : provider,
+    coder()
+  );
+}
+
+export function createReadOnlyTokenBridgeProgramInterface(
+  programId: PublicKeyInitData,
+  connection?: Connection
+): Program<TokenBridge> {
+  return createTokenBridgeProgramInterface(
+    programId,
+    createReadOnlyProvider(connection)
+  );
+}
+
+export function coder(): TokenBridgeCoder {
+  return new TokenBridgeCoder(IDL as TokenBridge);
+}

+ 69 - 0
sdk/js/src/solana/utils/account.ts

@@ -0,0 +1,69 @@
+import {
+  PublicKey,
+  AccountMeta,
+  AccountInfo,
+  PublicKeyInitData,
+} from "@solana/web3.js";
+
+/**
+ * Find valid program address. See {@link PublicKey.findProgramAddressSync} for details.
+ *
+ * @param {(Buffer | Uint8Array)[]} seeds - seeds for PDA
+ * @param {PublicKeyInitData} programId - program address
+ * @returns PDA
+ */
+export function deriveAddress(
+  seeds: (Buffer | Uint8Array)[],
+  programId: PublicKeyInitData
+): PublicKey {
+  return PublicKey.findProgramAddressSync(seeds, new PublicKey(programId))[0];
+}
+
+/**
+ * Factory to create AccountMeta with `isWritable` set to `true`
+ *
+ * @param {PublicKEyInitData} pubkey - account address
+ * @param {boolean} isSigner - whether account authorized transaction
+ * @returns metadata for writable account
+ */
+export function newAccountMeta(
+  pubkey: PublicKeyInitData,
+  isSigner: boolean
+): AccountMeta {
+  return {
+    pubkey: new PublicKey(pubkey),
+    isWritable: true,
+    isSigner,
+  };
+}
+
+/**
+ * Factory to create AccountMeta with `isWritable` set to `false`
+ *
+ * @param {PublicKEyInitData} pubkey - account address
+ * @param {boolean} isSigner - whether account authorized transaction
+ * @returns metadata for read-only account
+ */
+export function newReadOnlyAccountMeta(
+  pubkey: PublicKeyInitData,
+  isSigner: boolean
+): AccountMeta {
+  return {
+    pubkey: new PublicKey(pubkey),
+    isWritable: false,
+    isSigner,
+  };
+}
+
+/**
+ * Get serialized data from account
+ *
+ * @param {AccountInfo<Buffer>} info - Solana AccountInfo
+ * @returns serialized data as Buffer
+ */
+export function getAccountData(info: AccountInfo<Buffer> | null): Buffer {
+  if (info === null) {
+    throw Error("account info is null");
+  }
+  return info.data;
+}

+ 23 - 0
sdk/js/src/solana/utils/bpfLoaderUpgradeable.ts

@@ -0,0 +1,23 @@
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { deriveAddress } from "./account";
+
+export class BpfLoaderUpgradeable {
+  /**
+   * @internal
+   */
+  constructor() {}
+
+  /**
+   * Public key that identifies the SPL Token Metadata program
+   */
+  static programId: PublicKey = new PublicKey(
+    "BPFLoaderUpgradeab1e11111111111111111111111"
+  );
+}
+
+export function deriveUpgradeableProgramKey(programId: PublicKeyInitData) {
+  return deriveAddress(
+    [new PublicKey(programId).toBuffer()],
+    BpfLoaderUpgradeable.programId
+  );
+}

+ 12 - 0
sdk/js/src/solana/utils/connection.ts

@@ -0,0 +1,12 @@
+import { Provider } from "@project-serum/anchor";
+import { Connection } from "@solana/web3.js";
+
+export function createReadOnlyProvider(
+  connection?: Connection
+): Provider | undefined {
+  if (connection === undefined) {
+    return undefined;
+  }
+
+  return { connection };
+}

+ 6 - 0
sdk/js/src/solana/utils/index.ts

@@ -0,0 +1,6 @@
+export * from "./account";
+export * from "./bpfLoaderUpgradeable";
+export * from "./connection";
+export * from "./secp256k1";
+export * from "./splMetadata";
+export * from "./transaction";

+ 124 - 0
sdk/js/src/solana/utils/secp256k1.ts

@@ -0,0 +1,124 @@
+import { TransactionInstruction, Secp256k1Program } from "@solana/web3.js";
+
+export const SIGNATURE_LENGTH = 65;
+export const ETHEREUM_KEY_LENGTH = 20;
+
+/**
+ * Create {@link TransactionInstruction} for {@link Secp256k1Program}.
+ *
+ * @param {Buffer[]} signatures - 65-byte signatures (64 bytes + 1 byte recovery id)
+ * @param {Buffer[]} keys - 20-byte ethereum public keys
+ * @param {Buffer} message - 32-byte hash
+ * @returns Solana instruction for Secp256k1 program
+ */
+export function createSecp256k1Instruction(
+  signatures: Buffer[],
+  keys: Buffer[],
+  message: Buffer
+): TransactionInstruction {
+  return {
+    keys: [],
+    programId: Secp256k1Program.programId,
+    data: Secp256k1SignatureOffsets.serialize(signatures, keys, message),
+  };
+}
+
+/**
+ * Secp256k1SignatureOffsets serializer
+ *
+ * See {@link https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program} for more info.
+ */
+export class Secp256k1SignatureOffsets {
+  // https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program
+  //
+  // struct Secp256k1SignatureOffsets {
+  //     secp_signature_key_offset: u16,        // offset to [signature,recovery_id,etherum_address] of 64+1+20 bytes
+  //     secp_signature_instruction_index: u8,  // instruction index to find data
+  //     secp_pubkey_offset: u16,               // offset to [signature,recovery_id] of 64+1 bytes
+  //     secp_signature_instruction_index: u8,  // instruction index to find data
+  //     secp_message_data_offset: u16,         // offset to start of message data
+  //     secp_message_data_size: u16,           // size of message data
+  //     secp_message_instruction_index: u8,    // index of instruction data to get message data
+  // }
+  //
+  // Pseudo code of the operation:
+  //
+  // process_instruction() {
+  //     for i in 0..count {
+  //         // i'th index values referenced:
+  //         instructions = &transaction.message().instructions
+  //         signature = instructions[secp_signature_instruction_index].data[secp_signature_offset..secp_signature_offset + 64]
+  //         recovery_id = instructions[secp_signature_instruction_index].data[secp_signature_offset + 64]
+  //         ref_eth_pubkey = instructions[secp_pubkey_instruction_index].data[secp_pubkey_offset..secp_pubkey_offset + 32]
+  //         message_hash = keccak256(instructions[secp_message_instruction_index].data[secp_message_data_offset..secp_message_data_offset + secp_message_data_size])
+  //         pubkey = ecrecover(signature, recovery_id, message_hash)
+  //         eth_pubkey = keccak256(pubkey[1..])[12..]
+  //         if eth_pubkey != ref_eth_pubkey {
+  //             return Error
+  //         }
+  //     }
+  //     return Success
+  //   }
+
+  /**
+   * Serialize multiple signatures, ethereum public keys and message as Secp256k1 instruction data.
+   *
+   * @param {Buffer[]} signatures - 65-byte signatures (64 + 1 recovery id)
+   * @param {Buffer[]} keys - ethereum public keys
+   * @param {Buffer} message - 32-byte hash
+   * @returns serialized Secp256k1 instruction data
+   */
+  static serialize(signatures: Buffer[], keys: Buffer[], message: Buffer) {
+    if (signatures.length == 0) {
+      throw Error("signatures.length == 0");
+    }
+
+    if (signatures.length != keys.length) {
+      throw Error("signatures.length != keys.length");
+    }
+
+    if (message.length != 32) {
+      throw Error("message.length != 32");
+    }
+
+    const numSignatures = signatures.length;
+    const offsetSpan = 11;
+    const dataLoc = 1 + numSignatures * offsetSpan;
+
+    const dataLen = SIGNATURE_LENGTH + ETHEREUM_KEY_LENGTH; // 65 signature size + 20 eth pubkey size
+    const messageDataOffset = dataLoc + numSignatures * dataLen;
+    const messageDataSize = 32;
+    const serialized = Buffer.alloc(messageDataOffset + messageDataSize);
+
+    serialized.writeUInt8(numSignatures, 0);
+    serialized.write(message.toString("hex"), messageDataOffset, "hex");
+
+    for (let i = 0; i < numSignatures; ++i) {
+      const signature = signatures.at(i);
+      if (signature?.length != SIGNATURE_LENGTH) {
+        throw Error(`signatures[${i}].length != 65`);
+      }
+
+      const key = keys.at(i);
+      if (key?.length != ETHEREUM_KEY_LENGTH) {
+        throw Error(`keys[${i}].length != 20`);
+      }
+
+      const signatureOffset = dataLoc + dataLen * i;
+      const ethAddressOffset = signatureOffset + 65;
+
+      serialized.writeUInt16LE(signatureOffset, 1 + i * offsetSpan);
+      serialized.writeUInt8(0, 3 + i * offsetSpan);
+      serialized.writeUInt16LE(ethAddressOffset, 4 + i * offsetSpan);
+      serialized.writeUInt8(0, 6 + i * offsetSpan);
+      serialized.writeUInt16LE(messageDataOffset, 7 + i * offsetSpan);
+      serialized.writeUInt16LE(messageDataSize, 9 + i * offsetSpan);
+      serialized.writeUInt8(0, 10 + i * offsetSpan);
+
+      serialized.write(signature.toString("hex"), signatureOffset, "hex");
+      serialized.write(key.toString("hex"), ethAddressOffset, "hex");
+    }
+
+    return serialized;
+  }
+}

+ 331 - 0
sdk/js/src/solana/utils/splMetadata.ts

@@ -0,0 +1,331 @@
+import {
+  AccountMeta,
+  Commitment,
+  Connection,
+  PublicKey,
+  PublicKeyInitData,
+  SystemProgram,
+  SYSVAR_RENT_PUBKEY,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import {
+  deriveAddress,
+  getAccountData,
+  newAccountMeta,
+  newReadOnlyAccountMeta,
+} from "./account";
+
+export class Creator {
+  address: PublicKey;
+  verified: boolean;
+  share: number;
+
+  constructor(address: PublicKeyInitData, verified: boolean, share: number) {
+    this.address = new PublicKey(address);
+    this.verified = verified;
+    this.share = share;
+  }
+
+  static size: number = 34;
+
+  serialize() {
+    const serialized = Buffer.alloc(Creator.size);
+    serialized.write(this.address.toBuffer().toString("hex"), 0, "hex");
+    if (this.verified) {
+      serialized.writeUInt8(1, 32);
+    }
+    serialized.writeUInt8(this.share, 33);
+    return serialized;
+  }
+
+  static deserialize(data: Buffer): Creator {
+    const address = data.subarray(0, 32);
+    const verified = data.readUInt8(32) > 0;
+    const share = data.readUInt8(33);
+    return new Creator(address, verified, share);
+  }
+}
+
+export class Data {
+  name: string;
+  symbol: string;
+  uri: string;
+  sellerFeeBasisPoints: number;
+  creators: Creator[] | null;
+
+  constructor(
+    name: string,
+    symbol: string,
+    uri: string,
+    sellerFeeBasisPoints: number,
+    creators: Creator[] | null
+  ) {
+    this.name = name;
+    this.symbol = symbol;
+    this.uri = uri;
+    this.sellerFeeBasisPoints = sellerFeeBasisPoints;
+    this.creators = creators;
+  }
+
+  serialize() {
+    const nameLen = this.name.length;
+    const symbolLen = this.symbol.length;
+    const uriLen = this.uri.length;
+    const creators = this.creators;
+    const [creatorsLen, creatorsSize] = (() => {
+      if (creators === null) {
+        return [0, 0];
+      }
+
+      const creatorsLen = creators.length;
+      return [creatorsLen, 4 + creatorsLen * Creator.size];
+    })();
+    const serialized = Buffer.alloc(
+      15 + nameLen + symbolLen + uriLen + creatorsSize
+    );
+    serialized.writeUInt32LE(nameLen, 0);
+    serialized.write(this.name, 4);
+    serialized.writeUInt32LE(symbolLen, 4 + nameLen);
+    serialized.write(this.symbol, 8 + nameLen);
+    serialized.writeUInt32LE(uriLen, 8 + nameLen + symbolLen);
+    serialized.write(this.uri, 12 + nameLen + symbolLen);
+    serialized.writeUInt16LE(
+      this.sellerFeeBasisPoints,
+      12 + nameLen + symbolLen + uriLen
+    );
+    if (creators === null) {
+      serialized.writeUInt8(0, 14 + nameLen + symbolLen + uriLen);
+    } else {
+      serialized.writeUInt8(1, 14 + nameLen + symbolLen + uriLen);
+      serialized.writeUInt32LE(creatorsLen, 15 + nameLen + symbolLen + uriLen);
+      for (let i = 0; i < creatorsLen; ++i) {
+        const creator = creators.at(i)!;
+        const idx = 19 + nameLen + symbolLen + uriLen + i * Creator.size;
+        serialized.write(creator.serialize().toString("hex"), idx, "hex");
+      }
+    }
+    return serialized;
+  }
+
+  static deserialize(data: Buffer): Data {
+    const nameLen = data.readUInt32LE(0);
+    const name = data.subarray(4, 4 + nameLen).toString();
+    const symbolLen = data.readUInt32LE(4 + nameLen);
+    const symbol = data
+      .subarray(8 + nameLen, 8 + nameLen + symbolLen)
+      .toString();
+    const uriLen = data.readUInt32LE(8 + nameLen + symbolLen);
+    const uri = data
+      .subarray(12 + nameLen + symbolLen, 12 + nameLen + symbolLen + uriLen)
+      .toString();
+    const sellerFeeBasisPoints = data.readUInt16LE(
+      12 + nameLen + symbolLen + uriLen
+    );
+    const optionCreators = data.readUInt8(14 + nameLen + symbolLen + uriLen);
+    const creators = (() => {
+      if (optionCreators == 0) {
+        return null;
+      }
+
+      const creators: Creator[] = [];
+      const creatorsLen = data.readUInt32LE(15 + nameLen + symbolLen + uriLen);
+      for (let i = 0; i < creatorsLen; ++i) {
+        const idx = 19 + nameLen + symbolLen + uriLen + i * Creator.size;
+        creators.push(
+          Creator.deserialize(data.subarray(idx, idx + Creator.size))
+        );
+      }
+      return creators;
+    })();
+    return new Data(name, symbol, uri, sellerFeeBasisPoints, creators);
+  }
+}
+
+export class CreateMetadataAccountArgs extends Data {
+  isMutable: boolean;
+
+  constructor(
+    name: string,
+    symbol: string,
+    uri: string,
+    sellerFeeBasisPoints: number,
+    creators: Creator[] | null,
+    isMutable: boolean
+  ) {
+    super(name, symbol, uri, sellerFeeBasisPoints, creators);
+    this.isMutable = isMutable;
+  }
+
+  static serialize(
+    name: string,
+    symbol: string,
+    uri: string,
+    sellerFeeBasisPoints: number,
+    creators: Creator[] | null,
+    isMutable: boolean
+  ) {
+    return new CreateMetadataAccountArgs(
+      name,
+      symbol,
+      uri,
+      sellerFeeBasisPoints,
+      creators,
+      isMutable
+    ).serialize();
+  }
+
+  static serializeInstructionData(
+    name: string,
+    symbol: string,
+    uri: string,
+    sellerFeeBasisPoints: number,
+    creators: Creator[] | null,
+    isMutable: boolean
+  ) {
+    return Buffer.concat([
+      Buffer.alloc(1, 0),
+      CreateMetadataAccountArgs.serialize(
+        name,
+        symbol,
+        uri,
+        sellerFeeBasisPoints,
+        creators,
+        isMutable
+      ),
+    ]);
+  }
+
+  serialize() {
+    return Buffer.concat([
+      super.serialize(),
+      Buffer.alloc(1, this.isMutable ? 1 : 0),
+    ]);
+  }
+}
+
+export class SplTokenMetadataProgram {
+  /**
+   * @internal
+   */
+  constructor() {}
+
+  /**
+   * Public key that identifies the SPL Token Metadata program
+   */
+  static programId: PublicKey = new PublicKey(
+    "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
+  );
+
+  static createMetadataAccounts(
+    payer: PublicKey,
+    mint: PublicKey,
+    mintAuthority: PublicKey,
+    name: string,
+    symbol: string,
+    updateAuthority: PublicKey,
+    updateAuthorityIsSigner: boolean = false,
+    uri?: string,
+    creators?: Creator[] | null,
+    sellerFeeBasisPoints?: number,
+    isMutable: boolean = false,
+    metadataAccount: PublicKey = deriveSplTokenMetadataKey(mint)
+  ): TransactionInstruction {
+    const keys: AccountMeta[] = [
+      newAccountMeta(metadataAccount, false),
+      newReadOnlyAccountMeta(mint, false),
+      newReadOnlyAccountMeta(mintAuthority, true),
+      newReadOnlyAccountMeta(payer, true),
+      newReadOnlyAccountMeta(updateAuthority, updateAuthorityIsSigner),
+      newReadOnlyAccountMeta(SystemProgram.programId, false),
+      newReadOnlyAccountMeta(SYSVAR_RENT_PUBKEY, false),
+    ];
+    const data = CreateMetadataAccountArgs.serializeInstructionData(
+      name,
+      symbol,
+      uri === undefined ? "" : uri,
+      sellerFeeBasisPoints === undefined ? 0 : sellerFeeBasisPoints,
+      creators === undefined ? null : creators,
+      isMutable
+    );
+    return {
+      programId: SplTokenMetadataProgram.programId,
+      keys,
+      data,
+    };
+  }
+}
+
+export function deriveSplTokenMetadataKey(mint: PublicKeyInitData): PublicKey {
+  return deriveAddress(
+    [
+      Buffer.from("metadata"),
+      SplTokenMetadataProgram.programId.toBuffer(),
+      new PublicKey(mint).toBuffer(),
+    ],
+    SplTokenMetadataProgram.programId
+  );
+}
+
+export enum Key {
+  Uninitialized,
+  EditionV1,
+  MasterEditionV1,
+  ReservationListV1,
+  MetadataV1,
+  ReservationListV2,
+  MasterEditionV2,
+  EditionMarker,
+}
+
+export class Metadata {
+  key: Key;
+  updateAuthority: PublicKey;
+  mint: PublicKey;
+  data: Data;
+  primarySaleHappened: boolean;
+  isMutable: boolean;
+
+  constructor(
+    key: number,
+    updateAuthority: PublicKeyInitData,
+    mint: PublicKeyInitData,
+    data: Data,
+    primarySaleHappened: boolean,
+    isMutable: boolean
+  ) {
+    this.key = key as Key;
+    this.updateAuthority = new PublicKey(updateAuthority);
+    this.mint = new PublicKey(mint);
+    this.data = data;
+    this.primarySaleHappened = primarySaleHappened;
+    this.isMutable = isMutable;
+  }
+
+  static deserialize(data: Buffer): Metadata {
+    const key = data.readUInt8(0);
+    const updateAuthority = data.subarray(1, 33);
+    const mint = data.subarray(33, 65);
+    const meta = Data.deserialize(data.subarray(65));
+    const metaLen = meta.serialize().length;
+    const primarySaleHappened = data.readUInt8(65 + metaLen) > 0;
+    const isMutable = data.readUInt8(66 + metaLen) > 0;
+    return new Metadata(
+      key,
+      updateAuthority,
+      mint,
+      meta,
+      primarySaleHappened,
+      isMutable
+    );
+  }
+}
+
+export async function getMetadata(
+  connection: Connection,
+  mint: PublicKeyInitData,
+  commitment?: Commitment
+): Promise<Metadata> {
+  return connection
+    .getAccountInfo(deriveSplTokenMetadataKey(mint), commitment)
+    .then((info) => Metadata.deserialize(getAccountData(info)));
+}

+ 208 - 0
sdk/js/src/solana/utils/transaction.ts

@@ -0,0 +1,208 @@
+import {
+  Transaction,
+  Keypair,
+  Connection,
+  PublicKeyInitData,
+  PublicKey,
+  ConfirmOptions,
+  RpcResponseAndContext,
+  SignatureResult,
+  TransactionSignature,
+  Signer,
+} from "@solana/web3.js";
+
+/**
+ * Object that holds list of unsigned {@link Transaction}s and {@link Keypair}s
+ * required to sign for each transaction.
+ */
+export interface PreparedTransactions {
+  unsignedTransactions: Transaction[];
+  signers: Signer[];
+}
+
+export interface TransactionSignatureAndResponse {
+  signature: TransactionSignature;
+  response: RpcResponseAndContext<SignatureResult>;
+}
+
+/**
+ * Resembles WalletContextState and Anchor's NodeWallet's signTransaction function signature
+ */
+export type SignTransaction = (
+  transaction: Transaction
+) => Promise<Transaction>;
+
+/**
+ *
+ * @param payers
+ * @returns
+ */
+export function signTransactionFactory(...payers: Signer[]): SignTransaction {
+  return modifySignTransaction(
+    (transaction: Transaction) => Promise.resolve(transaction),
+    ...payers
+  );
+}
+
+export function modifySignTransaction(
+  signTransaction: SignTransaction,
+  ...payers: Signer[]
+): SignTransaction {
+  return (transaction: Transaction) => {
+    for (const payer of payers) {
+      transaction.partialSign(payer);
+    }
+    return signTransaction(transaction);
+  };
+}
+
+/**
+ * Wrapper for {@link Keypair} resembling Solana web3 browser wallet
+ */
+export class NodeWallet {
+  payer: Keypair;
+  signTransaction: SignTransaction;
+
+  constructor(payer: Keypair) {
+    this.payer = payer;
+    this.signTransaction = signTransactionFactory(this.payer);
+  }
+
+  static fromSecretKey(
+    secretKey: Uint8Array,
+    options?:
+      | {
+          skipValidation?: boolean | undefined;
+        }
+      | undefined
+  ): NodeWallet {
+    return new NodeWallet(Keypair.fromSecretKey(secretKey, options));
+  }
+
+  publicKey(): PublicKey {
+    return this.payer.publicKey;
+  }
+
+  pubkey(): PublicKey {
+    return this.publicKey();
+  }
+
+  key(): PublicKey {
+    return this.publicKey();
+  }
+
+  toString(): string {
+    return this.publicKey().toString();
+  }
+
+  keypair(): Keypair {
+    return this.payer;
+  }
+
+  signer(): Signer {
+    return this.keypair();
+  }
+}
+
+/**
+ * The transactions provided to this function should be ready to send.
+ * This function will do the following:
+ * 1. Add the {@param payer} as the feePayer and latest blockhash to the {@link Transaction}.
+ * 2. Sign using {@param signTransaction}.
+ * 3. Send raw transaction.
+ * 4. Confirm transaction.
+ */
+export async function signSendAndConfirmTransaction(
+  connection: Connection,
+  payer: PublicKeyInitData,
+  signTransaction: SignTransaction,
+  unsignedTransaction: Transaction,
+  options?: ConfirmOptions
+): Promise<TransactionSignatureAndResponse> {
+  const commitment = options?.commitment;
+  const { blockhash, lastValidBlockHeight } =
+    await connection.getLatestBlockhash(commitment);
+  unsignedTransaction.recentBlockhash = blockhash;
+  unsignedTransaction.feePayer = new PublicKey(payer);
+
+  // Sign transaction, broadcast, and confirm
+  const signed = await signTransaction(unsignedTransaction);
+  const signature = await connection.sendRawTransaction(
+    signed.serialize(),
+    options
+  );
+  const response = await connection.confirmTransaction(
+    {
+      blockhash,
+      lastValidBlockHeight,
+      signature,
+    },
+    commitment
+  );
+  return { signature, response };
+}
+
+/**
+ * @deprecated Please use {@link signSendAndConfirmTransaction} instead, which allows
+ * retries to be configured in {@link ConfirmOptions}.
+ *
+ * The transactions provided to this function should be ready to send.
+ * This function will do the following:
+ * 1. Add the {@param payer} as the feePayer and latest blockhash to the {@link Transaction}.
+ * 2. Sign using {@param signTransaction}.
+ * 3. Send raw transaction.
+ * 4. Confirm transaction.
+ */
+export async function sendAndConfirmTransactionsWithRetry(
+  connection: Connection,
+  signTransaction: SignTransaction,
+  payer: string,
+  unsignedTransactions: Transaction[],
+  maxRetries: number = 0,
+  options?: ConfirmOptions
+): Promise<TransactionSignatureAndResponse[]> {
+  if (unsignedTransactions.length == 0) {
+    return Promise.reject("No transactions provided to send.");
+  }
+
+  const commitment = options?.commitment;
+
+  let currentRetries = 0;
+  const output: TransactionSignatureAndResponse[] = [];
+  for (const transaction of unsignedTransactions) {
+    while (currentRetries <= maxRetries) {
+      try {
+        const latest = await connection.getLatestBlockhash(commitment);
+        transaction.recentBlockhash = latest.blockhash;
+        transaction.feePayer = new PublicKey(payer);
+
+        const signed = await signTransaction(transaction).catch((e) => null);
+        if (signed === null) {
+          return Promise.reject("Failed to sign transaction.");
+        }
+
+        const signature = await connection.sendRawTransaction(
+          signed.serialize(),
+          options
+        );
+        const response = await connection.confirmTransaction(
+          {
+            signature,
+            ...latest,
+          },
+          commitment
+        );
+        output.push({ signature, response });
+        break;
+      } catch (e) {
+        console.error(e);
+        ++currentRetries;
+      }
+    }
+    if (currentRetries > maxRetries) {
+      return Promise.reject("Reached the maximum number of retries.");
+    }
+  }
+
+  return Promise.resolve(output);
+}

+ 0 - 1
sdk/js/src/solana/wasm.ts

@@ -1 +0,0 @@
-export * from "@certusone/wormhole-sdk-wasm";

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.