Explorar el Código

solana: fix token metadata program interaction (#2913)

* testing: fix pubkey caused by 0b0b9cea70ae0dd0a5275adb08c3732fd5560ee7

* solana: fix token-metadata forked dependency

* sdk/js: fix tokenMetadata and Solana IDLs

* testing: fix tests; add token-metadata dependency

---------

Co-authored-by: A5 Pickle <a5-pickle@users.noreply.github.com>
A5 Pickle hace 2 años
padre
commit
6f8c8430ac
Se han modificado 48 ficheros con 831 adiciones y 901 borrados
  1. 1 1
      .github/workflows/build.yml
  2. 1 1
      devnet/solana-devnet.yaml
  3. 14 14
      sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts
  4. 12 15
      sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts
  5. 6 9
      sdk/js/src/solana/nftBridge/instructions/transferNative.ts
  6. 5 8
      sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts
  7. 1 1
      sdk/js/src/solana/tokenBridge/accounts/wrapped.ts
  8. 2 2
      sdk/js/src/solana/tokenBridge/instructions/attestToken.ts
  9. 13 13
      sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts
  10. 1 1
      sdk/js/src/solana/utils/index.ts
  11. 0 331
      sdk/js/src/solana/utils/splMetadata.ts
  12. 17 0
      sdk/js/src/solana/utils/tokenMetadata.ts
  13. 16 12
      solana/Cargo.lock
  14. 2 1
      solana/Dockerfile
  15. BIN
      solana/external/mpl_token_metadata.so
  16. 1 3
      solana/modules/nft_bridge/program/Cargo.toml
  17. 46 1
      solana/modules/nft_bridge/program/src/accounts.rs
  18. 4 1
      solana/modules/nft_bridge/program/src/api/complete_transfer.rs
  19. 4 36
      solana/modules/nft_bridge/program/src/api/transfer.rs
  20. 2 1
      solana/modules/nft_bridge/program/src/lib.rs
  21. 21 16
      solana/modules/nft_bridge/program/tests/common.rs
  22. 1 1
      solana/modules/token_bridge/client/Cargo.toml
  23. 13 4
      solana/modules/token_bridge/client/src/main.rs
  24. 1 3
      solana/modules/token_bridge/program/Cargo.toml
  25. 46 1
      solana/modules/token_bridge/program/src/accounts.rs
  26. 2 12
      solana/modules/token_bridge/program/src/api/attest.rs
  27. 23 18
      solana/modules/token_bridge/program/src/api/create_wrapped.rs
  28. 2 0
      solana/modules/token_bridge/program/src/lib.rs
  29. 21 16
      solana/modules/token_bridge/program/tests/common.rs
  30. 0 20
      solana/modules/token_bridge/token-metadata/Cargo.toml
  31. 0 8
      solana/modules/token_bridge/token-metadata/README.md
  32. 0 2
      solana/modules/token_bridge/token-metadata/Xargo.toml
  33. BIN
      solana/modules/token_bridge/token-metadata/spl_token_metadata.so
  34. 0 138
      solana/modules/token_bridge/token-metadata/src/instruction.rs
  35. 0 7
      solana/modules/token_bridge/token-metadata/src/lib.rs
  36. 0 124
      solana/modules/token_bridge/token-metadata/src/state.rs
  37. 0 16
      solana/modules/token_bridge/token-metadata/src/utils.rs
  38. 3 1
      testing/solana-test-validator/.gitignore
  39. 22 4
      testing/solana-test-validator/Makefile
  40. 1 0
      testing/solana-test-validator/package.json
  41. 3 2
      testing/solana-test-validator/run_sdk_tests.sh
  42. 3 3
      testing/solana-test-validator/sdk-tests/0_deploy_and_upgrade.ts
  43. 285 7
      testing/solana-test-validator/sdk-tests/2_token_bridge.ts
  44. 50 38
      testing/solana-test-validator/sdk-tests/3_nft_bridge.ts
  45. 13 0
      testing/solana-test-validator/sdk-tests/accounts/wrapped_deadbeef_metadata.json
  46. 13 0
      testing/solana-test-validator/sdk-tests/accounts/wrapped_deadbeef_mint.json
  47. 7 0
      testing/solana-test-validator/sdk-tests/helpers/consts.ts
  48. 153 9
      testing/solana-test-validator/yarn.lock

+ 1 - 1
.github/workflows/build.yml

@@ -177,7 +177,7 @@ jobs:
           export PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}"
 
           mkdir -p "${BPF_OUT_DIR}"
-          cp modules/token_bridge/token-metadata/spl_token_metadata.so "${BPF_OUT_DIR}"
+          cp external/mpl_token_metadata.so "${BPF_OUT_DIR}"
 
           BPF_PACKAGES=(
             bridge/program/Cargo.toml

+ 1 - 1
devnet/solana-devnet.yaml

@@ -52,7 +52,7 @@ spec:
             - /opt/solana/deps/cpi_poster.so
             - --bpf-program
             - metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
-            - /opt/solana/deps/spl_token_metadata.so
+            - /opt/solana/deps/mpl_token_metadata.so
             - --bpf-program
             - Ex9bCdVMSfx7EzB3pgSi2R4UHwJAXvTw18rBQm5YQ8gK
             - /opt/solana/deps/wormhole_migration.so

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

@@ -1,3 +1,7 @@
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
 import {
   PublicKey,
   PublicKeyInitData,
@@ -6,25 +10,21 @@ import {
   TransactionInstruction,
 } from "@solana/web3.js";
 import {
-  ASSOCIATED_TOKEN_PROGRAM_ID,
-  TOKEN_PROGRAM_ID,
-} from "@solana/spl-token";
-import { createReadOnlyNftBridgeProgramInterface } from "../program";
+  isBytes,
+  ParsedNftTransferVaa,
+  parseNftTransferVaa,
+  SignedVaa,
+} from "../../../vaa";
+import { TOKEN_METADATA_PROGRAM_ID } from "../../utils";
 import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
 import {
   deriveEndpointKey,
+  deriveMintAuthorityKey,
   deriveNftBridgeConfigKey,
-  deriveWrappedMintKey,
   deriveWrappedMetaKey,
-  deriveMintAuthorityKey,
+  deriveWrappedMintKey,
 } from "../accounts";
-import {
-  isBytes,
-  ParsedNftTransferVaa,
-  parseNftTransferVaa,
-  SignedVaa,
-} from "../../../vaa";
-import { SplTokenMetadataProgram } from "../../utils";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
 
 export function createCompleteTransferWrappedInstruction(
   nftBridgeProgramId: PublicKeyInitData,
@@ -110,7 +110,7 @@ export function getCompleteTransferWrappedAccounts(
     rent: SYSVAR_RENT_PUBKEY,
     systemProgram: SystemProgram.programId,
     tokenProgram: TOKEN_PROGRAM_ID,
-    splMetadataProgram: SplTokenMetadataProgram.programId,
+    splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
     associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
     wormholeProgram: new PublicKey(wormholeProgramId),
   };

+ 12 - 15
sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts

@@ -1,3 +1,4 @@
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import {
   PublicKey,
   PublicKeyInitData,
@@ -5,26 +6,22 @@ import {
   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 { deriveTokenMetadataKey, TOKEN_METADATA_PROGRAM_ID } from "../../utils";
+import { derivePostedVaaKey } from "../../wormhole";
 import {
-  deriveSplTokenMetadataKey,
-  SplTokenMetadataProgram,
-} from "../../utils";
+  deriveEndpointKey,
+  deriveMintAuthorityKey,
+  deriveNftBridgeConfigKey,
+  deriveWrappedMetaKey,
+  deriveWrappedMintKey,
+} from "../accounts";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
 
 export function createCompleteWrappedMetaInstruction(
   nftBridgeProgramId: PublicKeyInitData,
@@ -92,12 +89,12 @@ export function getCompleteWrappedMetaAccounts(
     ),
     mint,
     wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
-    splMetadata: deriveSplTokenMetadataKey(mint),
+    splMetadata: deriveTokenMetadataKey(mint),
     mintAuthority: deriveMintAuthorityKey(nftBridgeProgramId),
     rent: SYSVAR_RENT_PUBKEY,
     systemProgram: SystemProgram.programId,
     tokenProgram: TOKEN_PROGRAM_ID,
-    splMetadataProgram: SplTokenMetadataProgram.programId,
+    splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
     wormholeProgram: new PublicKey(wormholeProgramId),
   };
 }

+ 6 - 9
sdk/js/src/solana/nftBridge/instructions/transferNative.ts

@@ -1,21 +1,18 @@
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import {
   PublicKey,
   PublicKeyInitData,
   TransactionInstruction,
 } from "@solana/web3.js";
-import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
-import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { TOKEN_METADATA_PROGRAM_ID, deriveTokenMetadataKey } from "../../utils";
 import { getPostMessageAccounts } from "../../wormhole";
 import {
   deriveAuthoritySignerKey,
+  deriveCustodyKey,
   deriveCustodySignerKey,
   deriveNftBridgeConfigKey,
-  deriveCustodyKey,
 } from "../accounts";
-import {
-  deriveSplTokenMetadataKey,
-  SplTokenMetadataProgram,
-} from "../../utils";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
 
 export function createTransferNativeInstruction(
   nftBridgeProgramId: PublicKeyInitData,
@@ -103,7 +100,7 @@ export function getTransferNativeAccounts(
     config: deriveNftBridgeConfigKey(nftBridgeProgramId),
     from: new PublicKey(from),
     mint: new PublicKey(mint),
-    splMetadata: deriveSplTokenMetadataKey(mint),
+    splMetadata: deriveTokenMetadataKey(mint),
     custody: deriveCustodyKey(nftBridgeProgramId, mint),
     authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId),
     custodySigner: deriveCustodySignerKey(nftBridgeProgramId),
@@ -116,7 +113,7 @@ export function getTransferNativeAccounts(
     rent,
     systemProgram,
     tokenProgram: TOKEN_PROGRAM_ID,
-    splMetadataProgram: SplTokenMetadataProgram.programId,
+    splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
     wormholeProgram: new PublicKey(wormholeProgramId),
   };
 }

+ 5 - 8
sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts

@@ -1,10 +1,10 @@
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import {
   PublicKey,
   PublicKeyInitData,
   TransactionInstruction,
 } from "@solana/web3.js";
-import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
-import { createReadOnlyNftBridgeProgramInterface } from "../program";
+import { TOKEN_METADATA_PROGRAM_ID, deriveTokenMetadataKey } from "../../utils";
 import { getPostMessageAccounts } from "../../wormhole";
 import {
   deriveAuthoritySignerKey,
@@ -12,10 +12,7 @@ import {
   deriveWrappedMetaKey,
   deriveWrappedMintKey,
 } from "../accounts";
-import {
-  deriveSplTokenMetadataKey,
-  SplTokenMetadataProgram,
-} from "../../utils";
+import { createReadOnlyNftBridgeProgramInterface } from "../program";
 
 export function createTransferWrappedInstruction(
   nftBridgeProgramId: PublicKeyInitData,
@@ -120,7 +117,7 @@ export function getTransferWrappedAccounts(
     fromOwner: new PublicKey(fromOwner),
     mint,
     wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
-    splMetadata: deriveSplTokenMetadataKey(mint),
+    splMetadata: deriveTokenMetadataKey(mint),
     authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId),
     wormholeBridge,
     wormholeMessage,
@@ -131,7 +128,7 @@ export function getTransferWrappedAccounts(
     rent,
     systemProgram,
     tokenProgram: TOKEN_PROGRAM_ID,
-    splMetadataProgram: SplTokenMetadataProgram.programId,
+    splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
     wormholeProgram: new PublicKey(wormholeProgramId),
   };
 }

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

@@ -11,7 +11,7 @@ import {
 } from "../../../utils";
 import { deriveAddress, getAccountData } from "../../utils";
 
-export { deriveSplTokenMetadataKey } from "../../utils/splMetadata";
+export { deriveTokenMetadataKey } from "../../utils/tokenMetadata";
 
 export function deriveWrappedMintKey(
   tokenBridgeProgramId: PublicKeyInitData,

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

@@ -6,7 +6,7 @@ import {
 import { createReadOnlyTokenBridgeProgramInterface } from "../program";
 import { getPostMessageAccounts } from "../../wormhole";
 import {
-  deriveSplTokenMetadataKey,
+  deriveTokenMetadataKey,
   deriveTokenBridgeConfigKey,
   deriveWrappedMetaKey,
 } from "../accounts";
@@ -83,7 +83,7 @@ export function getAttestTokenAccounts(
     config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
     mint: new PublicKey(mint),
     wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
-    splMetadata: deriveSplTokenMetadataKey(mint),
+    splMetadata: deriveTokenMetadataKey(mint),
     wormholeBridge,
     wormholeMessage: new PublicKey(message),
     wormholeEmitter,

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

@@ -1,3 +1,4 @@
+import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import {
   PublicKey,
   PublicKeyInitData,
@@ -5,24 +6,23 @@ import {
   SYSVAR_RENT_PUBKEY,
   TransactionInstruction,
 } from "@solana/web3.js";
-import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
-import { createReadOnlyTokenBridgeProgramInterface } from "../program";
+import {
+  isBytes,
+  parseAttestMetaVaa,
+  ParsedAttestMetaVaa,
+  SignedVaa,
+} from "../../../vaa";
+import { TOKEN_METADATA_PROGRAM_ID } from "../../utils";
 import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
 import {
   deriveEndpointKey,
   deriveMintAuthorityKey,
-  deriveSplTokenMetadataKey,
-  deriveWrappedMetaKey,
   deriveTokenBridgeConfigKey,
+  deriveTokenMetadataKey,
+  deriveWrappedMetaKey,
   deriveWrappedMintKey,
 } from "../accounts";
-import {
-  isBytes,
-  parseAttestMetaVaa,
-  ParsedAttestMetaVaa,
-  SignedVaa,
-} from "../../../vaa";
-import { SplTokenMetadataProgram } from "../../utils";
+import { createReadOnlyTokenBridgeProgramInterface } from "../program";
 
 export function createCreateWrappedInstruction(
   tokenBridgeProgramId: PublicKeyInitData,
@@ -96,12 +96,12 @@ export function getCreateWrappedAccounts(
     ),
     mint,
     wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
-    splMetadata: deriveSplTokenMetadataKey(mint),
+    splMetadata: deriveTokenMetadataKey(mint),
     mintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId),
     rent: SYSVAR_RENT_PUBKEY,
     systemProgram: SystemProgram.programId,
     tokenProgram: TOKEN_PROGRAM_ID,
-    splMetadataProgram: SplTokenMetadataProgram.programId,
+    splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
     wormholeProgram: new PublicKey(wormholeProgramId),
   };
 }

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

@@ -2,5 +2,5 @@ export * from "./account";
 export * from "./bpfLoaderUpgradeable";
 export * from "./connection";
 export * from "./secp256k1";
-export * from "./splMetadata";
+export * from "./tokenMetadata";
 export * from "./transaction";

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

@@ -1,331 +0,0 @@
-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)));
-}

+ 17 - 0
sdk/js/src/solana/utils/tokenMetadata.ts

@@ -0,0 +1,17 @@
+import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
+import { deriveAddress } from "./account";
+
+export const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
+  "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
+);
+
+export function deriveTokenMetadataKey(mint: PublicKeyInitData): PublicKey {
+  return deriveAddress(
+    [
+      Buffer.from("metadata"),
+      TOKEN_METADATA_PROGRAM_ID.toBuffer(),
+      new PublicKey(mint).toBuffer(),
+    ],
+    TOKEN_METADATA_PROGRAM_ID
+  );
+}

