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

Commit b7949fc

Browse files
author
Joe C
authored
token-js: add GroupMemberPointer extension
As mentioned in #6175, and building on the previous PR, the `GroupMemberPointer` extension is live on Token-2022 mainnet-beta. This change adds support for `GroupMemberPointer` in `@solana/spl-token`!
1 parent e3262a9 commit b7949fc

File tree

8 files changed

+385
-0
lines changed

8 files changed

+385
-0
lines changed

token/js/src/extensions/extensionType.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MULTISIG_SIZE } from '../state/multisig.js';
77
import { ACCOUNT_TYPE_SIZE } from './accountType.js';
88
import { CPI_GUARD_SIZE } from './cpiGuard/index.js';
99
import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState/index.js';
10+
import { GROUP_MEMBER_POINTER_SIZE } from './groupMemberPointer/state.js';
1011
import { GROUP_POINTER_SIZE } from './groupPointer/state.js';
1112
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner.js';
1213
import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state.js';
@@ -43,6 +44,8 @@ export enum ExtensionType {
4344
TokenMetadata = 19, // Remove number once above extensions implemented
4445
GroupPointer = 20,
4546
// TokenGroup = 21, // Not implemented yet
47+
GroupMemberPointer = 22,
48+
// TokenGroupMember = 23, // Not implemented yet
4649
}
4750

