Skip to content

Commit

Permalink
token 2022: add metadata pointer extension to js @solana/spl-token cl…
Browse files Browse the repository at this point in the history
…ient (solana-labs#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
  • Loading branch information
mistersimon authored Nov 13, 2023
1 parent 6fe3c15 commit 20f27e2
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 0 deletions.
10 changes: 10 additions & 0 deletions token/js/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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}`);
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions token/js/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions token/js/src/extensions/metadataPointer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './instructions.js';
export * from './state.js';
98 changes: 98 additions & 0 deletions token/js/src/extensions/metadataPointer/instructions.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
36 changes: 36 additions & 0 deletions token/js/src/extensions/metadataPointer/state.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 3 additions & 0 deletions token/js/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ export enum TokenInstruction {
CpiGuardExtension = 34,
InitializePermanentDelegate = 35,
TransferHookExtension = 36,
// ConfidentialTransferFeeExtension = 37,
// WithdrawalExcessLamports = 38,
MetadataPointerExtension = 39,
}
97 changes: 97 additions & 0 deletions token/js/test/e2e-2022/metadataPointer.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
Loading

0 comments on commit 20f27e2

Please sign in to comment.