Repository: solana-labs/solana-program-library
Pull request #6666 (2024-04-30 11:17:10)

token-metadata-js: Upgrade to tp3
#### Problem

token-metadata-js is still on TP2, even though TP3 is out.

#### Solution

Upgrade to TP3! This one is very similar to #6665, but the biggest change that I have a question about is whether the `additionalMetadata` in the metadata struct should be `readonly`.


Patch:
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8c1f79c72..d7b3c0f81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -632,8 +632,8 @@ importers:
   token-metadata/js:
     dependencies:
       '@solana/codecs':
-        specifier: 2.0.0-preview.2
-        version: 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22)
+        specifier: 2.0.0-preview.3
+        version: 2.0.0-preview.3(fastestsmallesttextencoderdecoder@1.0.22)
       '@solana/spl-type-length-value':
         specifier: 0.1.0
         version: link:../../libraries/type-length-value/js
@@ -773,7 +773,7 @@ importers:
         version: link:../../token-group/js
       '@solana/spl-token-metadata':
         specifier: ^0.1.3
-        version: 0.1.4(@solana/web3.js@1.91.7)(fastestsmallesttextencoderdecoder@1.0.22)
+        version: link:../../token-metadata/js
       buffer:
         specifier: ^6.0.3
         version: 6.0.3
@@ -2363,7 +2363,6 @@ packages:
     resolution: {integrity: sha512-xQz6USSBs82lUNoVa/wwnm6wa2y2eWtGwPLUwF/NOGGpR+QH9EODijXvJ8wuC9llyqerqdC+5mrmx9C8VSMNYg==}
     dependencies:
       '@solana/errors': 2.0.0-preview.3
-    dev: true
 
   /@solana/codecs-data-structures@2.0.0-preview.2:
     resolution: {integrity: sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg==}
@@ -2372,6 +2371,14 @@ packages:
       '@solana/codecs-numbers': 2.0.0-preview.2
       '@solana/errors': 2.0.0-preview.2
 
+  /@solana/codecs-data-structures@2.0.0-preview.3:
+    resolution: {integrity: sha512-PfXvZCf9qDF+Dv4WG6cb4xZoY9tj117bmZWS17iKimuNSsvuFSHpzERy0mmX2hwYEAM4CnQBd/9dgx+eAeMAsg==}
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.3
+      '@solana/codecs-numbers': 2.0.0-preview.3
+      '@solana/errors': 2.0.0-preview.3
+    dev: false
+
   /@solana/codecs-numbers@2.0.0-preview.2:
     resolution: {integrity: sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw==}
     dependencies:
@@ -2383,7 +2390,6 @@ packages:
     dependencies:
       '@solana/codecs-core': 2.0.0-preview.3
       '@solana/errors': 2.0.0-preview.3
-    dev: true
 
   /@solana/codecs-strings@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22):
     resolution: {integrity: sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g==}
@@ -2404,7 +2410,6 @@ packages:
       '@solana/codecs-numbers': 2.0.0-preview.3
       '@solana/errors': 2.0.0-preview.3
       fastestsmallesttextencoderdecoder: 1.0.22
-    dev: true
 
   /@solana/codecs@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22):
     resolution: {integrity: sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA==}
@@ -2417,6 +2422,18 @@ packages:
     transitivePeerDependencies:
       - fastestsmallesttextencoderdecoder
 
