Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 47a646c

Browse files
Adding Token Metadata Support to @solana/spl-token (#5667)
* added init, update and emit token metadata actions * added init with rent transfer, remote key, update authority * add updateTokenMetadata state * fixed tokenMetadataUpdateFieldWithRentTransfer * removed getEmittedTokenMetadata and made error more verbose * split additional rent into new and update * factored out generic functions for getting account len * simplified functions * undo order change to simplify diff * clean up based on review comments * Refactored to unroll rent calculations for update and new into separate functions * added addTypeandLengthtoLen function * tidying up & refactoring tests --------- Co-authored-by: Joe <[email protected]>
1 parent ef44df9 commit 47a646c

File tree

10 files changed

+1272
-13
lines changed

10 files changed

+1272
-13
lines changed

pnpm-lock.yaml

Lines changed: 10 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/js/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@
5656
"dependencies": {
5757
"@solana/buffer-layout": "^4.0.0",
5858
"@solana/buffer-layout-utils": "^0.2.0",
59+
"@solana/spl-token-metadata": "^0.1.2",
5960
"buffer": "^6.0.3"
6061
},
6162
"devDependencies": {
63+
"@solana/codecs-strings": "2.0.0-experimental.9741939",
6264
"@solana/spl-memo": "0.2.3",
6365
"@solana/web3.js": "^1.87.6",
6466
"@types/chai-as-promised": "^7.1.4",

token/js/src/extensions/extensionType.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { AccountInfo, PublicKey } from '@solana/web3.js';
2+
13
import { ACCOUNT_SIZE } from '../state/account.js';
24
import type { Mint } from '../state/mint.js';
3-
import { MINT_SIZE } from '../state/mint.js';
5+
import { MINT_SIZE, unpackMint } from '../state/mint.js';
46
import { MULTISIG_SIZE } from '../state/multisig.js';
57
import { ACCOUNT_TYPE_SIZE } from './accountType.js';
68
import { CPI_GUARD_SIZE } from './cpiGuard/index.js';
@@ -14,6 +16,7 @@ import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTrans
1416
import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
1517
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
1618
import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js';
19+
import { TOKEN_2022_PROGRAM_ID } from '../constants.js';
1720

1821
// Sequence from https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/mod.rs#L903
1922
export enum ExtensionType {
@@ -36,11 +39,16 @@ export enum ExtensionType {
3639
// ConfidentialTransferFee, // Not implemented yet
3740
// ConfidentialTransferFeeAmount, // Not implemented yet
3841
MetadataPointer = 18, // Remove number once above extensions implemented
42+
TokenMetadata = 19, // Remove number once above extensions implemented
3943
}
4044

4145
export const TYPE_SIZE = 2;
4246
export const LENGTH_SIZE = 2;
4347

48+
function addTypeAndLengthToLen(len: number): number {
49+
return len + TYPE_SIZE + LENGTH_SIZE;
50+
}
51+
4452
// NOTE: All of these should eventually use their type's Span instead of these
4553
// constants. This is provided for at least creation to work.
4654
export function getTypeLen(e: ExtensionType): number {
@@ -79,6 +87,8 @@ export function getTypeLen(e: ExtensionType): number {
7987
return TRANSFER_HOOK_SIZE;
8088
case ExtensionType.TransferHookAccount:
8189
return TRANSFER_HOOK_ACCOUNT_SIZE;
90+
case ExtensionType.TokenMetadata:
91+
throw Error(`Cannot get type length for variable extension type: ${e}`);
8292
default:
8393
throw Error(`Unknown extension type: ${e}`);
8494
}
@@ -95,6 +105,7 @@ export function isMintExtension(e: ExtensionType): boolean {
95105
case ExtensionType.PermanentDelegate:
96106
case ExtensionType.TransferHook:
97107
case ExtensionType.MetadataPointer:
108+
case ExtensionType.TokenMetadata:
98109
return true;
99110
case ExtensionType.Uninitialized:
100111
case ExtensionType.TransferFeeAmount:
@@ -130,6 +141,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
130141
case ExtensionType.PermanentDelegate:
131142
case ExtensionType.TransferHook:
132143
case ExtensionType.MetadataPointer:
144+
case ExtensionType.TokenMetadata:
133145
return false;
134146
default:
135147
throw Error(`Unknown extension type: ${e}`);
@@ -154,6 +166,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
154166
case ExtensionType.MemoTransfer:
155167
case ExtensionType.MintCloseAuthority:
156168
case ExtensionType.MetadataPointer:
169+
case ExtensionType.TokenMetadata:
157170
case ExtensionType.Uninitialized:
158171
case ExtensionType.InterestBearingConfig:
159172
case ExtensionType.PermanentDelegate:
@@ -172,7 +185,7 @@ function getLen(extensionTypes: ExtensionType[], baseSize: number): number {
172185
ACCOUNT_TYPE_SIZE +
173186
extensionTypes
174187
.filter((element, i) => i === extensionTypes.indexOf(element))
175-
.map((element) => getTypeLen(element) + TYPE_SIZE + LENGTH_SIZE)
188+
.map((element) => addTypeAndLengthToLen(getTypeLen(element)))
176189
.reduce((a, b) => a + b);
177190
if (accountLength === MULTISIG_SIZE) {
178191
return accountLength + TYPE_SIZE;
@@ -192,10 +205,10 @@ export function getAccountLen(extensionTypes: ExtensionType[]): number {
192205

193206
export function getExtensionData(extension: ExtensionType, tlvData: Buffer): Buffer | null {
194207
let extensionTypeIndex = 0;
195-
while (extensionTypeIndex + TYPE_SIZE + LENGTH_SIZE <= tlvData.length) {
208+
while (addTypeAndLengthToLen(extensionTypeIndex) <= tlvData.length) {
196209
const entryType = tlvData.readUInt16LE(extensionTypeIndex);
197210
const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE);
198-
const typeIndex = extensionTypeIndex + TYPE_SIZE + LENGTH_SIZE;
211+
const typeIndex = addTypeAndLengthToLen(extensionTypeIndex);
199212
if (entryType == extension) {
200213
return tlvData.slice(typeIndex, typeIndex + entryLength);
201214
}
@@ -211,7 +224,7 @@ export function getExtensionTypes(tlvData: Buffer): ExtensionType[] {
211224
const entryType = tlvData.readUInt16LE(extensionTypeIndex);
212225
extensionTypes.push(entryType);
213226
const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE);
214-
extensionTypeIndex += TYPE_SIZE + LENGTH_SIZE + entryLength;
227+
extensionTypeIndex += addTypeAndLengthToLen(entryLength);
215228
}
216229
return extensionTypes;
217230
}
@@ -221,3 +234,19 @@ export function getAccountLenForMint(mint: Mint): number {
221234
const accountExtensions = extensionTypes.map(getAccountTypeOfMintType);
222235
return getAccountLen(accountExtensions);
223236
}
237+
238+
export function getNewAccountLenForExtensionLen(
239+
info: AccountInfo<Buffer>,
240+
address: PublicKey,
241+
extensionType: ExtensionType,
242+
extensionLen: number,
243+
programId = TOKEN_2022_PROGRAM_ID
244+
): number {
245+
const mint = unpackMint(address, info, programId);
246+
const extensionData = getExtensionData(extensionType, mint.tlvData);
247+
248+
const currentExtensionLen = extensionData ? addTypeAndLengthToLen(extensionData.length) : 0;
249+
const newExtensionLen = addTypeAndLengthToLen(extensionLen);
250+
251+
return info.data.length + newExtensionLen - currentExtensionLen;
252+
}

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './immutableOwner.js';
66
export * from './interestBearingMint/index.js';
77
export * from './memoTransfer/index.js';
88
export * from './metadataPointer/index.js';
9+
export * from './tokenMetadata/index.js';
910
export * from './mintCloseAuthority.js';
1011
export * from './nonTransferable.js';
1112
export * from './transferFee/index.js';

0 commit comments

Comments
 (0)