Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

token-js: add GroupMemberPointer extension #6292

Merged
merged 1 commit into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions token/js/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MULTISIG_SIZE } from '../state/multisig.js';
import { ACCOUNT_TYPE_SIZE } from './accountType.js';
import { CPI_GUARD_SIZE } from './cpiGuard/index.js';
import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState/index.js';
import { GROUP_MEMBER_POINTER_SIZE } from './groupMemberPointer/state.js';
import { GROUP_POINTER_SIZE } from './groupPointer/state.js';
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner.js';
import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state.js';
Expand Down Expand Up @@ -43,6 +44,8 @@ export enum ExtensionType {
TokenMetadata = 19, // Remove number once above extensions implemented
GroupPointer = 20,
// TokenGroup = 21, // Not implemented yet
GroupMemberPointer = 22,
// TokenGroupMember = 23, // Not implemented yet
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -101,6 +104,8 @@ export function getTypeLen(e: ExtensionType): number {
return TRANSFER_HOOK_ACCOUNT_SIZE;
case ExtensionType.GroupPointer:
return GROUP_POINTER_SIZE;
case ExtensionType.GroupMemberPointer:
return GROUP_MEMBER_POINTER_SIZE;
case ExtensionType.TokenMetadata:
throw Error(`Cannot get type length for variable extension type: ${e}`);
default:
Expand All @@ -121,6 +126,7 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.MetadataPointer:
case ExtensionType.TokenMetadata:
case ExtensionType.GroupPointer:
case ExtensionType.GroupMemberPointer:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeAmount:
Expand Down Expand Up @@ -158,6 +164,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.MetadataPointer:
case ExtensionType.TokenMetadata:
case ExtensionType.GroupPointer:
case ExtensionType.GroupMemberPointer:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand Down Expand Up @@ -189,6 +196,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.NonTransferableAccount:
case ExtensionType.TransferHookAccount:
case ExtensionType.GroupPointer:
case ExtensionType.GroupMemberPointer:
return ExtensionType.Uninitialized;
}
}
Expand Down
2 changes: 2 additions & 0 deletions token/js/src/extensions/groupMemberPointer/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/groupMemberPointer/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 GroupMemberPointerInstruction {
Initialize = 0,
Update = 1,
}

export const initializeGroupMemberPointerData = struct<{
instruction: TokenInstruction.GroupMemberPointerExtension;
groupMemberPointerInstruction: number;
authority: PublicKey;
memberAddress: PublicKey;
}>([
// prettier-ignore
u8('instruction'),
u8('groupMemberPointerInstruction'),
publicKey('authority'),
publicKey('memberAddress'),
]);

/**
* Construct an Initialize GroupMemberPointer instruction
*
* @param mint Token mint account
* @param authority Optional Authority that can set the member address
* @param memberAddress Optional Account address that holds the member
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createInitializeGroupMemberPointerInstruction(
mint: PublicKey,
authority: PublicKey | null,
memberAddress: PublicKey | null,
programId: PublicKey = TOKEN_2022_PROGRAM_ID
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];

const data = Buffer.alloc(initializeGroupMemberPointerData.span);
initializeGroupMemberPointerData.encode(
{
instruction: TokenInstruction.GroupMemberPointerExtension,
groupMemberPointerInstruction: GroupMemberPointerInstruction.Initialize,
authority: authority ?? PublicKey.default,
memberAddress: memberAddress ?? PublicKey.default,
},
data
);

return new TransactionInstruction({ keys, programId, data: data });
}

export const updateGroupMemberPointerData = struct<{
instruction: TokenInstruction.GroupMemberPointerExtension;
groupMemberPointerInstruction: number;
memberAddress: PublicKey;
}>([
// prettier-ignore
u8('instruction'),
u8('groupMemberPointerInstruction'),
publicKey('memberAddress'),
]);

export function createUpdateGroupMemberPointerInstruction(
mint: PublicKey,
authority: PublicKey,
memberAddress: 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(updateGroupMemberPointerData.span);
updateGroupMemberPointerData.encode(
{
instruction: TokenInstruction.GroupMemberPointerExtension,
groupMemberPointerInstruction: GroupMemberPointerInstruction.Update,
memberAddress: memberAddress ?? PublicKey.default,
},
data
);

return new TransactionInstruction({ keys, programId, data: data });
}
36 changes: 36 additions & 0 deletions token/js/src/extensions/groupMemberPointer/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';

/** GroupMemberPointer as stored by the program */
export interface GroupMemberPointer {
/** Optional authority that can set the member address */
authority: PublicKey | null;
/** Optional account address that holds the member */
memberAddress: PublicKey | null;
}

