Repository: solana-labs/solana-program-library
Pull request #5805 (2023-11-11 07:26:22)

token 2022: add metadata pointer extension to js @solana/spl-token client
Adding support for the metadata pointer extension


Split out of wider task to add metadata support for ease of review
https://github.com/solana-labs/solana-program-library/pull/5667

mistersimon: > Nice! We are cruising right along with Token2022's JS support, thanks to you mate.
> 
> Just some small bits from my side, then we should be able to wrap soon.

Thanks for the kind words @buffalojoec. 

Addressed most of your comments - wanted to clarify one thing on the enums before changing

Patch:
diff --git a/account-compression/sdk/README.md b/account-compression/sdk/README.md
index c631c7fac..7df70ec58 100644
--- a/account-compression/sdk/README.md
+++ b/account-compression/sdk/README.md
@@ -137,7 +137,7 @@ await sendAndConfirmTransaction(connection, tx, [payer]);
 
 4. Replace a leaf in the tree, using a 3rd party indexer
 
-This example assumes that some 3rd party service is indexing the the tree at `cmtKeypair.publicKey` for you, and providing MerkleProofs via some REST endpoint.
+This example assumes that some 3rd party service is indexing the tree at `cmtKeypair.publicKey` for you, and providing MerkleProofs via some REST endpoint.
 The `getProofFromAnIndexer` function is a **placeholder** to exemplify this relationship.
 
 ```typescript
@@ -177,4 +177,4 @@ Here are some examples using account compression in the wild:
 
 2. Then build the SDK with `yarn build`.
 
-3. Run tests with `yarn test`. (Expect `jest` to detect an open handle that prevents it from exiting naturally)
\ No newline at end of file
+3. Run tests with `yarn test`. (Expect `jest` to detect an open handle that prevents it from exiting naturally)
diff --git a/docs/src/feature-proposal.md b/docs/src/feature-proposal.md
index 4f6f556d0..0f4acd6af 100644
--- a/docs/src/feature-proposal.md
+++ b/docs/src/feature-proposal.md
@@ -60,7 +60,7 @@ This section describes the life cycle of a feature proposal.
 The first step is to conceive of the new feature and realize it in the
 Solana code base, working with the core Solana developers at https://github.com/solana-labs/solana.
 
-During the implementation, a *feature id* will be required to identity the new
+During the implementation, a *feature id* will be required to identify the new
 feature in the code base to avoid the new functionality until its activation.
 The *feature id* for a feature proposal is derived by running the following
 commands.
@@ -140,8 +140,8 @@ acceptance, the votes are tallied by running:
 ```
 $ spl-feature-proposal tally 8CyUVvio2oYAP28ZkMBPHq88ikhRgWet6i4NYsCW5Cxa
 ```
-Anybody may tally the vote.  Once the required number of votes are tallied, the
+Anybody may tally the vote.  Once the required number of votes is tallied, the
 feature will be automatically activated at the start of the next epoch.
 
 Upon a successful activation the feature will now show as activated by
-`solana feature status` as well.
\ No newline at end of file
+`solana feature status` as well.
diff --git a/docs/src/single-pool.mdx b/docs/src/single-pool.mdx
index 683945b93..86e37393b 100644
--- a/docs/src/single-pool.mdx
+++ b/docs/src/single-pool.mdx
@@ -94,7 +94,7 @@ $ solana config set --keypair usb://ledger/
 the wallet to draw funds from (for instance, to fund new stake accounts),
 and the signing authority on accounts that require one.
 When token accounts are required, it defaults to the default keypair's associated account.
-All of these roles can be overrided by command-line flags.
+All of these roles can be overriden by command-line flags.
 
   </TabItem>
   <TabItem value="jsc" label="WEB3.JS CLASSIC">
diff --git a/docs/src/stake-pool/overview.md b/docs/src/stake-pool/overview.md
index a576b9594..5fd9fcdcd 100644
--- a/docs/src/stake-pool/overview.md
+++ b/docs/src/stake-pool/overview.md
@@ -66,7 +66,7 @@ using the `add-validator` instruction. In this command, the stake pool uses
 reserve funds to create a new stake account and delegate it to the desired validator.
 
 At this point, users can also deposit a stake account into the pool.  To do this,
-they must delegate a stake account to the one of the validators in the stake pool.
+they must delegate a stake account to one of the validators in the stake pool.
 If the stake pool has a preferred deposit validator, the user must delegate their
 stake to that validator's vote account.
 
@@ -172,7 +172,7 @@ In this way, a user's funds are never at risk, and always redeemable.
 ### Active stakes
 
 As mentioned earlier, the stake pool works with active stakes to
-maintains fungibility of stake pool tokens. Fully activated stakes
+maintain fungibility of stake pool tokens. Fully activated stakes
 are not equivalent to inactive, activating, or deactivating stakes due to the
 time cost of staking.
 
diff --git a/docs/src/stake-pool/quickstart.md b/docs/src/stake-pool/quickstart.md
index f4b483245..ae628af80 100644
--- a/docs/src/stake-pool/quickstart.md
+++ b/docs/src/stake-pool/quickstart.md
@@ -207,7 +207,7 @@ through an automated system to collect information about the stake pool and the
 network, and decide how much stake to allocate to each validator.
 
 The Solana Foundation maintains an open-source bot for its delegation program,
-which can be adapated for your stake pool. The source code is part of the
+which can be adapted for your stake pool. The source code is part of the
 [stake-o-matic GitHub repo](https://github.com/solana-labs/stake-o-matic/tree/master/bot).
 
 Additionally, there is a work-in-progress Python stake pool bot, found at the
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 869e442de..2c9ca21bf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -246,9 +246,6 @@ importers:
       solana-bankrun:
         specifier: ^0.2.0
         version: 0.2.0
-      ts-node:
-        specifier: ^10.9.1
-        version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2)
       tsx:
         specifier: ^3.12.7
         version: 3.14.0
@@ -262,18 +259,12 @@ importers:
         specifier: '=2.0.0-experimental.21e994f'
         version: 2.0.0-experimental.21e994f(node-fetch@2.7.0)(ws@8.14.2)
     devDependencies:
-      '@ava/typescript':
-        specifier: ^4.1.0
-        version: 4.1.0
       '@types/node':
         specifier: ^20.9.0
         version: 20.9.0
       '@typescript-eslint/eslint-plugin':
         specifier: ^6.4.1
         version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2)
-      ava:
-        specifier: ^5.3.1
-        version: 5.3.1(@ava/typescript@4.1.0)
       eslint:
         specifier: ^8.49.0
         version: 8.53.0
@@ -283,12 +274,6 @@ importers:
       eslint-plugin-prettier:
         specifier: ^5.0.0
         version: 5.0.1(@types/eslint@8.44.7)(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3)
-      ts-node:
-        specifier: ^10.9.1
-        version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2)
-      tsx:
-        specifier: ^3.12.7
-        version: 3.14.0
       typescript:
         specifier: ^5.2.2
         version: 5.2.2
diff --git a/single-pool/js/packages/classic/package.json b/single-pool/js/packages/classic/package.json
index 961de0e44..d70de2d01 100644
--- a/single-pool/js/packages/classic/package.json
+++ b/single-pool/js/packages/classic/package.json
@@ -24,7 +24,6 @@
     "eslint-config-prettier": "^9.0.0",
     "eslint-plugin-prettier": "^5.0.0",
     "solana-bankrun": "^0.2.0",
-    "ts-node": "^10.9.1",
     "tsx": "^3.12.7",
     "typescript": "^5.2.2"
   },
diff --git a/single-pool/js/packages/modern/package.json b/single-pool/js/packages/modern/package.json
index 57e96ffcf..77c43adcb 100644
--- a/single-pool/js/packages/modern/package.json
+++ b/single-pool/js/packages/modern/package.json
@@ -16,25 +16,13 @@
   },
   "devDependencies": {
     "@types/node": "^20.9.0",
-    "@ava/typescript": "^4.1.0",
     "@typescript-eslint/eslint-plugin": "^6.4.1",
-    "ava": "^5.3.1",
     "eslint": "^8.49.0",
     "eslint-config-prettier": "^9.0.0",
     "eslint-plugin-prettier": "^5.0.0",
-    "ts-node": "^10.9.1",
-    "tsx": "^3.12.7",
     "typescript": "^5.2.2"
   },
   "dependencies": {
     "@solana/web3.js": "=2.0.0-experimental.21e994f"
-  },
-  "ava": {
-    "extensions": {
-      "ts": "module"
-    },
-    "nodeArguments": [
-      "--import=tsx"
-    ]
   }
 }
diff --git a/single-pool/js/tsconfig-base.json b/single-pool/js/tsconfig-base.json
index 96f4e86f6..76d1a1395 100644
--- a/single-pool/js/tsconfig-base.json
+++ b/single-pool/js/tsconfig-base.json
@@ -17,7 +17,7 @@
         "skipLibCheck": true,
         "strict": true,
         "traceResolution": false,
-        "types": ["node", "ava"]
+        "types": ["node"]
     },
     "compileOnSave": false,
     "exclude": ["node_modules", "dist"],
diff --git a/token/js/src/extensions/extensionType.ts b/token/js/src/extensions/extensionType.ts
index 75172abec..bec17bda8 100644
--- a/token/js/src/extensions/extensionType.ts
+++ b/token/js/src/extensions/extensionType.ts
@@ -8,12 +8,14 @@ import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState/index.js';
 import { IMMUTABLE_OWNER_SIZE } from './immutableOwner.js';
 import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state.js';
 import { MEMO_TRANSFER_SIZE } from './memoTransfer/index.js';
+import { METADATA_POINTER_SIZE } from './metadataPointer/state.js';
 import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js';
 import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js';
 import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
 import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
 import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js';
 
+// Sequence from https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/mod.rs#L903
 export enum ExtensionType {
     Uninitialized,
     TransferFeeConfig,
@@ -31,6 +33,9 @@ export enum ExtensionType {
     NonTransferableAccount,
     TransferHook,
     TransferHookAccount,
+    // ConfidentialTransferFee, // Not implemented yet
+    // ConfidentialTransferFeeAmount, // Not implemented yet
+    MetadataPointer = 18, // Remove number once above extensions implemented
 }
 
 export const TYPE_SIZE = 2;
@@ -60,6 +65,8 @@ export function getTypeLen(e: ExtensionType): number {
             return IMMUTABLE_OWNER_SIZE;
         case ExtensionType.MemoTransfer:
             return MEMO_TRANSFER_SIZE;
+        case ExtensionType.MetadataPointer:
+            return METADATA_POINTER_SIZE;
         case ExtensionType.NonTransferable:
             return NON_TRANSFERABLE_SIZE;
         case ExtensionType.InterestBearingConfig:
@@ -87,6 +94,7 @@ export function isMintExtension(e: ExtensionType): boolean {
         case ExtensionType.InterestBearingConfig:
         case ExtensionType.PermanentDelegate:
         case ExtensionType.TransferHook:
+        case ExtensionType.MetadataPointer:
             return true;
         case ExtensionType.Uninitialized:
         case ExtensionType.TransferFeeAmount:
@@ -121,6 +129,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
         case ExtensionType.InterestBearingConfig:
         case ExtensionType.PermanentDelegate:
         case ExtensionType.TransferHook:
+        case ExtensionType.MetadataPointer:
             return false;
         default:
             throw Error(`Unknown extension type: ${e}`);
@@ -144,6 +153,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
         case ExtensionType.ImmutableOwner:
         case ExtensionType.MemoTransfer:
         case ExtensionType.MintCloseAuthority:
+        case ExtensionType.MetadataPointer:
         case ExtensionType.Uninitialized:
         case ExtensionType.InterestBearingConfig:
         case ExtensionType.PermanentDelegate:
diff --git a/token/js/src/extensions/index.ts b/token/js/src/extensions/index.ts
index 08aa516c5..0cb111e6e 100644
--- a/token/js/src/extensions/index.ts
+++ b/token/js/src/extensions/index.ts
@@ -5,6 +5,7 @@ export * from './extensionType.js';
 export * from './immutableOwner.js';
 export * from './interestBearingMint/index.js';
 export * from './memoTransfer/index.js';
+export * from './metadataPointer/index.js';
 export * from './mintCloseAuthority.js';
 export * from './nonTransferable.js';
 export * from './transferFee/index.js';
diff --git a/token/js/src/extensions/metadataPointer/index.ts b/token/js/src/extensions/metadataPointer/index.ts
new file mode 100644
index 000000000..8bf2a08d1
--- /dev/null
+++ b/token/js/src/extensions/metadataPointer/index.ts
@@ -0,0 +1,2 @@
+export * from './instructions.js';
+export * from './state.js';
diff --git a/token/js/src/extensions/metadataPointer/instructions.ts b/token/js/src/extensions/metadataPointer/instructions.ts
new file mode 100644
index 000000000..2d4b83541
--- /dev/null
+++ b/token/js/src/extensions/metadataPointer/instructions.ts
@@ -0,0 +1,98 @@
+import { struct, u8 } from '@solana/buffer-layout';
+import { publicKey } from '@solana/buffer-layout-utils';
+import type { Signer } from '@solana/web3.js';
+import { PublicKey, TransactionInstruction } from '@solana/web3.js';
+import { TOKEN_2022_PROGRAM_ID, programSupportsExtensions } from '../../constants.js';
+import { TokenUnsupportedInstructionError } from '../../errors.js';
+import { TokenInstruction } from '../../instructions/types.js';
+import { addSigners } from '../../instructions/internal.js';
+
+export enum MetadataPointerInstruction {
+    Initialize = 0,
+    Update = 1,
+}
+
+export const initializeMetadataPointerData = struct<{
+    instruction: TokenInstruction.MetadataPointerExtension;
+    metadataPointerInstruction: number;
+    authority: PublicKey;
+    metadataAddress: PublicKey;
+}>([
+    // prettier-ignore
+    u8('instruction'),
+    u8('metadataPointerInstruction'),
+    publicKey('authority'),
+    publicKey('metadataAddress'),
+]);
+
+/**
+ * Construct an Initialize MetadataPointer instruction
+ *
+ * @param mint            Token mint account
+ * @param authority       Optional Authority that can set the metadata address
+ * @param metadataAddress Optional Account address that holds the metadata
+ * @param programId       SPL Token program account
+ *
+ * @return Instruction to add to a transaction
+ */
+export function createInitializeMetadataPointerInstruction(
+    mint: PublicKey,
+    authority: PublicKey | null,
+    metadataAddress: PublicKey | null,
+    programId: PublicKey
+): TransactionInstruction {
+    if (!programSupportsExtensions(programId)) {
+        throw new TokenUnsupportedInstructionError();
+    }
+    const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
+
+    const data = Buffer.alloc(initializeMetadataPointerData.span);
+    initializeMetadataPointerData.encode(
+        {
+            instruction: TokenInstruction.MetadataPointerExtension,
+            metadataPointerInstruction: MetadataPointerInstruction.Initialize,
+            authority: authority ?? PublicKey.default,
+            metadataAddress: metadataAddress ?? PublicKey.default,
+        },
+        data
+    );
+
+    return new TransactionInstruction({ keys, programId, data: data });
+}
+
+export const updateMetadataPointerData = struct<{
+    instruction: TokenInstruction.MetadataPointerExtension;
+    metadataPointerInstruction: number;
+    metadataAddress: PublicKey;
+}>([
+    // prettier-ignore
+    u8('instruction'),
+    u8('metadataPointerInstruction'),
+    publicKey('metadataAddress'),
+]);
+
+export function createUpdateMetadataPointerInstruction(
+    mint: PublicKey,
+    authority: PublicKey,
+    metadataAddress: PublicKey | null,
+    multiSigners: (Signer | PublicKey)[] = [],
+    programId: PublicKey = TOKEN_2022_PROGRAM_ID
+): TransactionInstruction {
+    if (!programSupportsExtensions(programId)) {
+        throw new TokenUnsupportedInstructionError();
+    }
+
+    const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners);
+
+    const data = Buffer.alloc(updateMetadataPointerData.span);
+    updateMetadataPointerData.encode(
+        {
+            instruction: TokenInstruction.MetadataPointerExtension,
+            metadataPointerInstruction: MetadataPointerInstruction.Update,
+            metadataAddress: metadataAddress ?? PublicKey.default,
+        },
+        data
+    );
+
+    return new TransactionInstruction({ keys, programId, data: data });
+}
diff --git a/token/js/src/extensions/metadataPointer/state.ts b/token/js/src/extensions/metadataPointer/state.ts
new file mode 100644
index 000000000..5eb319818
--- /dev/null
+++ b/token/js/src/extensions/metadataPointer/state.ts
@@ -0,0 +1,36 @@
+import { struct } from '@solana/buffer-layout';
+import { publicKey } from '@solana/buffer-layout-utils';
+import { PublicKey } from '@solana/web3.js';
+import type { Mint } from '../../state/mint.js';
+import { ExtensionType, getExtensionData } from '../extensionType.js';
+
+/** MetadataPointer as stored by the program */
+export interface MetadataPointer {
+    /** Optional authority that can set the metadata address */
+    authority: PublicKey | null;
+    /** Optional Account Address that holds the metadata */
+    metadataAddress: PublicKey | null;
+}
+
+/** Buffer layout for de/serializing a Metadata Pointer extension */
+export const MetadataPointerLayout = struct<{ authority: PublicKey; metadataAddress: PublicKey }>([
+    publicKey('authority'),
+    publicKey('metadataAddress'),
+]);
+
+export const METADATA_POINTER_SIZE = MetadataPointerLayout.span;
+
+export function getMetadataPointerState(mint: Mint): Partial<MetadataPointer> | null {
+    const extensionData = getExtensionData(ExtensionType.MetadataPointer, mint.tlvData);
+    if (extensionData !== null) {
+        const { authority, metadataAddress } = MetadataPointerLayout.decode(extensionData);
+
+        // Explicity set None/Zero keys to null
+        return {
+            authority: authority.equals(PublicKey.default) ? null : authority,
+            metadataAddress: metadataAddress.equals(PublicKey.default) ? null : metadataAddress,
+        };
+    } else {
+        return null;
+    }
+}
diff --git a/token/js/src/instructions/types.ts b/token/js/src/instructions/types.ts
index 4448cbf34..2a60bb041 100644
--- a/token/js/src/instructions/types.ts
+++ b/token/js/src/instructions/types.ts
@@ -37,4 +37,7 @@ export enum TokenInstruction {
     CpiGuardExtension = 34,
     InitializePermanentDelegate = 35,
     TransferHookExtension = 36,
+    // ConfidentialTransferFeeExtension = 37,
+    // WithdrawalExcessLamports = 38,
+    MetadataPointerExtension = 39,
 }


Test patch:
diff --git a/token/js/test/e2e-2022/metadataPointer.test.ts b/token/js/test/e2e-2022/metadataPointer.test.ts
new file mode 100644
index 000000000..18eeeefa9
--- /dev/null
+++ b/token/js/test/e2e-2022/metadataPointer.test.ts
@@ -0,0 +1,97 @@
+import { expect } from 'chai';
+import type { Connection, Signer } from '@solana/web3.js';
+import { PublicKey } from '@solana/web3.js';
+import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js';
+
+import {
+    ExtensionType,
+    createInitializeMetadataPointerInstruction,
+    createInitializeMintInstruction,
+    createUpdateMetadataPointerInstruction,
+    getMetadataPointerState,
+    getMint,
+    getMintLen,
+} from '../../src';
+import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
+
+const TEST_TOKEN_DECIMALS = 2;
+const EXTENSIONS = [ExtensionType.MetadataPointer];
+
+describe('Metadata pointer', () => {
+    let connection: Connection;
+    let payer: Signer;
+    let mint: Keypair;
+    let mintAuthority: Keypair;
+    let metadataAddress: PublicKey;
+
+    before(async () => {
+        connection = await getConnection();
+        payer = await newAccountWithLamports(connection, 1000000000);
+        mintAuthority = Keypair.generate();
+    });
+
+    beforeEach(async () => {
+        mint = Keypair.generate();
+        metadataAddress = PublicKey.unique();
+
+        const mintLen = getMintLen(EXTENSIONS);
+        const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
+
+        const transaction = new Transaction().add(
+            SystemProgram.createAccount({
+                fromPubkey: payer.publicKey,
+                newAccountPubkey: mint.publicKey,
+                space: mintLen,
+                lamports,
+                programId: TEST_PROGRAM_ID,
+            }),
+            createInitializeMetadataPointerInstruction(
+                mint.publicKey,
+                mintAuthority.publicKey,
+                metadataAddress,
+                TEST_PROGRAM_ID
+            ),
+            createInitializeMintInstruction(
+                mint.publicKey,
+                TEST_TOKEN_DECIMALS,
+                mintAuthority.publicKey,
+                null,
+                TEST_PROGRAM_ID
+            )
+        );
+
+        await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined);
+    });
+
+    it('can successfully initialize', async () => {
+        const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID);
+        const metadataPointer = getMetadataPointerState(mintInfo);
+
+        expect(metadataPointer).to.deep.equal({
+            authority: mintAuthority.publicKey,
+            metadataAddress,
+        });
+    });
+
+    it('can update to new address', async () => {
+        const newMetadataAddress = PublicKey.unique();
+        const transaction = new Transaction().add(
+            createUpdateMetadataPointerInstruction(
+                mint.publicKey,
+                mintAuthority.publicKey,
+                newMetadataAddress,
+                undefined,
+                TEST_PROGRAM_ID
+            )
+        );
+        await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined);
+
+        const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID);
+        const metadataPointer = getMetadataPointerState(mintInfo);
+
+        expect(metadataPointer).to.deep.equal({
+            authority: mintAuthority.publicKey,
+            metadataAddress: newMetadataAddress,
+        });
+    });
+});
diff --git a/token/js/test/unit/metadataPointer.test.ts b/token/js/test/unit/metadataPointer.test.ts
new file mode 100644
index 000000000..d7469421a
--- /dev/null
+++ b/token/js/test/unit/metadataPointer.test.ts
@@ -0,0 +1,141 @@
+import { PublicKey, TransactionInstruction } from '@solana/web3.js';
+import { expect } from 'chai';
+import type { Mint } from '../../src';
+import {
+    TOKEN_2022_PROGRAM_ID,
+    createInitializeMetadataPointerInstruction,
+    createUpdateMetadataPointerInstruction,
+    getMetadataPointerState,
+} from '../../src';
+
+describe('SPL Token 2022 Metadata Extension', () => {
+    it('can create createInitializeMetadataPointerInstruction', () => {
+        const mint = PublicKey.unique();
+        const authority = new PublicKey('1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM');
+        const metadataAddress = new PublicKey('1111111ogCyDbaRMvkdsHB3qfdyFYaG1WtRUAfdh');
+
+        const instruction = createInitializeMetadataPointerInstruction(
+            mint,
+            authority,
+            metadataAddress,
+            TOKEN_2022_PROGRAM_ID
+        );
+
+        expect(instruction).to.deep.equal(
+            new TransactionInstruction({
+                programId: TOKEN_2022_PROGRAM_ID,
+                keys: [{ isSigner: false, isWritable: true, pubkey: mint }],
+                data: Buffer.from([
+                    // Output of rust implementation
+                    39, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                ]),
+            })
+        );
+    });
+
+    it('can create createUpdateMetadataPointerInstruction', () => {
+        const mint = PublicKey.unique();
+        const authority = PublicKey.unique();
+        const metadataAddress = new PublicKey('11111112cMQwSC9qirWGjZM6gLGwW69X22mqwLLGP');
+
+        const instruction = createUpdateMetadataPointerInstruction(mint, authority, metadataAddress);
+
+        expect(instruction).to.deep.equal(
+            new TransactionInstruction({
+                programId: TOKEN_2022_PROGRAM_ID,
+                keys: [
+                    { isSigner: false, isWritable: true, pubkey: mint },
+                    { isSigner: true, isWritable: false, pubkey: authority },
+                ],
+                data: Buffer.from([
+                    // Output of rust implementation
+                    39, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0,
+                ]),
+            })
+        );
+    });
+
+    it('can create createUpdateMetadataPointerInstruction to none', () => {
+        const mint = PublicKey.unique();
+        const authority = PublicKey.unique();
+        const metadataAddress = null;
+
+        const instruction = createUpdateMetadataPointerInstruction(mint, authority, metadataAddress);
+
+        expect(instruction).to.deep.equal(
+            new TransactionInstruction({
+                programId: TOKEN_2022_PROGRAM_ID,
+                keys: [
+                    { isSigner: false, isWritable: true, pubkey: mint },
+                    { isSigner: true, isWritable: false, pubkey: authority },
+                ],
+                data: Buffer.from([
+                    // Output of rust implementation
+                    39, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0,
+                ]),
+            })
+        );
+    });
+
+    it('can get state with authority and metadata address', async () => {
+        const mintInfo = {
+            tlvData: Buffer.from([
+                18, 0, 64, 0, 134, 125, 9, 16, 205, 223, 26, 224, 220, 174, 52, 213, 193, 216, 9, 80, 82, 181, 8, 228,
+                75, 112, 233, 116, 2, 183, 51, 228, 88, 64, 179, 158, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+            ]),
+        } as Mint;
+
+        const metadataPointer = getMetadataPointerState(mintInfo);
+
+        expect(metadataPointer).to.deep.equal({
+            authority: new PublicKey([
+                134, 125, 9, 16, 205, 223, 26, 224, 220, 174, 52, 213, 193, 216, 9, 80, 82, 181, 8, 228, 75, 112, 233,
+                116, 2, 183, 51, 228, 88, 64, 179, 158,
+            ]),
+            metadataAddress: new PublicKey([
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+            ]),
+        });
+    });
+    it('can get state with only metadata address', async () => {
+        const mintInfo = {
+            tlvData: Buffer.from([
+                18, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+            ]),
+        } as Mint;
+
+        const metadataPointer = getMetadataPointerState(mintInfo);
+
+        expect(metadataPointer).to.deep.equal({
+            authority: null,
+            metadataAddress: new PublicKey([
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+            ]),
+        });
+    });
+
+    it('can get state with only authority address', async () => {
+        const mintInfo = {
+            tlvData: Buffer.from([
+                18, 0, 64, 0, 16, 218, 238, 42, 17, 19, 152, 173, 216, 24, 229, 204, 215, 108, 49, 98, 233, 115, 53,
+                252, 9, 156, 216, 23, 14, 157, 139, 132, 28, 182, 4, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+            ]),
+        } as Mint;
+
+        const metadataPointer = getMetadataPointerState(mintInfo);
+
+        expect(metadataPointer).to.deep.equal({
+            authority: new PublicKey([
+                16, 218, 238, 42, 17, 19, 152, 173, 216, 24, 229, 204, 215, 108, 49, 98, 233, 115, 53, 252, 9, 156, 216,
+                23, 14, 157, 139, 132, 28, 182, 4, 191,
+            ]),
+            metadataAddress: null,
+        });
+    });
+});