Skip to content

Commit

Permalink
token 2022: Expose token-metadata state decoder in @solana/token-meta…
Browse files Browse the repository at this point in the history
…ta (#5806)

* only handle deserialization of state, no discriminator or length

* fix test case name

* addressed pr comments

* don't export codec

* Added test case with additional metadata

* remove comment
  • Loading branch information
mistersimon authored Nov 14, 2023
1 parent 8c8523d commit b5e20aa
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 78 deletions.
44 changes: 24 additions & 20 deletions token-metadata/js/src/state.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { getArrayDecoder, getBytesDecoder, getStructDecoder, getTupleDecoder } from '@solana/codecs-data-structures';
import { getStringDecoder } from '@solana/codecs-strings';
import { TlvState } from '@solana/spl-type-length-value';
import { PublicKey } from '@solana/web3.js';

import { TokenMetadataError } from './errors.js';
import { getArrayCodec, getBytesCodec, getStructCodec, getTupleCodec } from '@solana/codecs-data-structures';
import { getStringCodec } from '@solana/codecs-strings';

export const TOKEN_METADATA_DISCRIMINATOR = Buffer.from([112, 132, 90, 90, 11, 88, 157, 87]);

const tokenMetadataCodec = getStructCodec([
['updateAuthority', getBytesCodec({ size: 32 })],
['mint', getBytesCodec({ size: 32 })],
['name', getStringCodec()],
['symbol', getStringCodec()],
['uri', getStringCodec()],
['additionalMetadata', getArrayCodec(getTupleCodec([getStringCodec(), getStringCodec()]))],
]);

export interface TokenMetadata {
// The authority that can sign to update the metadata
updateAuthority?: PublicKey;
Expand All @@ -32,22 +38,20 @@ function isNonePubkey(buffer: Uint8Array): boolean {
return true;
}

export function unpack(buffer: Buffer): TokenMetadata {
const tlv = new TlvState(buffer, 8, 4);
const bytes = tlv.firstBytes(TOKEN_METADATA_DISCRIMINATOR);
if (bytes === null) {
throw new TokenMetadataError('Invalid Data');
}
const decoder = getStructDecoder([
['updateAuthority', getBytesDecoder({ size: 32 })],
['mint', getBytesDecoder({ size: 32 })],
['name', getStringDecoder()],
['symbol', getStringDecoder()],
['uri', getStringDecoder()],
['additionalMetadata', getArrayDecoder(getTupleDecoder([getStringDecoder(), getStringDecoder()]))],
]);
// Pack TokenMetadata into byte slab
export const pack = (meta: TokenMetadata): Uint8Array => {
// If no updateAuthority given, set it to the None/Zero PublicKey for encoding
const updateAuthority = meta.updateAuthority ?? PublicKey.default;
return tokenMetadataCodec.encode({
...meta,
updateAuthority: updateAuthority.toBuffer(),
mint: meta.mint.toBuffer(),
});
};

const data = decoder.decode(bytes);
// unpack byte slab into TokenMetadata
export function unpack(buffer: Buffer | Uint8Array): TokenMetadata {
const data = tokenMetadataCodec.decode(buffer);

return isNonePubkey(data[0].updateAuthority)
? {
Expand Down
116 changes: 58 additions & 58 deletions token-metadata/js/test/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,31 @@ import { PublicKey } from '@solana/web3.js';
import { expect } from 'chai';

import type { TokenMetadata } from '../src/state';
import { TOKEN_METADATA_DISCRIMINATOR, unpack } from '../src/state';
import { getArrayEncoder, getBytesEncoder, getStructEncoder, getTupleEncoder } from '@solana/codecs-data-structures';
import { getStringEncoder } from '@solana/codecs-strings';
import { unpack, pack } from '../src';

describe('Token Metadata State', () => {
const lengthBuffer = (buffer: Buffer | Uint8Array): Buffer => {
const length = Buffer.alloc(4);
length.writeUIntLE(buffer.length, 0, 4);
return length;
};

// Helper function to pack meta into tlv bytes slab
const pack = (meta: TokenMetadata) => {
const encoder = getStructEncoder([
['updateAuthority', getBytesEncoder({ size: 32 })],
['mint', getBytesEncoder({ size: 32 })],
['name', getStringEncoder()],
['symbol', getStringEncoder()],
['uri', getStringEncoder()],
['additionalMetadata', getArrayEncoder(getTupleEncoder([getStringEncoder(), getStringEncoder()]))],
]);
const data = encoder.encode({
...meta,
updateAuthority: meta.updateAuthority?.toBuffer(),
mint: meta.mint.toBuffer(),
});
return Buffer.concat([TOKEN_METADATA_DISCRIMINATOR, lengthBuffer(data), data]);
};

it('Can unpack', () => {
const data = Buffer.from([
// From rust implementation
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, 4, 0, 0, 0, 110, 97,
109, 101, 6, 0, 0, 0, 115, 121, 109, 98, 111, 108, 3, 0, 0, 0, 117, 114, 105, 0, 0, 0, 0,
]);

const input = Buffer.concat([TOKEN_METADATA_DISCRIMINATOR, lengthBuffer(data), data]);

const meta = unpack(input);
expect(meta).to.deep.equal({
it('Can pack and unpack as rust implementation', () => {
const meta = {
mint: PublicKey.default,
name: 'name',
symbol: 'symbol',
uri: 'uri',
additionalMetadata: [],
});
});
};

it('Can unpack with additionalMetadata', () => {
const data = Buffer.from([
// From rust implementation
// From rust implementation
const bytes = Buffer.from([
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, 8, 0, 0, 0, 110, 101,
119, 95, 110, 97, 109, 101, 10, 0, 0, 0, 110, 101, 119, 95, 115, 121, 109, 98, 111, 108, 7, 0, 0, 0, 110,
101, 119, 95, 117, 114, 105, 2, 0, 0, 0, 4, 0, 0, 0, 107, 101, 121, 49, 6, 0, 0, 0, 118, 97, 108, 117, 101,
49, 4, 0, 0, 0, 107, 101, 121, 50, 6, 0, 0, 0, 118, 97, 108, 117, 101, 50,
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, 4, 0, 0, 0, 110, 97,
109, 101, 6, 0, 0, 0, 115, 121, 109, 98, 111, 108, 3, 0, 0, 0, 117, 114, 105, 0, 0, 0, 0,
]);

const input = Buffer.concat([TOKEN_METADATA_DISCRIMINATOR, lengthBuffer(data), data]);
const meta = unpack(input);
expect(meta).to.deep.equal({
expect(pack(meta)).to.deep.equal(bytes);
expect(unpack(bytes)).to.deep.equal(meta);
});

it('Can pack and unpack as rust implementation with additionalMetadata', () => {
const meta: TokenMetadata = {
mint: PublicKey.default,
name: 'new_name',
symbol: 'new_symbol',
Expand All @@ -72,27 +35,64 @@ describe('Token Metadata State', () => {
['key1', 'value1'],
['key2', 'value2'],
],
});
};
// From rust implementation
const bytes = Buffer.from([
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, 8, 0, 0, 0, 110, 101,
119, 95, 110, 97, 109, 101, 10, 0, 0, 0, 110, 101, 119, 95, 115, 121, 109, 98, 111, 108, 7, 0, 0, 0, 110,
101, 119, 95, 117, 114, 105, 2, 0, 0, 0, 4, 0, 0, 0, 107, 101, 121, 49, 6, 0, 0, 0, 118, 97, 108, 117, 101,
49, 4, 0, 0, 0, 107, 101, 121, 50, 6, 0, 0, 0, 118, 97, 108, 117, 101, 50,
]);

expect(pack(meta)).to.deep.equal(bytes);
expect(unpack(bytes)).to.deep.equal(meta);
});

it('Can pack and unpack with mint and updateAuthority', () => {
const input = pack({
const meta = {
updateAuthority: new PublicKey('44444444444444444444444444444444444444444444'),
mint: new PublicKey('55555555555555555555555555555555555555555555'),
name: 'name',
symbol: 'symbol',
uri: 'uri',
additionalMetadata: [],
});
};

const bytes = Buffer.from([
45, 91, 65, 60, 101, 64, 222, 21, 12, 147, 115, 20, 77, 81, 51, 202, 76, 184, 48, 186, 15, 117, 103, 22,
172, 234, 14, 80, 215, 148, 53, 229, 60, 121, 172, 80, 135, 1, 40, 28, 16, 196, 153, 112, 103, 22, 239, 184,
102, 74, 235, 162, 191, 71, 52, 30, 59, 226, 189, 193, 31, 112, 71, 220, 4, 0, 0, 0, 110, 97, 109, 101, 6,
0, 0, 0, 115, 121, 109, 98, 111, 108, 3, 0, 0, 0, 117, 114, 105, 0, 0, 0, 0,
]);

expect(pack(meta)).to.deep.equal(bytes);
expect(unpack(bytes)).to.deep.equal(meta);
});

const meta = unpack(input);
expect(meta).to.deep.equal({
it('Can pack and unpack with mint, updateAuthority and additional metadata', () => {
const meta: TokenMetadata = {
updateAuthority: new PublicKey('44444444444444444444444444444444444444444444'),
mint: new PublicKey('55555555555555555555555555555555555555555555'),
name: 'name',
symbol: 'symbol',
uri: 'uri',
additionalMetadata: [],
});
additionalMetadata: [
['key1', 'value1'],
['key2', 'value2'],
],
};

const bytes = Buffer.from([
45, 91, 65, 60, 101, 64, 222, 21, 12, 147, 115, 20, 77, 81, 51, 202, 76, 184, 48, 186, 15, 117, 103, 22,
172, 234, 14, 80, 215, 148, 53, 229, 60, 121, 172, 80, 135, 1, 40, 28, 16, 196, 153, 112, 103, 22, 239, 184,
102, 74, 235, 162, 191, 71, 52, 30, 59, 226, 189, 193, 31, 112, 71, 220, 4, 0, 0, 0, 110, 97, 109, 101, 6,
0, 0, 0, 115, 121, 109, 98, 111, 108, 3, 0, 0, 0, 117, 114, 105, 2, 0, 0, 0, 4, 0, 0, 0, 107, 101, 121, 49,
6, 0, 0, 0, 118, 97, 108, 117, 101, 49, 4, 0, 0, 0, 107, 101, 121, 50, 6, 0, 0, 0, 118, 97, 108, 117, 101,
50,
]);

expect(pack(meta)).to.deep.equal(bytes);
expect(unpack(bytes)).to.deep.equal(meta);
});
});

0 comments on commit b5e20aa

Please sign in to comment.