Skip to content

Commit

Permalink
token-js: add GroupMemberPointer extension
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed Feb 23, 2024
1 parent fdd137c commit e44642b
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 0 deletions.
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
): 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 @@ -3,6 +3,7 @@ export * from './cpiGuard/index.js';
export * from './defaultAccountState/index.js';
export * from './extensionType.js';
export * from './groupPointer/index.js';
export * from './groupMemberPointer/index.js';
export * from './immutableOwner.js';
export * from './interestBearingMint/index.js';
export * from './memoTransfer/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,
}
143 changes: 143 additions & 0 deletions token/js/test/unit/groupMemberPointer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { expect } from 'chai';
import type { Mint } from '../../src';
import {
TOKEN_2022_PROGRAM_ID,
createInitializeGroupMemberPointerInstruction,
createUpdateGroupMemberPointerInstruction,
getGroupMemberPointerState,
} from '../../src';

const AUTHORITY_ADDRESS_BYTES = Buffer.alloc(32).fill(8);
const GROUP_MEMBER_ADDRESS_BYTES = Buffer.alloc(32).fill(5);
const NULL_OPTIONAL_NONZERO_PUBKEY_BYTES = Buffer.alloc(32).fill(0);

describe('SPL Token 2022 GroupMemberPointer Extension', () => {
it('can create InitializeGroupMemberPointerInstruction', () => {
const mint = PublicKey.unique();
const authority = new PublicKey(AUTHORITY_ADDRESS_BYTES);
const memberAddress = new PublicKey(GROUP_MEMBER_ADDRESS_BYTES);
const instruction = createInitializeGroupMemberPointerInstruction(
mint,
authority,
memberAddress,
TOKEN_2022_PROGRAM_ID
);
expect(instruction).to.deep.equal(
new TransactionInstruction({
programId: TOKEN_2022_PROGRAM_ID,
keys: [{ isSigner: false, isWritable: true, pubkey: mint }],
data: Buffer.concat([
Buffer.from([
41, // Token instruction discriminator
0, // GroupMemberPointer instruction discriminator
]),
AUTHORITY_ADDRESS_BYTES,
GROUP_MEMBER_ADDRESS_BYTES,
]),
})
);
});
it('can create UpdateGroupMemberPointerInstruction', () => {
const mint = PublicKey.unique();
const authority = PublicKey.unique();
const memberAddress = new PublicKey(GROUP_MEMBER_ADDRESS_BYTES);
const instruction = createUpdateGroupMemberPointerInstruction(mint, authority, memberAddress);
expect(instruction).to.deep.equal(
new TransactionInstruction({
programId: TOKEN_2022_PROGRAM_ID,
keys: [
{ isSigner: false, isWritable: true, pubkey: mint },
{ isSigner: true, isWritable: false, pubkey: authority },
],
data: Buffer.concat([
Buffer.from([
41, // Token instruction discriminator
1, // GroupMemberPointer instruction discriminator
]),
GROUP_MEMBER_ADDRESS_BYTES,
]),
})
);
});
it('can create UpdateGroupMemberPointerInstruction to none', () => {
const mint = PublicKey.unique();
const authority = PublicKey.unique();
const memberAddress = null;
const instruction = createUpdateGroupMemberPointerInstruction(mint, authority, memberAddress);
expect(instruction).to.deep.equal(
new TransactionInstruction({
programId: TOKEN_2022_PROGRAM_ID,
keys: [
{ isSigner: false, isWritable: true, pubkey: mint },
{ isSigner: true, isWritable: false, pubkey: authority },
],
data: Buffer.concat([
Buffer.from([
41, // Token instruction discriminator
1, // GroupMemberPointer instruction discriminator
]),
NULL_OPTIONAL_NONZERO_PUBKEY_BYTES,
]),
})
);
});
it('can get state with authority and group address', async () => {
const mintInfo = {
tlvData: Buffer.concat([
Buffer.from([
// Extension discriminator
22, 0,
// Extension length
64, 0,
]),
AUTHORITY_ADDRESS_BYTES,
GROUP_MEMBER_ADDRESS_BYTES,
]),
} as Mint;
const groupPointer = getGroupMemberPointerState(mintInfo);
expect(groupPointer).to.deep.equal({
authority: new PublicKey(AUTHORITY_ADDRESS_BYTES),
memberAddress: new PublicKey(GROUP_MEMBER_ADDRESS_BYTES),
});
});
it('can get state with only group address', async () => {
const mintInfo = {
tlvData: Buffer.concat([
Buffer.from([
// Extension discriminator
22, 0,
// Extension length
64, 0,
]),
NULL_OPTIONAL_NONZERO_PUBKEY_BYTES,
GROUP_MEMBER_ADDRESS_BYTES,
]),
} as Mint;
const groupPointer = getGroupMemberPointerState(mintInfo);
expect(groupPointer).to.deep.equal({
authority: null,
memberAddress: new PublicKey(GROUP_MEMBER_ADDRESS_BYTES),
});
});

it('can get state with only authority address', async () => {
const mintInfo = {
tlvData: Buffer.concat([
Buffer.from([
// Extension discriminator
22, 0,
// Extension length
64, 0,
]),
AUTHORITY_ADDRESS_BYTES,
NULL_OPTIONAL_NONZERO_PUBKEY_BYTES,
]),
} as Mint;
const groupPointer = getGroupMemberPointerState(mintInfo);
expect(groupPointer).to.deep.equal({
authority: new PublicKey(AUTHORITY_ADDRESS_BYTES),
memberAddress: null,
});
});
});

0 comments on commit e44642b

Please sign in to comment.