/** Buffer layout for de/serializing a Group Pointer extension */
export const GroupMemberPointerLayout = struct<{ authority: PublicKey; memberAddress: PublicKey }>([
publicKey('authority'),
publicKey('memberAddress'),
]);

export const GROUP_MEMBER_POINTER_SIZE = GroupMemberPointerLayout.span;

export function getGroupMemberPointerState(mint: Mint): Partial<GroupMemberPointer> | null {
const extensionData = getExtensionData(ExtensionType.GroupMemberPointer, mint.tlvData);
if (extensionData !== null) {
const { authority, memberAddress } = GroupMemberPointerLayout.decode(extensionData);

// Explicity set None/Zero keys to null
return {
authority: authority.equals(PublicKey.default) ? null : authority,
memberAddress: memberAddress.equals(PublicKey.default) ? null : memberAddress,
};
} else {
return null;
}
}
1 change: 1 addition & 0 deletions token/js/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './accountType.js';
export * from './cpiGuard/index.js';
export * from './defaultAccountState/index.js';
export * from './extensionType.js';
export * from './groupMemberPointer/index.js';
buffalojoec marked this conversation as resolved.
Show resolved Hide resolved
export * from './groupPointer/index.js';
export * from './immutableOwner.js';
export * from './interestBearingMint/index.js';
Expand Down
1 change: 1 addition & 0 deletions token/js/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ export enum TokenInstruction {
// WithdrawalExcessLamports = 38,
MetadataPointerExtension = 39,
GroupPointerExtension = 40,
GroupMemberPointerExtension = 41,
}
97 changes: 97 additions & 0 deletions token/js/test/e2e-2022/groupMemberPointer.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,
createInitializeGroupMemberPointerInstruction,
createInitializeMintInstruction,
createUpdateGroupMemberPointerInstruction,
getGroupMemberPointerState,
getMint,
getMintLen,
} from '../../src';
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';

const TEST_TOKEN_DECIMALS = 2;
const EXTENSIONS = [ExtensionType.GroupMemberPointer];

describe('GroupMember pointer', () => {
let connection: Connection;
let payer: Signer;
let mint: Keypair;
let mintAuthority: Keypair;
let memberAddress: PublicKey;

before(async () => {
connection = await getConnection();
payer = await newAccountWithLamports(connection, 1000000000);
mintAuthority = Keypair.generate();
});

beforeEach(async () => {
mint = Keypair.generate();
memberAddress = 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,
}),
createInitializeGroupMemberPointerInstruction(
mint.publicKey,
mintAuthority.publicKey,
memberAddress,
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 groupMemberPointer = getGroupMemberPointerState(mintInfo);

expect(groupMemberPointer).to.deep.equal({
authority: mintAuthority.publicKey,
memberAddress,
});
});

it('can update to new address', async () => {
const newGroupMemberAddress = PublicKey.unique();
const transaction = new Transaction().add(
createUpdateGroupMemberPointerInstruction(
mint.publicKey,
mintAuthority.publicKey,
newGroupMemberAddress,
undefined,
TEST_PROGRAM_ID
)
);
await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined);

const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID);
const groupMemberPointer = getGroupMemberPointerState(mintInfo);

expect(groupMemberPointer).to.deep.equal({
authority: mintAuthority.publicKey,
memberAddress: newGroupMemberAddress,
});
});
});
Loading
Loading