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

Commit 20f27e2

Browse files
authored
token 2022: add metadata pointer extension to js @solana/spl-token client (#5805)
* added metadata pointer extension to js client * changed to default public key * addressed pr comments * comment out added, but unused instructions * remove number from extensionType enum * removed trailing unimplemented extensions
1 parent 6fe3c15 commit 20f27e2

File tree

8 files changed

+388
-0
lines changed

8 files changed

+388
-0
lines changed

token/js/src/extensions/extensionType.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState/index.js';
88
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner.js';
99
import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state.js';
1010
import { MEMO_TRANSFER_SIZE } from './memoTransfer/index.js';
11+
import { METADATA_POINTER_SIZE } from './metadataPointer/state.js';
1112
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js';
1213
import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js';
1314
import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
1415
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
1516
import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js';
1617

18+
// Sequence from https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/mod.rs#L903
1719
export enum ExtensionType {
1820
Uninitialized,
1921
TransferFeeConfig,
@@ -31,6 +33,9 @@ export enum ExtensionType {
3133
NonTransferableAccount,
3234
TransferHook,
3335
TransferHookAccount,
36+
// ConfidentialTransferFee, // Not implemented yet
37+
// ConfidentialTransferFeeAmount, // Not implemented yet
38+
MetadataPointer = 18, // Remove number once above extensions implemented
3439
}
3540

3641
export const TYPE_SIZE = 2;
@@ -60,6 +65,8 @@ export function getTypeLen(e: ExtensionType): number {
6065
return IMMUTABLE_OWNER_SIZE;
6166
case ExtensionType.MemoTransfer:
6267
return MEMO_TRANSFER_SIZE;
68+
case ExtensionType.MetadataPointer:
69+
return METADATA_POINTER_SIZE;
6370
case ExtensionType.NonTransferable:
6471
return NON_TRANSFERABLE_SIZE;
6572
case ExtensionType.InterestBearingConfig:
@@ -87,6 +94,7 @@ export function isMintExtension(e: ExtensionType): boolean {
8794
case ExtensionType.InterestBearingConfig:
8895
case ExtensionType.PermanentDelegate:
8996
case ExtensionType.TransferHook:
97+
case ExtensionType.MetadataPointer:
9098
return true;
9199
case ExtensionType.Uninitialized:
92100
case ExtensionType.TransferFeeAmount:
@@ -121,6 +129,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
121129
case ExtensionType.InterestBearingConfig:
122130
case ExtensionType.PermanentDelegate:
123131
case ExtensionType.TransferHook:
132+
case ExtensionType.MetadataPointer:
124133
return false;
125134
default:
126135
throw Error(`Unknown extension type: ${e}`);
@@ -144,6 +153,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
144153
case ExtensionType.ImmutableOwner:
145154
case ExtensionType.MemoTransfer:
146155
case ExtensionType.MintCloseAuthority:
156+
case ExtensionType.MetadataPointer:
147157
case ExtensionType.Uninitialized:
148158
case ExtensionType.InterestBearingConfig:
149159
case ExtensionType.PermanentDelegate:

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './extensionType.js';
55
export * from './immutableOwner.js';
66
export * from './interestBearingMint/index.js';
77
export * from './memoTransfer/index.js';
8+
export * from './metadataPointer/index.js';
89
export * from './mintCloseAuthority.js';
910
export * from './nonTransferable.js';
1011
export * from './transferFee/index.js';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './instructions.js';
2+
export * from './state.js';
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { struct, u8 } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import type { Signer } from '@solana/web3.js';
4+
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
5+
import { TOKEN_2022_PROGRAM_ID, programSupportsExtensions } from '../../constants.js';
6+
import { TokenUnsupportedInstructionError } from '../../errors.js';
7+
import { TokenInstruction } from '../../instructions/types.js';
8+
import { addSigners } from '../../instructions/internal.js';
9+
10+
export enum MetadataPointerInstruction {
11+
Initialize = 0,
12+
Update = 1,
13+
}
14+
15+
export const initializeMetadataPointerData = struct<{
16+
instruction: TokenInstruction.MetadataPointerExtension;
17+
metadataPointerInstruction: number;
18+
authority: PublicKey;
19+
metadataAddress: PublicKey;
20+
}>([
21+
// prettier-ignore
22+
u8('instruction'),
23+
u8('metadataPointerInstruction'),
24+
publicKey('authority'),
25+
publicKey('metadataAddress'),
26+
]);
27+
28+
/**
29+
* Construct an Initialize MetadataPointer instruction
30+
*
31+
* @param mint Token mint account
32+
* @param authority Optional Authority that can set the metadata address
33+
* @param metadataAddress Optional Account address that holds the metadata
34+
* @param programId SPL Token program account
35+
*
36+
* @return Instruction to add to a transaction
37+
*/
38+
export function createInitializeMetadataPointerInstruction(
39+
mint: PublicKey,
40+
authority: PublicKey | null,
41+
metadataAddress: PublicKey | null,
42+
programId: PublicKey
43+
): TransactionInstruction {
44+
if (!programSupportsExtensions(programId)) {
45+
throw new TokenUnsupportedInstructionError();
46+
}
47+
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
48+
49+
const data = Buffer.alloc(initializeMetadataPointerData.span);
50+
initializeMetadataPointerData.encode(
51+
{
52+
instruction: TokenInstruction.MetadataPointerExtension,
53+
metadataPointerInstruction: MetadataPointerInstruction.Initialize,
54+
authority: authority ?? PublicKey.default,
55+
metadataAddress: metadataAddress ?? PublicKey.default,
56+
},
57+
data
58+
);
59+
60+
return new TransactionInstruction({ keys, programId, data: data });
61+
}
62+
63+
export const updateMetadataPointerData = struct<{
64+
instruction: TokenInstruction.MetadataPointerExtension;
65+
metadataPointerInstruction: number;
66+
metadataAddress: PublicKey;
67+
}>([
68+
// prettier-ignore
69+
u8('instruction'),
70+
u8('metadataPointerInstruction'),
71+
publicKey('metadataAddress'),
72+
]);
73+
74+
export function createUpdateMetadataPointerInstruction(
75+
mint: PublicKey,
76+
authority: PublicKey,
77+
metadataAddress: PublicKey | null,
78+
multiSigners: (Signer | PublicKey)[] = [],
79+
programId: PublicKey = TOKEN_2022_PROGRAM_ID
80+
): TransactionInstruction {
81+
if (!programSupportsExtensions(programId)) {
82+
throw new TokenUnsupportedInstructionError();
83+
}
84+
85+
const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners);
86+
87+
const data = Buffer.alloc(updateMetadataPointerData.span);
88+
updateMetadataPointerData.encode(
89+
{
90+
instruction: TokenInstruction.MetadataPointerExtension,
91+
metadataPointerInstruction: MetadataPointerInstruction.Update,
92+
metadataAddress: metadataAddress ?? PublicKey.default,
93+
},
94+
data
95+
);
96+
97+
return new TransactionInstruction({ keys, programId, data: data });
98+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { struct } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import { PublicKey } from '@solana/web3.js';
4+
import type { Mint } from '../../state/mint.js';
5+
import { ExtensionType, getExtensionData } from '../extensionType.js';
6+
7+
/** MetadataPointer as stored by the program */
8+
export interface MetadataPointer {
9+
/** Optional authority that can set the metadata address */
10+
authority: PublicKey | null;
11+
/** Optional Account Address that holds the metadata */
12+
metadataAddress: PublicKey | null;
13+
}
14+
15+
/** Buffer layout for de/serializing a Metadata Pointer extension */
16+
export const MetadataPointerLayout = struct<{ authority: PublicKey; metadataAddress: PublicKey }>([
17+
publicKey('authority'),
18+
publicKey('metadataAddress'),
19+
]);
20+
21+
export const METADATA_POINTER_SIZE = MetadataPointerLayout.span;
22+
23+
export function getMetadataPointerState(mint: Mint): Partial<MetadataPointer> | null {
24+
const extensionData = getExtensionData(ExtensionType.MetadataPointer, mint.tlvData);
25+
if (extensionData !== null) {
26+
const { authority, metadataAddress } = MetadataPointerLayout.decode(extensionData);
27+
28+
// Explicity set None/Zero keys to null
29+
return {
30+
authority: authority.equals(PublicKey.default) ? null : authority,
31+
metadataAddress: metadataAddress.equals(PublicKey.default) ? null : metadataAddress,
32+
};
33+
} else {
34+
return null;
35+
}
36+
}

token/js/src/instructions/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,7 @@ export enum TokenInstruction {
3737
CpiGuardExtension = 34,
3838
InitializePermanentDelegate = 35,
3939
TransferHookExtension = 36,
40+
// ConfidentialTransferFeeExtension = 37,
41+
// WithdrawalExcessLamports = 38,
42+
MetadataPointerExtension = 39,
4043
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { expect } from 'chai';
2+
import type { Connection, Signer } from '@solana/web3.js';
3+
import { PublicKey } from '@solana/web3.js';
4+
import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js';
5+
6+
import {
7+
ExtensionType,
8+
createInitializeMetadataPointerInstruction,
9+
createInitializeMintInstruction,
10+
createUpdateMetadataPointerInstruction,
11+
getMetadataPointerState,
12+
getMint,
13+
getMintLen,
14+
} from '../../src';
15+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
16+
17+
const TEST_TOKEN_DECIMALS = 2;
18+
const EXTENSIONS = [ExtensionType.MetadataPointer];
19+
20+
describe('Metadata pointer', () => {
21+
let connection: Connection;
22+
let payer: Signer;
23+
let mint: Keypair;
24+
let mintAuthority: Keypair;
25+
let metadataAddress: PublicKey;
26+
27+
before(async () => {
28+
connection = await getConnection();
29+
payer = await newAccountWithLamports(connection, 1000000000);
30+
mintAuthority = Keypair.generate();
31+
});
32+
33+
beforeEach(async () => {
34+
mint = Keypair.generate();
35+
metadataAddress = PublicKey.unique();
36+
37+
const mintLen = getMintLen(EXTENSIONS);
38+
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
39+
40+
const transaction = new Transaction().add(
41+
SystemProgram.createAccount({
42+
fromPubkey: payer.publicKey,
43+
newAccountPubkey: mint.publicKey,
44+
space: mintLen,
45+
lamports,
46+
programId: TEST_PROGRAM_ID,
47+
}),
48+
createInitializeMetadataPointerInstruction(
49+
mint.publicKey,
50+
mintAuthority.publicKey,
51+
metadataAddress,
52+
TEST_PROGRAM_ID
53+
),
54+
createInitializeMintInstruction(
55+
mint.publicKey,
56+
TEST_TOKEN_DECIMALS,
57+
mintAuthority.publicKey,
58+
null,
59+
TEST_PROGRAM_ID
60+
)
61+
);
62+
63+
await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined);
64+
});
65+
66+
it('can successfully initialize', async () => {
67+
const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID);
68+
const metadataPointer = getMetadataPointerState(mintInfo);
69+
70+
expect(metadataPointer).to.deep.equal({
71+
authority: mintAuthority.publicKey,
72+
metadataAddress,
73+
});
74+
});
75+
76+
it('can update to new address', async () => {
77+
const newMetadataAddress = PublicKey.unique();
78+
const transaction = new Transaction().add(
79+
createUpdateMetadataPointerInstruction(
80+
mint.publicKey,
81+
mintAuthority.publicKey,
82+
newMetadataAddress,
83+
undefined,
84+
TEST_PROGRAM_ID
85+
)
86+
);
87+
await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined);
88+
89+
const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID);
90+
const metadataPointer = getMetadataPointerState(mintInfo);
91+
92+
expect(metadataPointer).to.deep.equal({
93+
authority: mintAuthority.publicKey,
94+
metadataAddress: newMetadataAddress,
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)