+ 16 - 12
solana/Cargo.lock

@@ -1847,6 +1847,19 @@ dependencies = [
  "syn 1.0.91",
 ]
 
+[[package]]
+name = "mpl-token-metadata"
+version = "0.0.1"
+source = "git+https://github.com/wormhole-foundation/metaplex-program-library?rev=a7ab32ab0defd89c98f205c80ebdaf77ed60152d#a7ab32ab0defd89c98f205c80ebdaf77ed60152d"
+dependencies = [
+ "borsh",
+ "num-derive",
+ "num-traits",
+ "solana-program",
+ "spl-token",
+ "thiserror",
+]
+
 [[package]]
 name = "nft-bridge"
 version = "0.1.0"
@@ -1857,6 +1870,7 @@ dependencies = [
  "hex",
  "hex-literal",
  "libsecp256k1",
+ "mpl-token-metadata",
  "primitive-types",
  "rand 0.7.3",
  "rocksalt",
@@ -1868,7 +1882,6 @@ dependencies = [
  "solitaire",
  "spl-associated-token-account",
  "spl-token",
- "spl-token-metadata",
  "wasm-bindgen",
  "wormhole-bridge-solana",
 ]
@@ -3985,15 +3998,6 @@ dependencies = [
  "thiserror",
 ]
 
-[[package]]
-name = "spl-token-metadata"
-version = "0.0.1"
-dependencies = [
- "borsh",
- "solana-program",
- "spl-token",
-]
-
 [[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
@@ -4269,6 +4273,7 @@ dependencies = [
  "hex",
  "hex-literal",
  "libsecp256k1",
+ "mpl-token-metadata",
  "primitive-types",
  "rand 0.7.3",
  "rocksalt",
@@ -4279,7 +4284,6 @@ dependencies = [
  "solana-sdk",
  "solitaire",
  "spl-token",
- "spl-token-metadata",
  "wasm-bindgen",
  "wormhole-bridge-solana",
 ]
@@ -4292,6 +4296,7 @@ dependencies = [
  "borsh",
  "clap",
  "hex",
+ "mpl-token-metadata",
  "rand 0.7.3",
  "shellexpand",
  "solana-clap-utils",
@@ -4300,7 +4305,6 @@ dependencies = [
  "solana-program",
  "solana-sdk",
  "solitaire",
- "spl-token-metadata",
  "token-bridge",
 ]
 

+ 2 - 1
solana/Dockerfile

@@ -15,6 +15,7 @@ COPY migration migration
 COPY Cargo.toml Cargo.toml
 COPY Cargo.lock Cargo.lock
 COPY solitaire solitaire
+COPY external external
 
 ENV RUST_LOG="solana_runtime::system_instruction_processor=trace,solana_runtime::message_processor=trace,solana_bpf_loader=debug,solana_rbpf=debug"
 ENV RUST_BACKTRACE=1
@@ -40,7 +41,7 @@ RUN --mount=type=cache,target=target,id=build \
     cp target/deploy/wormhole_migration.so /opt/solana/deps/wormhole_migration.so && \
     cp target/deploy/token_bridge.so /opt/solana/deps/token_bridge.so && \
     cp target/deploy/nft_bridge.so /opt/solana/deps/nft_bridge.so && \
-    cp modules/token_bridge/token-metadata/spl_token_metadata.so /opt/solana/deps/spl_token_metadata.so
+    cp external/mpl_token_metadata.so /opt/solana/deps/mpl_token_metadata.so
 
 FROM scratch AS export-stage
 COPY --from=builder /opt/solana/deps /

BIN
solana/external/mpl_token_metadata.so


+ 1 - 3
solana/modules/nft_bridge/program/Cargo.toml

@@ -29,7 +29,7 @@ solana-program = "*"
 spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
 spl-associated-token-account = { version = "1.0.2", features = ["no-entrypoint"] }
 primitive-types = { version = "0.9.0", default-features = false }
-spl-token-metadata = { path = "../../token_bridge/token-metadata" }
+spl-token-metadata = { git = "https://github.com/wormhole-foundation/metaplex-program-library", rev = "a7ab32ab0defd89c98f205c80ebdaf77ed60152d", package = "mpl-token-metadata" }
 wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true }
 serde = { version = "1.0", features = ["derive"] }
 
@@ -40,5 +40,3 @@ libsecp256k1 = { version = "0.6.0", features = [] }
 rand = "0.7.3"
 solana-program-test = "=1.10.31"
 solana-sdk = "=1.10.31"
-spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
-spl-token-metadata = { path = "../../token_bridge/token-metadata" }

+ 46 - 1
solana/modules/nft_bridge/program/src/accounts.rs

@@ -1,4 +1,7 @@
-use crate::types::*;
+use crate::{
+    types::*,
+    TokenBridgeError,
+};
 use bridge::{
     accounts::BridgeData,
     api::ForeignAddress,
@@ -9,6 +12,7 @@ use solitaire::{
     processors::seeded::Seeded,
     *,
 };
+use spl_token_metadata::state::Key::MetadataV1;
 
 pub type AuthoritySigner<'b> = Derive<Info<'b>, "authority_signer">;
 pub type CustodySigner<'b> = Derive<Info<'b>, "custody_signer">;
@@ -106,3 +110,44 @@ impl<'b> Seeded<&SplTokenMetaDerivationData> for SplTokenMeta<'b> {
         ]
     }
 }
+
+/// This method removes code duplication when checking token metadata. When metadata is read for
+/// attestation and transfers, Token Bridge does not invoke Metaplex's Token Metadata program, so
+/// it must validate the account the same way Token Metadata program does to ensure the correct
+/// account is passed into Token Bridge's instruction context.
+pub fn deserialize_and_verify_metadata(
+    info: &Info,
+    derivation_data: SplTokenMetaDerivationData,
+) -> Result<spl_token_metadata::state::Metadata> {
+    // Verify pda.
+    info.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
+
+    // There must be account data for token's metadata.
+    if info.data_is_empty() {
+        return Err(TokenBridgeError::NonexistentTokenMetadataAccount.into());
+    }
+
+    // Account must belong to Metaplex Token Metadata program.
+    if *info.owner != spl_token_metadata::id() {
+        return Err(TokenBridgeError::WrongAccountOwner.into());
+    }
+
+    // Account must be the expected Metadata length.
+    if info.data_len() != spl_token_metadata::state::MAX_METADATA_LEN {
+        return Err(TokenBridgeError::InvalidMetadata.into());
+    }
+
+    let mut data: &[u8] = &info.data.borrow_mut();
+
+    // Unfortunately we cannot use `map_err` easily, so we will match certain deserialization conditions.
+    match spl_token_metadata::utils::meta_deser_unchecked(&mut data) {
+        Ok(deserialized) => {
+            if deserialized.key == MetadataV1 {
+                Ok(deserialized)
+            } else {
+                Err(TokenBridgeError::NotMetadataV1Account.into())
+            }
+        }
+        _ => Err(TokenBridgeError::InvalidMetadata.into()),
+    }
+}

+ 4 - 1
solana/modules/nft_bridge/program/src/api/complete_transfer.rs

@@ -382,7 +382,7 @@ pub fn complete_wrapped_meta(
     symbol.retain(|&c| c != '\u{FFFD}');
     let symbol: String = symbol.iter().collect();
 
-    let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts(
+    let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts_v3(
         spl_token_metadata::id(),
         *accs.spl_metadata.key,
         *accs.mint.info().key,
@@ -396,6 +396,9 @@ pub fn complete_wrapped_meta(
         0,
         false,
         true,
+        None,
+        None,
+        None,
     );
     invoke_seeded(&spl_token_metadata_ix, ctx, &accs.mint_authority, None)?;
 

+ 4 - 36
solana/modules/nft_bridge/program/src/api/transfer.rs

@@ -1,5 +1,6 @@
 use crate::{
     accounts::{
+        deserialize_and_verify_metadata,
         AuthoritySigner,
         ConfigAccount,
         CoreBridge,
@@ -17,11 +18,7 @@ use crate::{
     messages::PayloadTransfer,
     types::*,
     TokenBridgeError,
-    TokenBridgeError::{
-        InvalidMetadata,
-        TokenNotNFT,
-        WrongAccountOwner,
-    },
+    TokenBridgeError::WrongAccountOwner,
 };
 use bridge::{
     api::PostMessageData,
@@ -50,7 +47,6 @@ use solitaire::{
     CreationLamports::Exempt,
     *,
 };
-use spl_token_metadata::state::Metadata;
 
 #[derive(FromAccounts)]
 pub struct TransferNative<'b> {
@@ -123,24 +119,11 @@ pub fn transfer_native(
     accs.custody
         .verify_derivation(ctx.program_id, &derivation_data)?;
 
-    let derivation_data: SplTokenMetaDerivationData = (&*accs).into();
-    accs.spl_metadata
-        .verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
-
     // Verify mints
     if accs.from.mint != *accs.mint.info().key {
         return Err(TokenBridgeError::InvalidMint.into());
     }
 
-    // Token must have metadata
-    if accs.spl_metadata.data_is_empty() {
-        return Err(TokenNotNFT.into());
-    }
-
-    if *accs.spl_metadata.owner != spl_token_metadata::id() {
-        return Err(WrongAccountOwner.into());
-    }
-
     // Verify that the token is not a wrapped token
     if let COption::Some(mint_authority) = accs.mint.mint_authority {
         if mint_authority == MintSigner::key(None, ctx.program_id) {
@@ -180,8 +163,7 @@ pub fn transfer_native(
     );
     invoke(&transfer_ix, ctx.accounts)?;
 
-    let metadata: Metadata =
-        Metadata::from_account_info(accs.spl_metadata.info()).ok_or(InvalidMetadata)?;
+    let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
 
     // Post message
     // Given there is no tokenID equivalent on Solana and each distinct token address is translated
@@ -326,21 +308,7 @@ pub fn transfer_wrapped(
     accs.wrapped_meta
         .verify_derivation(ctx.program_id, &derivation_data)?;
 
-    // Token must have metadata
-    if accs.spl_metadata.data_is_empty() {
-        return Err(TokenNotNFT.into());
-    }
-
-    let derivation_data: SplTokenMetaDerivationData = (&*accs).into();
-    accs.spl_metadata
-        .verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
-
-    if *accs.spl_metadata.owner != spl_token_metadata::id() {
-        return Err(WrongAccountOwner.into());
-    }
-
-    let metadata: Metadata =
-        Metadata::from_account_info(accs.spl_metadata.info()).ok_or(InvalidMetadata)?;
+    let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
 
     // Post message
     let payload = PayloadTransfer {

+ 2 - 1
solana/modules/nft_bridge/program/src/lib.rs

@@ -59,9 +59,10 @@ pub enum TokenBridgeError {
     TokenNotNative,
     UninitializedMint,
     WrongAccountOwner,
-    TokenNotNFT,
+    NonexistentTokenMetadataAccount,
     InvalidAssociatedAccount,
     InvalidRecipient,
+    NotMetadataV1Account,
 }
 
 impl From<TokenBridgeError> for SolitaireError {

+ 21 - 16
solana/modules/nft_bridge/program/tests/common.rs

@@ -128,7 +128,7 @@ mod helpers {
         );
 
         let mut builder = ProgramTest::new("bridge", program, processor!(bridge::solitaire));
-        builder.add_program("spl_token_metadata", spl_token_metadata::id(), None);
+        builder.add_program("mpl_token_metadata", spl_token_metadata::id(), None);
         builder.add_program(
             "nft_bridge",
             token_program,
@@ -506,21 +506,26 @@ mod helpers {
             client,
             payer,
             &[payer, mint_authority],
-            &[spl_token_metadata::instruction::create_metadata_accounts(
-                spl_token_metadata::id(),
-                metadata_account,
-                mint,
-                mint_authority.pubkey(),
-                payer.pubkey(),
-                update_authority,
-                name,
-                symbol,
-                uri,
-                None,
-                0,
-                false,
-                false,
-            )],
+            &[
+                spl_token_metadata::instruction::create_metadata_accounts_v3(
+                    spl_token_metadata::id(),
+                    metadata_account,
+                    mint,
+                    mint_authority.pubkey(),
+                    payer.pubkey(),
+                    update_authority,
+                    name,
+                    symbol,
+                    uri,
+                    None,
+                    0,
+                    false,
+                    false,
+                    None,
+                    None,
+                    None,
+                ),
+            ],
             CommitmentLevel::Processed,
         )
         .await

+ 1 - 1
solana/modules/token_bridge/client/Cargo.toml

@@ -17,4 +17,4 @@ solana-cli-config = "=1.10.31"
 solitaire = { path = "../../../solitaire/program" }
 solana-clap-utils = "=1.10.31"
 hex = "0.4.3"
-spl-token-metadata = { path = "../token-metadata" }
+spl-token-metadata = { git = "https://github.com/wormhole-foundation/metaplex-program-library", rev = "a7ab32ab0defd89c98f205c80ebdaf77ed60152d", package = "mpl-token-metadata" }

+ 13 - 4
solana/modules/token_bridge/client/src/main.rs

@@ -107,7 +107,7 @@ fn command_create_meta(
     )
     .0;
     println!("Meta account: {}", meta_acc);
-    let ix = spl_token_metadata::instruction::create_metadata_accounts(
+    let ix = spl_token_metadata::instruction::create_metadata_accounts_v3(
         spl_token_metadata::id(),
         meta_acc,
         *mint,
@@ -121,6 +121,9 @@ fn command_create_meta(
         0,
         false,
         false,
+        None,
+        None,
+        None,
     );
     let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
 
@@ -334,9 +337,15 @@ fn main() {
                 &spl_token_metadata::id(),
             )
             .0;
-            let meta_info = config.rpc_client.get_account(&meta_acc).unwrap();
-            let meta_info =
-                spl_token_metadata::state::Metadata::from_bytes(&meta_info.data).unwrap();
+            let meta_info = spl_token_metadata::utils::meta_deser_unchecked(
+                &mut config
+                    .rpc_client
+                    .get_account(&meta_acc)
+                    .unwrap()
+                    .data
+                    .as_slice(),
+            )
+            .unwrap();
             println!("Key: {:?}", meta_info.key);
             println!("Mint: {}", meta_info.mint);
             println!("Metadata Key: {}", meta_acc);

+ 1 - 3
solana/modules/token_bridge/program/Cargo.toml

@@ -28,7 +28,7 @@ sha3 = "0.9.1"
 solana-program = "*"
 spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
 primitive-types = { version = "0.9.0", default-features = false }
-spl-token-metadata = { path = "../token-metadata" }
+spl-token-metadata = { git = "https://github.com/wormhole-foundation/metaplex-program-library", rev = "a7ab32ab0defd89c98f205c80ebdaf77ed60152d", package = "mpl-token-metadata" }
 wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true }
 serde = { version = "1.0", features = ["derive"] }
 
@@ -39,5 +39,3 @@ libsecp256k1 = { version = "0.6.0", features = [] }
 rand = "0.7.3"
 solana-program-test = "=1.10.31"
 solana-sdk = "=1.10.31"
-spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
-spl-token-metadata = { path = "../token-metadata" }

+ 46 - 1
solana/modules/token_bridge/program/src/accounts.rs

@@ -1,4 +1,7 @@
-use crate::types::*;
+use crate::{
+    types::*,
+    TokenBridgeError,
+};
 use bridge::{
     accounts::BridgeData,
     api::ForeignAddress,
@@ -8,6 +11,7 @@ use solitaire::{
     processors::seeded::Seeded,
     *,
 };
+use spl_token_metadata::state::Key::MetadataV1;
 
 pub type AuthoritySigner<'b> = Derive<Info<'b>, "authority_signer">;
 pub type CustodySigner<'b> = Derive<Info<'b>, "custody_signer">;
@@ -101,3 +105,44 @@ impl<'b> Seeded<&SplTokenMetaDerivationData> for SplTokenMeta<'b> {
         ]
     }
 }
+
+/// This method removes code duplication when checking token metadata. When metadata is read for
+/// attestation and transfers, Token Bridge does not invoke Metaplex's Token Metadata program, so
+/// it must validate the account the same way Token Metadata program does to ensure the correct
+/// account is passed into Token Bridge's instruction context.
+pub fn deserialize_and_verify_metadata(
+    info: &Info,
+    derivation_data: SplTokenMetaDerivationData,
+) -> Result<spl_token_metadata::state::Metadata> {
+    // Verify pda.
+    info.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
+
+    // There must be account data for token's metadata.
+    if info.data_is_empty() {
+        return Err(TokenBridgeError::NonexistentTokenMetadataAccount.into());
+    }
+
+    // Account must belong to Metaplex Token Metadata program.
+    if *info.owner != spl_token_metadata::id() {
+        return Err(TokenBridgeError::WrongAccountOwner.into());
+    }
+
+    // Account must be the expected Metadata length.
+    if info.data_len() != spl_token_metadata::state::MAX_METADATA_LEN {
+        return Err(TokenBridgeError::InvalidMetadata.into());
+    }
+
+    let mut data: &[u8] = &info.data.borrow_mut();
+
+    // Unfortunately we cannot use `map_err` easily, so we will match certain deserialization conditions.
+    match spl_token_metadata::utils::meta_deser_unchecked(&mut data) {
+        Ok(deserialized) => {
+            if deserialized.key == MetadataV1 {
+                Ok(deserialized)
+            } else {
+                Err(TokenBridgeError::NotMetadataV1Account.into())
+            }
+        }
+        _ => Err(TokenBridgeError::InvalidMetadata.into()),
+    }
+}

+ 2 - 12
solana/modules/token_bridge/program/src/api/attest.rs

@@ -1,5 +1,6 @@
 use crate::{
     accounts::{
+        deserialize_and_verify_metadata,
         ConfigAccount,
         CoreBridge,
         EmitterAccount,
@@ -10,7 +11,6 @@ use crate::{
     },
     messages::PayloadAssetMeta,
     types::*,
-    TokenBridgeError::*,
 };
 use bridge::{
     api::PostMessageData,
@@ -34,7 +34,6 @@ use solitaire::{
     },
     *,
 };
-use spl_token_metadata::state::Metadata;
 
 #[derive(FromAccounts)]
 pub struct AttestToken<'b> {
@@ -118,16 +117,7 @@ pub fn attest_token(
 
     // Assign metadata if an SPL Metadata account exists for the SPL token in question.
     if !accs.spl_metadata.data_is_empty() {
-        let derivation_data: SplTokenMetaDerivationData = (&*accs).into();
-        accs.spl_metadata
-            .verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
-
-        if *accs.spl_metadata.owner != spl_token_metadata::id() {
-            return Err(WrongAccountOwner.into());
-        }
-
-        let metadata: Metadata =
-            Metadata::from_account_info(accs.spl_metadata.info()).ok_or(InvalidMetadata)?;
+        let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
         payload.name = metadata.data.name.clone();
         payload.symbol = metadata.data.symbol;
     }

+ 23 - 18
solana/modules/token_bridge/program/src/api/create_wrapped.rs

@@ -1,5 +1,6 @@
 use crate::{
     accounts::{
+        deserialize_and_verify_metadata,
         ConfigAccount,
         Endpoint,
         EndpointDerivationData,
@@ -14,7 +15,6 @@ use crate::{
     messages::PayloadAssetMeta,
     TokenBridgeError::{
         InvalidChain,
-        InvalidMetadata,
         InvalidVAA,
     },
     INVALID_VAAS,
@@ -40,10 +40,6 @@ use solitaire::{
     *,
 };
 
-use spl_token_metadata::state::{
-    Data as SplData,
-    Metadata,
-};
 use std::cmp::min;
 
 #[derive(FromAccounts)]
@@ -164,7 +160,7 @@ pub fn create_accounts(
     let name = truncate_utf8(&accs.vaa.name, 32 - 11) + " (Wormhole)";
     let symbol = truncate_utf8(&accs.vaa.symbol, 10);
 
-    let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts(
+    let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts_v3(
         spl_token_metadata::id(),
         *accs.spl_metadata.key,
         *accs.mint.info().key,
@@ -178,6 +174,9 @@ pub fn create_accounts(
         0,
         false,
         true,
+        None,
+        None,
+        None,
     );
     invoke_seeded(&spl_token_metadata_ix, ctx, &accs.mint_authority, None)?;
 
@@ -194,28 +193,34 @@ pub fn update_accounts(
     accs: &mut CreateWrapped,
     _data: CreateWrappedData,
 ) -> Result<()> {
-    accs.spl_metadata.verify_derivation(
-        &spl_token_metadata::id(),
-        &SplTokenMetaDerivationData {
+    // Checks in this method are redundant with what occurs in `update_metadata_accounts_v2`, but we want to make
+    // sure that the account we are deserializing is legitimate.
+    let metadata = deserialize_and_verify_metadata(
+        &accs.spl_metadata,
+        SplTokenMetaDerivationData {
             mint: *accs.mint.info().key,
         },
     )?;
 
-    let mut metadata: SplData = Metadata::from_account_info(accs.spl_metadata.info())
-        .ok_or(InvalidMetadata)?
-        .data;
-
-    // Normalize token metadata.
-    metadata.name = truncate_utf8(&accs.vaa.name, 32 - 11) + " (Wormhole)";
-    metadata.symbol = truncate_utf8(&accs.vaa.symbol, 10);
+    // Normalize token metadata's name and symbol.
+    let new_data_v2 = spl_token_metadata::state::DataV2 {
+        name: truncate_utf8(&accs.vaa.name, 32 - 11) + " (Wormhole)",
+        symbol: truncate_utf8(&accs.vaa.symbol, 10),
+        uri: metadata.data.uri,
+        seller_fee_basis_points: metadata.data.seller_fee_basis_points,
+        creators: metadata.data.creators,
+        collection: metadata.collection,
+        uses: metadata.uses,
+    };
 
     // Update SPL Metadata
-    let spl_token_metadata_ix = spl_token_metadata::instruction::update_metadata_accounts(
+    let spl_token_metadata_ix = spl_token_metadata::instruction::update_metadata_accounts_v2(
         spl_token_metadata::id(),
         *accs.spl_metadata.key,
         *accs.mint_authority.info().key,
         None,
-        Some(metadata),
+        Some(new_data_v2),
+        None,
         None,
     );
     invoke_seeded(&spl_token_metadata_ix, ctx, &accs.mint_authority, None)?;

+ 2 - 0
solana/modules/token_bridge/program/src/lib.rs

@@ -89,6 +89,8 @@ pub enum TokenBridgeError {
     InvalidFee,
     InvalidRecipient,
     InvalidVAA,
+    NonexistentTokenMetadataAccount,
+    NotMetadataV1Account,
 }
 
 impl From<TokenBridgeError> for SolitaireError {

+ 21 - 16
solana/modules/token_bridge/program/tests/common.rs

@@ -131,7 +131,7 @@ mod helpers {
         );
 
         let mut builder = ProgramTest::new("bridge", program, processor!(bridge::solitaire));
-        builder.add_program("spl_token_metadata", spl_token_metadata::id(), None);
+        builder.add_program("mpl_token_metadata", spl_token_metadata::id(), None);
         builder.add_program(
             "token_bridge",
             token_program,
@@ -618,21 +618,26 @@ mod helpers {
             client,
             payer,
             &[payer, mint_authority],
-            &[spl_token_metadata::instruction::create_metadata_accounts(
-                spl_token_metadata::id(),
-                metadata_account,
-                mint.pubkey(),
-                mint_authority.pubkey(),
-                payer.pubkey(),
-                update_authority,
-                name,
-                symbol,
-                "https://token.org".to_string(),
-                None,
-                0,
-                false,
-                false,
-            )],
+            &[
+                spl_token_metadata::instruction::create_metadata_accounts_v3(
+                    spl_token_metadata::id(),
+                    metadata_account,
+                    mint.pubkey(),
+                    mint_authority.pubkey(),
+                    payer.pubkey(),
+                    update_authority,
+                    name,
+                    symbol,
+                    "https://token.org".to_string(),
+                    None,
+                    0,
+                    false,
+                    false,
+                    None,
+                    None,
+                    None,
+                ),
+            ],
             CommitmentLevel::Processed,
         )
         .await

+ 0 - 20
solana/modules/token_bridge/token-metadata/Cargo.toml

@@ -1,20 +0,0 @@
-[package]
-name = "spl-token-metadata"
-version = "0.0.1"
-description = "Metaplex Metadata"
-authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
-repository = "https://github.com/metaplex-foundation/metaplex"
-license = "Apache-2.0"
-edition = "2018"
-
-[lib]
-crate-type = ["cdylib", "lib"]
-
-[features]
-no-entrypoint = []
-test-bpf = []
-
-[dependencies]
-borsh = "=0.9.3"
-solana-program = "=1.10.31"
-spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }

+ 0 - 8
solana/modules/token_bridge/token-metadata/README.md

@@ -1,8 +0,0 @@
----
-title: Token Metadata Program
----
-
-Fork of the SPL Token Metadata program from the Metaplex repository, this is
-temporary until there are versioned releases we can compile against. Currently
-the upstream version depends on a version of solana with conflicting versions
-of borsh.

+ 0 - 2
solana/modules/token_bridge/token-metadata/Xargo.toml

@@ -1,2 +0,0 @@
-[target.bpfel-unknown-unknown.dependencies.std]
-features = []

BIN
solana/modules/token_bridge/token-metadata/spl_token_metadata.so


+ 0 - 138
solana/modules/token_bridge/token-metadata/src/instruction.rs

@@ -1,138 +0,0 @@
-use crate::state::{
-    Creator,
-    Data,
-    EDITION,
-    EDITION_MARKER_BIT_SIZE,
-    PREFIX,
-};
-use borsh::{
-    BorshDeserialize,
-    BorshSerialize,
-};
-use solana_program::{
-    instruction::{
-        AccountMeta,
-        Instruction,
-    },
-    pubkey::Pubkey,
-    sysvar,
-};
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
-/// Args for update call
-pub struct UpdateMetadataAccountArgs {
-    pub data: Option<Data>,
-    pub update_authority: Option<Pubkey>,
-    pub primary_sale_happened: Option<bool>,
-}
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
-/// Args for create call
-pub struct CreateMetadataAccountArgs {
-    /// Note that unique metadatas are disabled for now.
-    pub data: Data,
-    /// Whether you want your metadata to be updateable in the future.
-    pub is_mutable: bool,
-}
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
-pub struct CreateMasterEditionArgs {
-    /// If set, means that no more than this number of editions can ever be minted. This is immutable.
-    pub max_supply: Option<u64>,
-}
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
-pub struct MintNewEditionFromMasterEditionViaTokenArgs {
-    pub edition: u64,
-}
-
-/// Instructions supported by the Metadata program.
-#[derive(BorshSerialize, BorshDeserialize, Clone)]
-pub enum MetadataInstruction {
-    /// Create Metadata object.
-    ///   0. `[writable]`  Metadata key (pda of ['metadata', program id, mint id])
-    ///   1. `[]` Mint of token asset
-    ///   2. `[signer]` Mint authority
-    ///   3. `[signer]` payer
-    ///   4. `[]` update authority info
-    ///   5. `[]` System program
-    ///   6. `[]` Rent info
-    CreateMetadataAccount(CreateMetadataAccountArgs),
-
-    /// Update a Metadata
-    ///   0. `[writable]` Metadata account
-    ///   1. `[signer]` Update authority key
-    UpdateMetadataAccount(UpdateMetadataAccountArgs),
-}
-
-/// Creates an CreateMetadataAccounts instruction
-#[allow(clippy::too_many_arguments)]
-pub fn create_metadata_accounts(
-    program_id: Pubkey,
-    metadata_account: Pubkey,
-    mint: Pubkey,
-    mint_authority: Pubkey,
-    payer: Pubkey,
-    update_authority: Pubkey,
-    name: String,
-    symbol: String,
-    uri: String,
-    creators: Option<Vec<Creator>>,
-    seller_fee_basis_points: u16,
-    update_authority_is_signer: bool,
-    is_mutable: bool,
-) -> Instruction {
-    Instruction {
-        program_id,
-        accounts: vec![
-            AccountMeta::new(metadata_account, false),
-            AccountMeta::new_readonly(mint, false),
-            AccountMeta::new_readonly(mint_authority, true),
-            AccountMeta::new(payer, true),
-            AccountMeta::new_readonly(update_authority, update_authority_is_signer),
-            AccountMeta::new_readonly(solana_program::system_program::id(), false),
-            AccountMeta::new_readonly(sysvar::rent::id(), false),
-        ],
-        data: MetadataInstruction::CreateMetadataAccount(CreateMetadataAccountArgs {
-            data: Data {
-                name,
-                symbol,
-                uri,
-                seller_fee_basis_points,
-                creators,
-            },
-            is_mutable,
-        })
-        .try_to_vec()
-        .unwrap(),
-    }
-}
-
-/// update metadata account instruction
-pub fn update_metadata_accounts(
-    program_id: Pubkey,
-    metadata_account: Pubkey,
-    update_authority: Pubkey,
-    new_update_authority: Option<Pubkey>,
-    data: Option<Data>,
-    primary_sale_happened: Option<bool>,
-) -> Instruction {
-    Instruction {
-        program_id,
-        accounts: vec![
-            AccountMeta::new(metadata_account, false),
-            AccountMeta::new_readonly(update_authority, true),
-        ],
-        data: MetadataInstruction::UpdateMetadataAccount(UpdateMetadataAccountArgs {
-            data,
-            update_authority: new_update_authority,
-            primary_sale_happened,
-        })
-        .try_to_vec()
-        .unwrap(),
-    }
-}

+ 0 - 7
solana/modules/token_bridge/token-metadata/src/lib.rs

@@ -1,7 +0,0 @@
-// The solana_program::declare_id! macro generates spurious import statements.
-#[allow(unused_imports)]
-pub mod instruction;
-pub mod state;
-pub mod utils;
-
-solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");

+ 0 - 124
solana/modules/token_bridge/token-metadata/src/state.rs

@@ -1,124 +0,0 @@
-use crate::utils::try_from_slice_checked;
-use borsh::{
-    BorshDeserialize,
-    BorshSerialize,
-};
-use solana_program::{
-    account_info::AccountInfo,
-    pubkey::Pubkey,
-};
-
-/// prefix used for PDAs to avoid certain collision attacks (https://en.wikipedia.org/wiki/Collision_attack#Chosen-prefix_collision_attack)
-pub const PREFIX: &str = "metadata";
-
-/// Used in seeds to make Edition model pda address
-pub const EDITION: &str = "edition";
-
-pub const RESERVATION: &str = "reservation";
-
-pub const MAX_NAME_LENGTH: usize = 32;
-
-pub const MAX_SYMBOL_LENGTH: usize = 10;
-
-pub const MAX_URI_LENGTH: usize = 200;
-
-pub const MAX_METADATA_LEN: usize = 1
-    + 32
-    + 32
-    + MAX_NAME_LENGTH
-    + MAX_SYMBOL_LENGTH
-    + MAX_URI_LENGTH
-    + MAX_CREATOR_LIMIT * MAX_CREATOR_LEN
-    + 2
-    + 1
-    + 1
-    + 198;
-
-pub const MAX_EDITION_LEN: usize = 1 + 32 + 8 + 200;
-
-// Large buffer because the older master editions have two pubkeys in them,
-// need to keep two versions same size because the conversion process actually changes the same account
-// by rewriting it.
-pub const MAX_MASTER_EDITION_LEN: usize = 1 + 9 + 8 + 264;
-
-pub const MAX_CREATOR_LIMIT: usize = 5;
-
-pub const MAX_CREATOR_LEN: usize = 32 + 1 + 1;
-
-pub const MAX_RESERVATIONS: usize = 200;
-
-// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
-pub const MAX_RESERVATION_LIST_V1_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 34 + 100;
-
-// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
-pub const MAX_RESERVATION_LIST_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 48 + 8 + 8 + 84;
-
-pub const MAX_EDITION_MARKER_SIZE: usize = 32;
-
-pub const EDITION_MARKER_BIT_SIZE: u64 = 248;
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone, Copy)]
-pub enum Key {
-    Uninitialized,
-    EditionV1,
-    MasterEditionV1,
-    ReservationListV1,
-    MetadataV1,
-    ReservationListV2,
-    MasterEditionV2,
-    EditionMarker,
-}
-
-impl Default for Key {
-    fn default() -> Self {
-        Key::Uninitialized
-    }
-}
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, Default, PartialEq, Debug, Clone)]
-pub struct Data {
-    /// The name of the asset
-    pub name: String,
-    /// The symbol for the asset
-    pub symbol: String,
-    /// URI pointing to JSON representing the asset
-    pub uri: String,
-    /// Royalty basis points that goes to creators in secondary sales (0-10000)
-    pub seller_fee_basis_points: u16,
-    /// Array of creators, optional
-    pub creators: Option<Vec<Creator>>,
-}
-
-#[repr(C)]
-#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Default)]
-pub struct Metadata {
-    pub key: Key,
-    pub update_authority: Pubkey,
-    pub mint: Pubkey,
-    pub data: Data,
-    // Immutable, once flipped, all sales of this metadata are considered secondary.
-    pub primary_sale_happened: bool,
-    // Whether or not the data struct is mutable, default is not
-    pub is_mutable: bool,
-}
-
-impl Metadata {
-    pub fn from_bytes(a: &[u8]) -> Option<Metadata> {
-        try_from_slice_checked(a, Key::MetadataV1, MAX_METADATA_LEN)
-    }
-
-    pub fn from_account_info(a: &AccountInfo) -> Option<Metadata> {
-        try_from_slice_checked(&a.data.borrow_mut(), Key::MetadataV1, MAX_METADATA_LEN)
-    }
-}
-
-#[repr(C)]
-#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
-pub struct Creator {
-    pub address: Pubkey,
-    pub verified: bool,
-    // In percentages, NOT basis points ;) Watch out!
-    pub share: u8,
-}

+ 0 - 16
solana/modules/token_bridge/token-metadata/src/utils.rs

@@ -1,16 +0,0 @@
-use crate::state::Key;
-use borsh::BorshDeserialize;
-use solana_program::borsh::try_from_slice_unchecked;
-
-pub fn try_from_slice_checked<T: BorshDeserialize>(
-    data: &[u8],
-    data_type: Key,
-    data_size: usize,
-) -> Option<T> {
-    if (data[0] != data_type as u8 && data[0] != Key::Uninitialized as u8)
-        || data.len() != data_size
-    {
-        return None;
-    }
-    try_from_slice_unchecked(data).ok()
-}

+ 3 - 1
testing/solana-test-validator/.gitignore

@@ -1,3 +1,5 @@
 .test
-artifacts
+artifacts*
 node_modules
+wormhole-main
+validator.log

+ 22 - 4
testing/solana-test-validator/Makefile

@@ -7,15 +7,33 @@
 node_modules:
 	yarn
 
-artifacts: node_modules
-	cd ../../solana && DOCKER_BUILDKIT=1 docker build -f Dockerfile --build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC -o ../testing/solana-test-validator/artifacts .
+artifacts:
+	cd ../../solana && \
+		DOCKER_BUILDKIT=1 docker build \
+			-f Dockerfile \
+			--build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC \
+			-o ../testing/solana-test-validator/artifacts .
+
+artifacts-main:
+	git clone \
+		--depth 1 \
+		--branch main \
+		--filter=blob:none \
+		https://github.com/wormhole-foundation/wormhole \
+		wormhole-main
+	cd wormhole-main/solana && \
+		DOCKER_BUILDKIT=1 docker build \
+			-f Dockerfile \
+			--build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC \
+			-o ../../artifacts-main .
+	rm -rf wormhole-main
 
 .PHONY: test
-test: artifacts
+test: node_modules artifacts-main artifacts
 	@echo "Running integration tests"
 	yarn run sdk-tests
 
 .PHONY: clean
 clean:
-	rm -rf artifacts node_modules validator.log .test
+	rm -rf artifacts artifacts-main wormhole-main node_modules validator.log .test
 

+ 1 - 0
testing/solana-test-validator/package.json

@@ -6,6 +6,7 @@
     "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
   },
   "dependencies": {
+    "@metaplex-foundation/mpl-token-metadata": "^2.11.1",
     "@project-serum/anchor": "0.25.0",
     "@solana/spl-token": "^0.3.1",
     "@solana/web3.js": "^1.53.0",

+ 3 - 2
testing/solana-test-validator/run_sdk_tests.sh

@@ -15,9 +15,10 @@ ACCOUNTS=$ROOT/sdk-tests/accounts
 TEST=$ROOT/.test
 
 solana-test-validator --reset \
-  --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s $ARTIFACTS/spl_token_metadata.so \
+  --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s $ARTIFACTS/mpl_token_metadata.so \
+  --account-dir $ACCOUNTS \
   --ledger $TEST > validator.log 2>&1 &
-sleep 2
+sleep 5
 
 ### write program logs
 PROGRAM_LOGS=$TEST/program-logs

+ 3 - 3
testing/solana-test-validator/sdk-tests/0_deploy_and_upgrade.ts

@@ -69,7 +69,7 @@ describe("Deploy and Upgrade Programs", () => {
 
   describe("Wormhole (Core Bridge)", () => {
     it("Deploy and Initialize", async () => {
-      const artifactPath = `${__dirname}/../artifacts/bridge.so`;
+      const artifactPath = `${__dirname}/../artifacts-main/bridge.so`;
       const programIdPath = `${__dirname}/keys/${CORE_BRIDGE_ADDRESS}.json`;
       const upgradeAuthority = deriveUpgradeAuthorityKey(CORE_BRIDGE_ADDRESS);
 
@@ -176,7 +176,7 @@ describe("Deploy and Upgrade Programs", () => {
 
   describe("Token Bridge", () => {
     it("Deploy and Initialize", async () => {
-      const artifactPath = `${__dirname}/../artifacts/token_bridge.so`;
+      const artifactPath = `${__dirname}/../artifacts-main/token_bridge.so`;
       const programIdPath = `${__dirname}/keys/${TOKEN_BRIDGE_ADDRESS}.json`;
       const upgradeAuthority = deriveUpgradeAuthorityKey(TOKEN_BRIDGE_ADDRESS);
 
@@ -270,7 +270,7 @@ describe("Deploy and Upgrade Programs", () => {
 
   describe("NFT Bridge", () => {
     it("Deploy and Initialize", async () => {
-      const artifactPath = `${__dirname}/../artifacts/nft_bridge.so`;
+      const artifactPath = `${__dirname}/../artifacts-main/nft_bridge.so`;
       const programIdPath = `${__dirname}/keys/${NFT_BRIDGE_ADDRESS}.json`;
       const upgradeAuthority = deriveUpgradeAuthorityKey(NFT_BRIDGE_ADDRESS);
 

+ 285 - 7
testing/solana-test-validator/sdk-tests/2_token_bridge.ts

@@ -1,5 +1,9 @@
 import { expect } from "chai";
 import * as web3 from "@solana/web3.js";
+import {
+  Metadata,
+  PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
+} from "@metaplex-foundation/mpl-token-metadata";
 import {
   createMint,
   getAccount,
@@ -30,6 +34,7 @@ import {
   deriveEndpointKey,
   deriveMintAuthorityKey,
   deriveRedeemerAccountKey,
+  deriveTokenMetadataKey,
   deriveWrappedMintKey,
   getAttestTokenAccounts,
   getCompleteTransferNativeAccounts,
@@ -54,7 +59,6 @@ import {
   getTransferWrappedWithPayloadCpiAccounts,
   NodeWallet,
   signSendAndConfirmTransaction,
-  SplTokenMetadataProgram,
 } from "../../../sdk/js/src/solana";
 import {
   deriveWormholeEmitterKey,
@@ -80,6 +84,9 @@ import {
   GUARDIAN_SET_INDEX,
   LOCALHOST,
   WETH_ADDRESS,
+  DEADBEEF_ADDRESS,
+  DEADBEEF_METADATA_ADDRESS,
+  DEADBEEF_MINT_ADDRESS,
 } from "./helpers/consts";
 import { ethAddressToBuffer, now } from "./helpers/utils";
 import {
@@ -517,7 +524,7 @@ describe("Token Bridge", () => {
         "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru"
       );
       expect(accounts.vaa.toString()).to.equal(
-        "AaZqjqvg8QirKetuR799Smfw5gyAWobUooGsvxzr1aoX"
+        "4NDyWDtRvfEdi48a9JgYG28m919hrcdW8gNgRg3jwU99"
       );
       expect(accounts.claim.toString()).to.equal(
         "4dyk94hhqektDX9wUBCL1ZkyQC1Xn3QaTSAdJeZzbTcJ"
@@ -538,9 +545,8 @@ describe("Token Bridge", () => {
       expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
         .true;
       expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
-      expect(
-        accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
-      ).is.true;
+      expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
+        .true;
       expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
     });
 
@@ -1552,6 +1558,278 @@ describe("Token Bridge", () => {
           Buffer.compare(wrappedMeta.tokenAddress, expectedTokenAddress)
         ).to.equal(0);
         expect(wrappedMeta.originalDecimals).to.equal(decimals);
+
+        // check metadata
+        const expectedName = `${name} (Wormhole)`.padEnd(32, "\0");
+        const metadata = await Metadata.fromAccountAddress(
+          connection,
+          deriveTokenMetadataKey(mint)
+        );
+        expect(metadata.data.symbol.toString()).equals(symbol.padEnd(10, "\0"));
+        expect(metadata.data.name.toString()).equals(expectedName);
+        localVariables.oldName = expectedName;
+      });
+
+      it("Update (Create) Wrapped with New Metadata", async () => {
+        const tokenAddress = WETH_ADDRESS;
+        const oldName: string = localVariables.oldName;
+
+        const mint = deriveWrappedMintKey(
+          TOKEN_BRIDGE_ADDRESS,
+          ethereumTokenBridge.chain,
+          tokenAddress
+        );
+
+        // check existing metadata
+        {
+          const metadata = await Metadata.fromAccountAddress(
+            connection,
+            deriveTokenMetadataKey(mint)
+          );
+          expect(metadata.data.name.toString()).equals(oldName);
+        }
+
+        const decimals = 18;
+        const symbol = "WETH";
+        const name = "Wrapped Ether";
+        const nonce = 420;
+        const message = ethereumTokenBridge.publishAttestMeta(
+          tokenAddress,
+          decimals,
+          symbol,
+          name,
+          nonce
+        );
+        const signedVaa = guardians.addSignatures(
+          message,
+          [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18]
+        );
+
+        const txSignatures = await postVaa(
+          connection,
+          wallet.signTransaction,
+          CORE_BRIDGE_ADDRESS,
+          wallet.key(),
+          signedVaa
+        ).then((results) => results.map((result) => result.signature));
+        const postTx = txSignatures.pop()!;
+        for (const verifyTx of txSignatures) {
+          // console.log(`verifySignatures: ${verifyTx}`);
+        }
+        // console.log(`postVaa:          ${postTx}`);
+
+        const createWrappedIx = createCreateWrappedInstruction(
+          TOKEN_BRIDGE_ADDRESS,
+          CORE_BRIDGE_ADDRESS,
+          wallet.key(),
+          signedVaa
+        );
+
+        const createWrappedTx = await web3.sendAndConfirmTransaction(
+          connection,
+          new web3.Transaction().add(createWrappedIx),
+          [wallet.signer()]
+        );
+        // console.log(`createWrappedTx: ${createWrappedTx}`);
+
+        // verify data
+        const parsed = parseAttestMetaVaa(signedVaa);
+        const messageData = await getPostedVaa(
+          connection,
+          CORE_BRIDGE_ADDRESS,
+          parsed.hash
+        ).then((posted) => posted.message);
+
+        expect(messageData.consistencyLevel).to.equal(
+          ethereumTokenBridge.consistencyLevel
+        );
+        const expectedEmitter = ethAddressToBuffer(
+          ETHEREUM_TOKEN_BRIDGE_ADDRESS
+        );
+        expect(
+          Buffer.compare(messageData.emitterAddress, expectedEmitter)
+        ).to.equal(0);
+        expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain);
+        expect(messageData.nonce).to.equal(nonce);
+        expect(messageData.sequence).to.equal(3n);
+        expect(messageData.vaaTime).to.equal(0);
+        expect(messageData.vaaVersion).to.equal(1);
+        expect(Buffer.compare(parsed.payload, messageData.payload)).to.equal(0);
+
+        const assetMeta = parseAttestMetaPayload(messageData.payload);
+        expect(assetMeta.payloadType).to.equal(2);
+        const expectedTokenAddress = ethAddressToBuffer(tokenAddress);
+        expect(
+          Buffer.compare(assetMeta.tokenAddress, expectedTokenAddress)
+        ).to.equal(0);
+        expect(assetMeta.tokenChain).to.equal(ethereumTokenBridge.chain);
+        expect(assetMeta.decimals).to.equal(decimals);
+        expect(assetMeta.symbol).to.equal(symbol);
+        expect(assetMeta.name).to.equal(name);
+
+        // check wrapped mint
+        const mintInfo = await getMint(connection, mint);
+        expect(mintInfo.decimals).to.equal(8);
+        expect(mintInfo.mintAuthority).is.not.null;
+        expect(
+          mintInfo.mintAuthority?.equals(
+            deriveMintAuthorityKey(TOKEN_BRIDGE_ADDRESS)
+          )
+        ).is.true;
+        expect(mintInfo.supply).to.equal(0n);
+
+        // check wrapped meta
+        const wrappedMeta = await getWrappedMeta(
+          connection,
+          TOKEN_BRIDGE_ADDRESS,
+          mint
+        );
+        expect(wrappedMeta.chain).to.equal(ethereumTokenBridge.chain);
+        expect(
+          Buffer.compare(wrappedMeta.tokenAddress, expectedTokenAddress)
+        ).to.equal(0);
+        expect(wrappedMeta.originalDecimals).to.equal(decimals);
+
+        // check metadata
+        const metadata = await Metadata.fromAccountAddress(
+          connection,
+          deriveTokenMetadataKey(mint)
+        );
+        expect(metadata.data.name.toString()).not.equals(oldName);
+
+        expect(metadata.data.symbol.toString()).equals(symbol.padEnd(10, "\0"));
+        expect(metadata.data.name.toString()).equals(
+          `${name} (Wormhole)`.padEnd(32, "\0")
+        );
+      });
+
+      it("Update (Create) Wrapped with New Metadata for V1 Metadata Account", async () => {
+        const tokenAddress = DEADBEEF_ADDRESS;
+        const oldExpectedName = "Dead Beef (Wormhole)".padEnd(32, "\0");
+
+        // fetch previously created metadata account
+        // check wrapped mint
+        {
+          const mint = deriveWrappedMintKey(
+            TOKEN_BRIDGE_ADDRESS,
+            ethereumTokenBridge.chain,
+            tokenAddress
+          );
+          expect(mint.toString()).equals(DEADBEEF_MINT_ADDRESS);
+
+          const metadataKey = deriveTokenMetadataKey(mint);
+          expect(metadataKey.toString()).equals(DEADBEEF_METADATA_ADDRESS);
+
+          const metadata = await Metadata.fromAccountAddress(
+            connection,
+            metadataKey
+          );
+          expect(metadata.data.name.toString()).equals(oldExpectedName);
+        }
+
+        const decimals = 18;
+        const symbol = "BEEF";
+        const name = "Dead Beef Modified";
+        const nonce = 420;
+        const message = ethereumTokenBridge.publishAttestMeta(
+          tokenAddress,
+          decimals,
+          symbol,
+          name,
+          nonce
+        );
+        const signedVaa = guardians.addSignatures(
+          message,
+          [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18]
+        );
+
+        const txSignatures = await postVaa(
+          connection,
+          wallet.signTransaction,
+          CORE_BRIDGE_ADDRESS,
+          wallet.key(),
+          signedVaa
+        ).then((results) => results.map((result) => result.signature));
+        const postTx = txSignatures.pop()!;
+        for (const verifyTx of txSignatures) {
+          // console.log(`verifySignatures: ${verifyTx}`);
+        }
+        // console.log(`postVaa:          ${postTx}`);
+
+        const createWrappedIx = createCreateWrappedInstruction(
+          TOKEN_BRIDGE_ADDRESS,
+          CORE_BRIDGE_ADDRESS,
+          wallet.key(),
+          signedVaa
+        );
+
+        const createWrappedTx = await web3.sendAndConfirmTransaction(
+          connection,
+          new web3.Transaction().add(createWrappedIx),
+          [wallet.signer()]
+        );
+        // console.log(`createWrappedTx: ${createWrappedTx}`);
+
+        // verify data
+        const parsed = parseAttestMetaVaa(signedVaa);
+        const messageData = await getPostedVaa(
+          connection,
+          CORE_BRIDGE_ADDRESS,
+          parsed.hash
+        ).then((posted) => posted.message);
+
+        expect(messageData.consistencyLevel).to.equal(
+          ethereumTokenBridge.consistencyLevel
+        );
+        const expectedEmitter = ethAddressToBuffer(
+          ETHEREUM_TOKEN_BRIDGE_ADDRESS
+        );
+        expect(
+          Buffer.compare(messageData.emitterAddress, expectedEmitter)
+        ).to.equal(0);
+        expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain);
+        expect(messageData.nonce).to.equal(nonce);
+        expect(messageData.sequence).to.equal(4n);
+        expect(messageData.vaaTime).to.equal(0);
+        expect(messageData.vaaVersion).to.equal(1);
+        expect(Buffer.compare(parsed.payload, messageData.payload)).to.equal(0);
+
+        const assetMeta = parseAttestMetaPayload(messageData.payload);
+        expect(assetMeta.payloadType).to.equal(2);
+        const expectedTokenAddress = ethAddressToBuffer(tokenAddress);
+        expect(
+          Buffer.compare(assetMeta.tokenAddress, expectedTokenAddress)
+        ).to.equal(0);
+        expect(assetMeta.tokenChain).to.equal(ethereumTokenBridge.chain);
+        expect(assetMeta.decimals).to.equal(decimals);
+        expect(assetMeta.symbol).to.equal(symbol);
+        expect(assetMeta.name).to.equal(name);
+
+        // check wrapped mint
+        const mint = deriveWrappedMintKey(
+          TOKEN_BRIDGE_ADDRESS,
+          assetMeta.tokenChain,
+          assetMeta.tokenAddress
+        );
+        const mintInfo = await getMint(connection, mint);
+        expect(mintInfo.decimals).to.equal(8);
+        expect(mintInfo.mintAuthority).is.not.null;
+        expect(
+          mintInfo.mintAuthority?.equals(
+            deriveMintAuthorityKey(TOKEN_BRIDGE_ADDRESS)
+          )
+        ).is.true;
+        expect(mintInfo.supply).to.equal(0n);
+
+        // check metadata
+        const metadata = await Metadata.fromAccountAddress(
+          connection,
+          deriveTokenMetadataKey(mint)
+        );
+        expect(metadata.data.name.toString()).not.equals(oldExpectedName);
+        expect(metadata.data.name.toString()).equals(
+          `${name} (Wormhole)`.padEnd(32, "\0")
+        );
       });
 
       it("Receive Token", async () => {
@@ -1653,7 +1931,7 @@ describe("Token Bridge", () => {
         ).to.equal(0);
         expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain);
         expect(messageData.nonce).to.equal(nonce);
-        expect(messageData.sequence).to.equal(3n);
+        expect(messageData.sequence).to.equal(5n);
         expect(messageData.vaaTime).to.equal(0);
         expect(messageData.vaaVersion).to.equal(1);
         expect(
@@ -1900,7 +2178,7 @@ describe("Token Bridge", () => {
     // nft bridge on Ethereum
     const ethereumTokenBridge = new MockEthereumTokenBridge(
       ETHEREUM_TOKEN_BRIDGE_ADDRESS,
-      3 // startSequence
+      10 // startSequence
     );
 
     describe("getOriginalAssetSolana", () => {

+ 50 - 38
testing/solana-test-validator/sdk-tests/3_nft_bridge.ts

@@ -1,5 +1,10 @@
 import { expect } from "chai";
 import * as web3 from "@solana/web3.js";
+import {
+  Metadata,
+  PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
+  createCreateMetadataAccountV3Instruction,
+} from "@metaplex-foundation/mpl-token-metadata";
 import {
   ASSOCIATED_TOKEN_PROGRAM_ID,
   createMint,
@@ -19,9 +24,8 @@ import {
 import { postVaa } from "../../../sdk/js/src/solana/sendAndConfirmPostVaa";
 import {
   BpfLoaderUpgradeable,
-  getMetadata,
   NodeWallet,
-  SplTokenMetadataProgram,
+  deriveTokenMetadataKey,
 } from "../../../sdk/js/src/solana";
 import {
   deriveWormholeEmitterKey,
@@ -115,26 +119,37 @@ describe("NFT Bridge", () => {
       uri: "https://spl.solana.com/token#example-create-a-non-fungible-token",
     };
 
-    const mint = localVariables.mint;
-    const name = localVariables.nftMeta.name;
-    const symbol = localVariables.nftMeta.symbol;
-    const updateAuthorityIsSigner = false;
-    const uri = localVariables.nftMeta.uri;
-    const creators = null;
-    const sellerFeeBasisPoints = 0;
-    const isMutable = false;
-    const createMetadataIx = SplTokenMetadataProgram.createMetadataAccounts(
-      wallet.key(),
+    const mint: web3.PublicKey = localVariables.mint;
+    const name: string = localVariables.nftMeta.name;
+    const symbol: string = localVariables.nftMeta.symbol;
+    const uri: string = localVariables.nftMeta.uri;
+
+    const accounts = {
+      metadata: deriveTokenMetadataKey(mint),
       mint,
-      wallet.key(),
-      name,
-      symbol,
-      wallet.key(),
-      updateAuthorityIsSigner,
-      uri,
-      creators,
-      sellerFeeBasisPoints,
-      isMutable
+      mintAuthority: wallet.key(),
+      payer: wallet.key(),
+      updateAuthority: wallet.key(),
+    };
+    const args = {
+      createMetadataAccountArgsV3: {
+        data: {
+          name,
+          symbol,
+          uri,
+          sellerFeeBasisPoints: 0,
+          creators: null,
+          collection: null,
+          uses: null,
+        },
+        isMutable: false,
+        collectionDetails: null,
+      },
+    };
+    const createMetadataIx = createCreateMetadataAccountV3Instruction(
+      accounts,
+      args,
+      TOKEN_METADATA_PROGRAM_ID
     );
 
     const createMetadataTx = await web3.sendAndConfirmTransaction(
@@ -323,9 +338,8 @@ describe("NFT Bridge", () => {
       expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
         .true;
       expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
-      expect(
-        accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
-      ).is.true;
+      expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
+        .true;
       expect(
         accounts.associatedTokenProgram.equals(ASSOCIATED_TOKEN_PROGRAM_ID)
       ).is.true;
@@ -401,9 +415,8 @@ describe("NFT Bridge", () => {
       expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
         .true;
       expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
-      expect(
-        accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
-      ).is.true;
+      expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
+        .true;
       expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
     });
 
@@ -467,9 +480,8 @@ describe("NFT Bridge", () => {
       expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
         .true;
       expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
-      expect(
-        accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
-      ).is.true;
+      expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
+        .true;
       expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
     });
 
@@ -524,9 +536,8 @@ describe("NFT Bridge", () => {
       expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
         .true;
       expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
-      expect(
-        accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
-      ).is.true;
+      expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
+        .true;
       expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
     });
 
@@ -792,8 +803,9 @@ describe("NFT Bridge", () => {
           custodyAccount
         ).then((account) => account.amount);
 
-        const metadata = await getMetadata(connection, mint).then(
-          (info) => info.data
+        const metadata = await Metadata.fromAccountAddress(
+          connection,
+          deriveTokenMetadataKey(mint)
         );
 
         const tokenChain = 1;
@@ -803,10 +815,10 @@ describe("NFT Bridge", () => {
         const message = ethereumNftBridge.publishTransferNft(
           NFT_TRANSFER_NATIVE_TOKEN_ADDRESS.toString("hex"),
           tokenChain,
-          metadata.name,
-          metadata.symbol,
+          metadata.data.name,
+          metadata.data.symbol,
           tokenId,
-          metadata.uri,
+          metadata.data.uri,
           recipientChain,
           mintAta.toBuffer().toString("hex"),
           nonce

+ 13 - 0
testing/solana-test-validator/sdk-tests/accounts/wrapped_deadbeef_metadata.json

@@ -0,0 +1,13 @@
+{
+  "pubkey": "A2mcN1tknMBAgMnCUF6ZT8tEJj1QE1AuCsquvXHgpWVJ",
+  "account": {
+    "lamports": 5616720,
+    "data": [
+      "BP0KziEuVYQFCsdn5PTCokaUsJwqy3LxSbhyCV219Z2d8nzo8j6OCMEC0yd3q42xDE00sIuZJtJx185IZhkbzwkgAAAARGVhZCBCZWVmIChXb3JtaG9sZSkAAAAAAAAAAAAAAAAKAAAAQkVFRgAAAAAAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+      "base64"
+    ],
+    "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
+    "executable": false,
+    "rentEpoch": 0
+  }
+}

+ 13 - 0
testing/solana-test-validator/sdk-tests/accounts/wrapped_deadbeef_mint.json

@@ -0,0 +1,13 @@
+{
+  "pubkey": "HKa8mGMJJm2qnja4xmMaoeGqF4LBpGCyXVZuaiGYFaYY",
+  "account": {
+    "lamports": 1461600,
+    "data": [
+      "AQAAAP0KziEuVYQFCsdn5PTCokaUsJwqy3LxSbhyCV219Z2dAAAAAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+      "base64"
+    ],
+    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
+    "executable": false,
+    "rentEpoch": 0
+  }
+}

+ 7 - 0
testing/solana-test-validator/sdk-tests/helpers/consts.ts

@@ -50,3 +50,10 @@ export const ETHEREUM_TOKEN_BRIDGE_ADDRESS =
 export const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
 export const ETHEREUM_NFT_BRIDGE_ADDRESS =
   "0x26b4afb60d6c903165150c6f0aa14f8016be4aec";
+
+// preloaded
+export const DEADBEEF_ADDRESS = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+export const DEADBEEF_MINT_ADDRESS =
+  "HKa8mGMJJm2qnja4xmMaoeGqF4LBpGCyXVZuaiGYFaYY";
+export const DEADBEEF_METADATA_ADDRESS =
+  "A2mcN1tknMBAgMnCUF6ZT8tEJj1QE1AuCsquvXHgpWVJ";

+ 153 - 9
testing/solana-test-validator/yarn.lock

@@ -55,11 +55,60 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
+"@metaplex-foundation/beet-solana@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz#52891e78674aaa54e0031f1bca5bfbc40de12e8d"
+  integrity sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==
+  dependencies:
+    "@metaplex-foundation/beet" ">=0.1.0"
+    "@solana/web3.js" "^1.56.2"
+    bs58 "^5.0.0"
+    debug "^4.3.4"
+
+"@metaplex-foundation/beet@>=0.1.0", "@metaplex-foundation/beet@^0.7.1":
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.7.1.tgz#0975314211643f87b5f6f3e584fa31abcf4c612c"
+  integrity sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA==
+  dependencies:
+    ansicolors "^0.3.2"
+    bn.js "^5.2.0"
+    debug "^4.3.3"
+
+"@metaplex-foundation/cusper@^0.0.2":
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz#dc2032a452d6c269e25f016aa4dd63600e2af975"
+  integrity sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==
+
+"@metaplex-foundation/mpl-token-metadata@^2.11.1":
+  version "2.11.1"
+  resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.11.1.tgz#b7755c5cc7bae5e98e285dd675fa65b62e9bd881"
+  integrity sha512-FNJhDAFmpXD5K9lstJYXROjUjHQmCHFpzVs4asUpVvtkF645+PGyDtqoUENfVEwoUPY8ZT6Exs7d0exRgYqxUA==
+  dependencies:
+    "@metaplex-foundation/beet" "^0.7.1"
+    "@metaplex-foundation/beet-solana" "^0.4.0"
+    "@metaplex-foundation/cusper" "^0.0.2"
+    "@solana/spl-token" "^0.3.6"
+    "@solana/web3.js" "^1.66.2"
+    bn.js "^5.2.0"
+    debug "^4.3.4"
+
+"@noble/curves@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932"
+  integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==
+  dependencies:
+    "@noble/hashes" "1.3.0"
+
 "@noble/ed25519@^1.7.0":
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.0.tgz#583ac38340a479314b9e348d4572101ed9492f9d"
   integrity sha512-LeAxFK0+181zQOhOUuKE8Jnd3duzYhDNd3iCLxpmzA5K+e4I1FdbrK3Ot0ZHBwZMeRD/6EojyUfTbpHZ+hkQHg==
 
+"@noble/hashes@1.3.0", "@noble/hashes@^1.3.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
+  integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
+
 "@noble/hashes@^1.1.2":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183"
@@ -125,6 +174,15 @@
     "@solana/buffer-layout-utils" "^0.2.0"
     "@solana/web3.js" "^1.41.0"
 
+"@solana/spl-token@^0.3.6":
+  version "0.3.7"
+  resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.7.tgz#6f027f9ad8e841f792c32e50920d9d2e714fc8da"
+  integrity sha512-bKGxWTtIw6VDdCBngjtsGlKGLSmiu/8ghSt/IOYJV24BsymRbgq7r12GToeetpxmPaZYLddKwAz7+EwprLfkfg==
+  dependencies:
+    "@solana/buffer-layout" "^4.0.0"
+    "@solana/buffer-layout-utils" "^0.2.0"
+    buffer "^6.0.3"
+
 "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.41.0", "@solana/web3.js@^1.53.0":
   version "1.53.0"
   resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.53.0.tgz"
@@ -170,6 +228,27 @@
     rpc-websockets "^7.5.0"
     superstruct "^0.14.2"
 
+"@solana/web3.js@^1.56.2", "@solana/web3.js@^1.66.2":
+  version "1.76.0"
+  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.76.0.tgz#0f888e25d727d0dadf3dd8a01967347555200b2b"
+  integrity sha512-aJtF/nTs+9St+KtTK/wgVJ+SinfjYzn+3w1ygYIPw8ST6LH+qHBn8XkodgDTwlv/xzNkaVz1kkUDOZ8BPXyZWA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@noble/curves" "^1.0.0"
+    "@noble/hashes" "^1.3.0"
+    "@solana/buffer-layout" "^4.0.0"
+    agentkeepalive "^4.2.1"
+    bigint-buffer "^1.1.5"
+    bn.js "^5.0.0"
+    borsh "^0.7.0"
+    bs58 "^4.0.1"
+    buffer "6.0.3"
+    fast-stable-stringify "^1.0.0"
+    jayson "^3.4.4"
+    node-fetch "^2.6.7"
+    rpc-websockets "^7.5.1"
+    superstruct "^0.14.2"
+
 "@tsconfig/node10@^1.0.7":
   version "1.0.9"
   resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz"
@@ -283,6 +362,15 @@ acorn@^8.4.1:
   resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz"
   integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
 
+agentkeepalive@^4.2.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255"
+  integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==
+  dependencies:
+    debug "^4.1.0"
+    depd "^2.0.0"
+    humanize-ms "^1.2.1"
+
 ansi-colors@4.1.1:
   version "4.1.1"
   resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz"
@@ -300,6 +388,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
+ansicolors@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
+  integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==
+
 anymatch@~3.1.2:
   version "3.1.2"
   resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz"
@@ -340,6 +433,11 @@ base-x@^3.0.2:
   dependencies:
     safe-buffer "^5.0.1"
 
+base-x@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
+  integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
+
 base64-js@^1.3.1, base64-js@^1.5.1:
   version "1.5.1"
   resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
@@ -420,6 +518,13 @@ bs58@^4.0.0, bs58@^4.0.1:
   dependencies:
     base-x "^3.0.2"
 
+bs58@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
+  integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
+  dependencies:
+    base-x "^4.0.0"
+
 buffer-from@^1.0.0, buffer-from@^1.1.0:
   version "1.1.2"
   resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
@@ -438,6 +543,14 @@ buffer@6.0.1:
     base64-js "^1.3.1"
     ieee754 "^1.2.1"
 
+buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3:
+  version "6.0.3"
+  resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
+  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.2.1"
+
 buffer@^5.4.3:
   version "5.7.1"
   resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz"
@@ -446,14 +559,6 @@ buffer@^5.4.3:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
-buffer@^6.0.3, buffer@~6.0.3:
-  version "6.0.3"
-  resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
-  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.2.1"
-
 bufferutil@^4.0.1:
   version "4.0.6"
   resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz"
@@ -577,6 +682,13 @@ debug@4.3.3:
   dependencies:
     ms "2.1.2"
 
+debug@^4.1.0, debug@^4.3.3, debug@^4.3.4:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
 decamelize@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz"
@@ -594,6 +706,11 @@ delay@^5.0.0:
   resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz"
   integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
 
+depd@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
 diff@5.0.0:
   version "5.0.0"
   resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz"
@@ -768,6 +885,13 @@ hmac-drbg@^1.0.1:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
+humanize-ms@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+  integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
+  dependencies:
+    ms "^2.0.0"
+
 ieee754@^1.1.13, ieee754@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
@@ -1018,7 +1142,7 @@ ms@2.1.2:
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@2.1.3:
+ms@2.1.3, ms@^2.0.0:
   version "2.1.3"
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -1048,6 +1172,13 @@ node-fetch@2, node-fetch@2.6.7:
   dependencies:
     whatwg-url "^5.0.0"
 
+node-fetch@^2.6.7:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25"
+  integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==
+  dependencies:
+    whatwg-url "^5.0.0"
+
 node-gyp-build@^4.2.0, node-gyp-build@^4.3.0:
   version "4.5.0"
   resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz"
@@ -1167,6 +1298,19 @@ rpc-websockets@^7.5.0:
     bufferutil "^4.0.1"
     utf-8-validate "^5.0.2"
 
+rpc-websockets@^7.5.1:
+  version "7.5.1"
+  resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.1.tgz#e0a05d525a97e7efc31a0617f093a13a2e10c401"
+  integrity sha512-kGFkeTsmd37pHPMaHIgN1LVKXMi0JD782v4Ds9ZKtLlwdTKjn+CxM9A9/gLT2LaOuEcEFGL98h1QWQtlOIdW0w==
+  dependencies:
+    "@babel/runtime" "^7.17.2"
+    eventemitter3 "^4.0.7"
+    uuid "^8.3.2"
+    ws "^8.5.0"
+  optionalDependencies:
+    bufferutil "^4.0.1"
+    utf-8-validate "^5.0.2"
+
 safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
   version "5.2.1"
   resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"