4851
export const TYPE_SIZE = 2;
@@ -101,6 +104,8 @@ export function getTypeLen(e: ExtensionType): number {
101104
return TRANSFER_HOOK_ACCOUNT_SIZE;
102105
case ExtensionType.GroupPointer:
103106
return GROUP_POINTER_SIZE;
107+
case ExtensionType.GroupMemberPointer:
108+
return GROUP_MEMBER_POINTER_SIZE;
104109
case ExtensionType.TokenMetadata:
105110
throw Error(`Cannot get type length for variable extension type: ${e}`);
106111
default:
@@ -121,6 +126,7 @@ export function isMintExtension(e: ExtensionType): boolean {
121126
case ExtensionType.MetadataPointer:
122127
case ExtensionType.TokenMetadata:
123128
case ExtensionType.GroupPointer:
129+
case ExtensionType.GroupMemberPointer:
124130
return true;
125131
case ExtensionType.Uninitialized:
126132
case ExtensionType.TransferFeeAmount:
@@ -158,6 +164,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
158164
case ExtensionType.MetadataPointer:
159165
case ExtensionType.TokenMetadata:
160166
case ExtensionType.GroupPointer:
167+
case ExtensionType.GroupMemberPointer:
161168
return false;
162169
default:
163170
throw Error(`Unknown extension type: ${e}`);
@@ -189,6 +196,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
189196
case ExtensionType.NonTransferableAccount:
190197
case ExtensionType.TransferHookAccount:
191198
case ExtensionType.GroupPointer:
199+
case ExtensionType.GroupMemberPointer:
192200
return ExtensionType.Uninitialized;
193201
}
194202
}
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 GroupMemberPointerInstruction {
11+
Initialize = 0,
12+
Update = 1,
13+
}
14+
15+
export const initializeGroupMemberPointerData = struct<{
16+
instruction: TokenInstruction.GroupMemberPointerExtension;
17+
groupMemberPointerInstruction: number;
18+
authority: PublicKey;
19+
memberAddress: PublicKey;
20+
}>([
21+
// prettier-ignore
22+
u8('instruction'),
23+
u8('groupMemberPointerInstruction'),
24+
publicKey('authority'),
25+
publicKey('memberAddress'),
26+
]);
27+
28+
/**
29+
* Construct an Initialize GroupMemberPointer instruction
30+
*
31+
* @param mint Token mint account
32+
* @param authority Optional Authority that can set the member address
33+
* @param memberAddress Optional Account address that holds the member
34+
* @param programId SPL Token program account
35+
*
36+
* @return Instruction to add to a transaction
37+
*/
38+
export function createInitializeGroupMemberPointerInstruction(
39+
mint: PublicKey,
40+
authority: PublicKey | null,
41+
memberAddress: PublicKey | null,
42+
programId: PublicKey = TOKEN_2022_PROGRAM_ID
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(initializeGroupMemberPointerData.span);
50+
initializeGroupMemberPointerData.encode(
51+
{
52+
instruction: TokenInstruction.GroupMemberPointerExtension,
53+
groupMemberPointerInstruction: GroupMemberPointerInstruction.Initialize,
54+
authority: authority ?? PublicKey.default,
55+
memberAddress: memberAddress ?? PublicKey.default,
56+
},
57+
data
58+
);
59+
60+
return new TransactionInstruction({ keys, programId, data: data });
61+
}
62+
63+
export const updateGroupMemberPointerData = struct<{
64+
instruction: TokenInstruction.GroupMemberPointerExtension;
65+
groupMemberPointerInstruction: number;
66+
memberAddress: PublicKey;
67+
}>([
68+
// prettier-ignore
69+
u8('instruction'),
70+
u8('groupMemberPointerInstruction'),
71+
publicKey('memberAddress'),
72+
]);
73+
74+
export function createUpdateGroupMemberPointerInstruction(
75+
mint: PublicKey,
76+
authority: PublicKey,
77+
memberAddress: 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(updateGroupMemberPointerData.span);
88+
updateGroupMemberPointerData.encode(
89+
{
90+
instruction: TokenInstruction.GroupMemberPointerExtension,
91+
groupMemberPointerInstruction: GroupMemberPointerInstruction.Update,
92+
memberAddress: memberAddress ?? 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+
/** GroupMemberPointer as stored by the program */
8+
export interface GroupMemberPointer {
9+
/** Optional authority that can set the member address */
10+
authority: PublicKey | null;
11+
/** Optional account address that holds the member */
12+
memberAddress: PublicKey | null;
13+
}
14+
15+
/** Buffer layout for de/serializing a Group Pointer extension */
16+
export const GroupMemberPointerLayout = struct<{ authority: PublicKey; memberAddress: PublicKey }>([
17+
publicKey('authority'),
18+
publicKey('memberAddress'),
19+
]);
20+
21+
export const GROUP_MEMBER_POINTER_SIZE = GroupMemberPointerLayout.span;
22+
23+
export function getGroupMemberPointerState(mint: Mint): Partial<GroupMemberPointer> | null {
24+
const extensionData = getExtensionData(ExtensionType.GroupMemberPointer, mint.tlvData);
25+
if (extensionData !== null) {
26+
const { authority, memberAddress } = GroupMemberPointerLayout.decode(extensionData);
27+
28+
// Explicity set None/Zero keys to null
29+
return {
30+
authority: authority.equals(PublicKey.default) ? null : authority,
31+
memberAddress: memberAddress.equals(PublicKey.default) ? null : memberAddress,
32+
};
33+
} else {
34+
return null;
35+
}
36+
}

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './accountType.js';
22
export * from './cpiGuard/index.js';
33
export * from './defaultAccountState/index.js';
44
export * from './extensionType.js';
5+
export * from './groupMemberPointer/index.js';
56
export * from './groupPointer/index.js';
67
export * from './immutableOwner.js';
78
export * from './interestBearingMint/index.js';

token/js/src/instructions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export enum TokenInstruction {
4141
// WithdrawalExcessLamports = 38,
4242
MetadataPointerExtension = 39,
4343
GroupPointerExtension = 40,
44+
GroupMemberPointerExtension = 41,
4445
}
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+
createInitializeGroupMemberPointerInstruction,
9+
createInitializeMintInstruction,
10+
createUpdateGroupMemberPointerInstruction,
11+
getGroupMemberPointerState,
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.GroupMemberPointer];
19+
20+
describe('GroupMember pointer', () => {
21+
let connection: Connection;
22+
let payer: Signer;
23+
let mint: Keypair;
24+
let mintAuthority: Keypair;
25+
let memberAddress: 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+
memberAddress = 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+
createInitializeGroupMemberPointerInstruction(
49+
mint.publicKey,
50+
mintAuthority.publicKey,
51+
memberAddress,
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 groupMemberPointer = getGroupMemberPointerState(mintInfo);
69+
70+
expect(groupMemberPointer).to.deep.equal({
71+
authority: mintAuthority.publicKey,
72+
memberAddress,
73+
});
74+
});
75+
76+
it('can update to new address', async () => {
77+
const newGroupMemberAddress = PublicKey.unique();
78+
const transaction = new Transaction().add(
79+
createUpdateGroupMemberPointerInstruction(
80+
mint.publicKey,
81+
mintAuthority.publicKey,
82+
newGroupMemberAddress,
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 groupMemberPointer = getGroupMemberPointerState(mintInfo);
91+
92+
expect(groupMemberPointer).to.deep.equal({
93+
authority: mintAuthority.publicKey,
94+
memberAddress: newGroupMemberAddress,
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)