+  /@solana/codecs@2.0.0-preview.3(fastestsmallesttextencoderdecoder@1.0.22):
+    resolution: {integrity: sha512-uB0GMAY1VrNoJxZ9S4F1RBL57gI+8YwxnV9DD5EP5rU8iD7Wq4wbaB2IPcENyJi7rmzytIjKJg0MI6i2bBr+0w==}
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.3
+      '@solana/codecs-data-structures': 2.0.0-preview.3
+      '@solana/codecs-numbers': 2.0.0-preview.3
+      '@solana/codecs-strings': 2.0.0-preview.3(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/options': 2.0.0-preview.3(fastestsmallesttextencoderdecoder@1.0.22)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+    dev: false
+
   /@solana/errors@2.0.0-preview.2:
     resolution: {integrity: sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA==}
     hasBin: true
@@ -2430,7 +2447,6 @@ packages:
     dependencies:
       chalk: 5.3.0
       commander: 12.0.0
-    dev: true
 
   /@solana/eslint-config-solana@3.0.3(@typescript-eslint/eslint-plugin@6.21.0)(@typescript-eslint/parser@6.21.0)(eslint-plugin-jest@27.9.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0)(eslint@8.57.0)(typescript@5.4.5):
     resolution: {integrity: sha512-yTaeCbOBwjmK4oUkknixOpwOzzAK8+4YWvJEJFNHuueESetieDnAeEHV7rzJllFgHEWa9nXps9Q3aD4/XJp71A==}
@@ -2500,6 +2516,18 @@ packages:
       '@solana/codecs-core': 2.0.0-preview.2
       '@solana/codecs-numbers': 2.0.0-preview.2
 
+  /@solana/options@2.0.0-preview.3(fastestsmallesttextencoderdecoder@1.0.22):
+    resolution: {integrity: sha512-tT5O1CCJVE+rzo4VeeivYLNUL4L/2BjIeiy0MRh04lPxieiR346vUOPC1uCWGD6WqyTOOVUL0tsY4saYLmCTtA==}
+    dependencies:
+      '@solana/codecs-core': 2.0.0-preview.3
+      '@solana/codecs-data-structures': 2.0.0-preview.3
+      '@solana/codecs-numbers': 2.0.0-preview.3
+      '@solana/codecs-strings': 2.0.0-preview.3(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/errors': 2.0.0-preview.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+    dev: false
+
   /@solana/prettier-config-solana@0.0.5(prettier@3.2.5):
     resolution: {integrity: sha512-igtLH1QaX5xzSLlqteexRIg9X1QKA03xKYQc2qY1TrMDDhxKXoRZOStQPWdita2FVJzxTGz/tdMGC1vS0biRcg==}
     peerDependencies:
diff --git a/token-metadata/js/package.json b/token-metadata/js/package.json
index 91aeee418..cba823d55 100644
--- a/token-metadata/js/package.json
+++ b/token-metadata/js/package.json
@@ -47,7 +47,7 @@
         "@solana/web3.js": "^1.91.7"
     },
     "dependencies": {
-        "@solana/codecs": "2.0.0-preview.2",
+        "@solana/codecs": "2.0.0-preview.3",
         "@solana/spl-type-length-value": "0.1.0"
     },
     "devDependencies": {
diff --git a/token-metadata/js/src/field.ts b/token-metadata/js/src/field.ts
index 3e9c7aabe..58db19433 100644
--- a/token-metadata/js/src/field.ts
+++ b/token-metadata/js/src/field.ts
@@ -1,5 +1,12 @@
 import type { Codec } from '@solana/codecs';
-import { getStringCodec, getStructCodec, getTupleCodec, getUnitCodec } from '@solana/codecs';
+import {
+    addCodecSizePrefix,
+    getU32Codec,
+    getUtf8Codec,
+    getStructCodec,
+    getTupleCodec,
+    getUnitCodec,
+} from '@solana/codecs';
 
 export enum Field {
     Name,
@@ -14,7 +21,7 @@ export const getFieldCodec = () =>
         ['Name', getUnitCodec()],
         ['Symbol', getUnitCodec()],
         ['Uri', getUnitCodec()],
-        ['Key', getStructCodec([['value', getTupleCodec([getStringCodec()])]])],
+        ['Key', getStructCodec([['value', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])]])],
     ] as const;
 
 export function getFieldConfig(field: Field | string): FieldLayout {
diff --git a/token-metadata/js/src/instruction.ts b/token-metadata/js/src/instruction.ts
index 10493bfb0..b428d4fef 100644
--- a/token-metadata/js/src/instruction.ts
+++ b/token-metadata/js/src/instruction.ts
@@ -1,15 +1,19 @@
 import type { Encoder } from '@solana/codecs';
 import {
+    addEncoderSizePrefix,
+    fixEncoderSize,
     getBooleanEncoder,
     getBytesEncoder,
     getDataEnumCodec,
     getOptionEncoder,
-    getStringEncoder,
+    getUtf8Encoder,
     getStructEncoder,
     getTupleEncoder,
+    getU32Encoder,
     getU64Encoder,
-    mapEncoder,
+    transformEncoder,
 } from '@solana/codecs';
+import type { VariableSizeEncoder } from '@solana/codecs';
 import { splDiscriminate } from '@solana/spl-type-length-value';
 import type { PublicKey } from '@solana/web3.js';
 import { SystemProgram, TransactionInstruction } from '@solana/web3.js';
@@ -18,14 +22,18 @@ import type { Field } from './field.js';
 import { getFieldCodec, getFieldConfig } from './field.js';
 
 function getInstructionEncoder<T extends object>(discriminator: Uint8Array, dataEncoder: Encoder<T>): Encoder<T> {
-    return mapEncoder(getTupleEncoder([getBytesEncoder(), dataEncoder]), (data: T): [Uint8Array, T] => [
+    return transformEncoder(getTupleEncoder([getBytesEncoder(), dataEncoder]), (data: T): [Uint8Array, T] => [
         discriminator,
         data,
     ]);
 }
 
 function getPublicKeyEncoder(): Encoder<PublicKey> {
-    return mapEncoder(getBytesEncoder({ size: 32 }), (publicKey: PublicKey) => publicKey.toBytes());
+    return transformEncoder(fixEncoderSize(getBytesEncoder(), 32), (publicKey: PublicKey) => publicKey.toBytes());
+}
+
+function getStringEncoder(): VariableSizeEncoder<string> {
+    return addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder());
 }
 
 /**
diff --git a/token-metadata/js/src/state.ts b/token-metadata/js/src/state.ts
index 5583a6bae..6f689ebd8 100644
--- a/token-metadata/js/src/state.ts
+++ b/token-metadata/js/src/state.ts
@@ -1,11 +1,25 @@
 import { PublicKey } from '@solana/web3.js';
-import { getArrayCodec, getBytesCodec, getStringCodec, getStructCodec, getTupleCodec } from '@solana/codecs';
+import {
+    addCodecSizePrefix,
+    fixCodecSize,
+    getArrayCodec,
+    getBytesCodec,
+    getUtf8Codec,
+    getU32Codec,
+    getStructCodec,
+    getTupleCodec,
+} from '@solana/codecs';
+import type { ReadonlyUint8Array, VariableSizeCodec } from '@solana/codecs';
 
 export const TOKEN_METADATA_DISCRIMINATOR = Buffer.from([112, 132, 90, 90, 11, 88, 157, 87]);
 
+function getStringCodec(): VariableSizeCodec<string> {
+    return addCodecSizePrefix(getUtf8Codec(), getU32Codec());
+}
+
 const tokenMetadataCodec = getStructCodec([
-    ['updateAuthority', getBytesCodec({ size: 32 })],
-    ['mint', getBytesCodec({ size: 32 })],
+    ['updateAuthority', fixCodecSize(getBytesCodec(), 32)],
+    ['mint', fixCodecSize(getBytesCodec(), 32)],
     ['name', getStringCodec()],
     ['symbol', getStringCodec()],
     ['uri', getStringCodec()],
@@ -24,11 +38,11 @@ export interface TokenMetadata {
     // The URI pointing to richer metadata
     uri: string;
     // Any additional metadata about the token as key-value pairs
-    additionalMetadata: [string, string][];
+    additionalMetadata: (readonly [string, string])[];
 }
 
 // Checks if all elements in the array are 0
-function isNonePubkey(buffer: Uint8Array): boolean {
+function isNonePubkey(buffer: ReadonlyUint8Array): boolean {
     for (let i = 0; i < buffer.length; i++) {
         if (buffer[i] !== 0) {
             return false;
@@ -38,7 +52,7 @@ function isNonePubkey(buffer: Uint8Array): boolean {
 }
 
 // Pack TokenMetadata into byte slab
-export function pack(meta: TokenMetadata): Uint8Array {
+export function pack(meta: TokenMetadata): ReadonlyUint8Array {
     // If no updateAuthority given, set it to the None/Zero PublicKey for encoding
     const updateAuthority = meta.updateAuthority ?? PublicKey.default;
     return tokenMetadataCodec.encode({
@@ -49,7 +63,7 @@ export function pack(meta: TokenMetadata): Uint8Array {
 }
 
 // unpack byte slab into TokenMetadata
-export function unpack(buffer: Buffer | Uint8Array): TokenMetadata {
+export function unpack(buffer: Buffer | Uint8Array | ReadonlyUint8Array): TokenMetadata {
     const data = tokenMetadataCodec.decode(buffer);
 
     return isNonePubkey(data.updateAuthority)


Test patch:
diff --git a/token-metadata/js/test/instruction.test.ts b/token-metadata/js/test/instruction.test.ts
index 442bbe4d8..e206393c7 100644
--- a/token-metadata/js/test/instruction.test.ts
+++ b/token-metadata/js/test/instruction.test.ts
@@ -10,17 +10,20 @@ import {
     getFieldConfig,
 } from '../src';
 import {
+    addDecoderSizePrefix,
+    fixDecoderSize,
     getBooleanDecoder,
     getBytesDecoder,
     getDataEnumCodec,
     getOptionDecoder,
-    getStringDecoder,
+    getUtf8Decoder,
+    getU32Decoder,
     getU64Decoder,
     getStructDecoder,
     some,
 } from '@solana/codecs';
 import { splDiscriminate } from '@solana/spl-type-length-value';
-import type { Decoder, Option } from '@solana/codecs';
+import type { Decoder, Option, VariableSizeDecoder } from '@solana/codecs';
 import { PublicKey, type TransactionInstruction } from '@solana/web3.js';
 
 function checkPackUnpack<T extends object>(
@@ -34,6 +37,10 @@ function checkPackUnpack<T extends object>(
     expect(unpacked).to.deep.equal(values);
 }
 
+function getStringDecoder(): VariableSizeDecoder<string> {
+    return addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder());
+}
+
 describe('Token Metadata Instructions', () => {
     const programId = new PublicKey('22222222222222222222222222222222222222222222');
     const metadata = new PublicKey('33333333333333333333333333333333333333333333');
@@ -134,7 +141,7 @@ describe('Token Metadata Instructions', () => {
                 newAuthority,
             }),
             splDiscriminate('spl_token_metadata_interface:update_the_authority'),
-            getStructDecoder([['newAuthority', getBytesDecoder({ size: 32 })]]),
+            getStructDecoder([['newAuthority', fixDecoderSize(getBytesDecoder(), 32)]]),
             { newAuthority: Uint8Array.from(newAuthority.toBuffer()) }
         );
     });