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

Commit e3262a9

Browse files
author
Joe C
authored
token-js: add GroupPointer extension
As mentioned in #6175, the `GroupPointer` extension is live on Token-2022 mainnet-beta, but it's not currently supported in the `@solana/spl-token`. This change adds that support!
1 parent 2062613 commit e3262a9

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_POINTER_SIZE } from './groupPointer/state.js';
1011
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner.js';
1112
import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state.js';
1213
import { MEMO_TRANSFER_SIZE } from './memoTransfer/index.js';
@@ -40,6 +41,8 @@ export enum ExtensionType {
4041
// ConfidentialTransferFeeAmount, // Not implemented yet
4142
MetadataPointer = 18, // Remove number once above extensions implemented
4243
TokenMetadata = 19, // Remove number once above extensions implemented
44+
GroupPointer = 20,
45+
// TokenGroup = 21, // Not implemented yet
4346
}
4447

4548
export const TYPE_SIZE = 2;
@@ -96,6 +99,8 @@ export function getTypeLen(e: ExtensionType): number {
9699
return TRANSFER_HOOK_SIZE;
97100
case ExtensionType.TransferHookAccount:
98101
return TRANSFER_HOOK_ACCOUNT_SIZE;
102+
case ExtensionType.GroupPointer:
103+
return GROUP_POINTER_SIZE;
99104
case ExtensionType.TokenMetadata:
100105
throw Error(`Cannot get type length for variable extension type: ${e}`);
101106
default:
@@ -115,6 +120,7 @@ export function isMintExtension(e: ExtensionType): boolean {
115120
case ExtensionType.TransferHook:
116121
case ExtensionType.MetadataPointer:
117122
case ExtensionType.TokenMetadata:
123+
case ExtensionType.GroupPointer:
118124
return true;
119125
case ExtensionType.Uninitialized:
120126
case ExtensionType.TransferFeeAmount:
@@ -151,6 +157,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
151157
case ExtensionType.TransferHook:
152158
case ExtensionType.MetadataPointer:
153159
case ExtensionType.TokenMetadata:
160+
case ExtensionType.GroupPointer:
154161
return false;
155162
default:
156163
throw Error(`Unknown extension type: ${e}`);
@@ -181,6 +188,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
181188
case ExtensionType.PermanentDelegate:
182189
case ExtensionType.NonTransferableAccount:
183190
case ExtensionType.TransferHookAccount:
191+
case ExtensionType.GroupPointer:
184192
return ExtensionType.Uninitialized;
185193
}
186194
}
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 GroupPointerInstruction {
11+
Initialize = 0,
12+
Update = 1,
13+
}
14+
15+
export const initializeGroupPointerData = struct<{
16+
instruction: TokenInstruction.GroupPointerExtension;
17+
groupPointerInstruction: number;
18+
authority: PublicKey;
19+
groupAddress: PublicKey;
20+
}>([
21+
// prettier-ignore
22+
u8('instruction'),
23+
u8('groupPointerInstruction'),
24+
publicKey('authority'),
25+
publicKey('groupAddress'),
26+
]);
27+
28+
/**
29+
* Construct an Initialize GroupPointer instruction
30+
*
31+
* @param mint Token mint account
32+
* @param authority Optional Authority that can set the group address
33+
* @param groupAddress Optional Account address that holds the group
34+
* @param programId SPL Token program account
35+
*
36+
* @return Instruction to add to a transaction
37+
*/
38+
export function createInitializeGroupPointerInstruction(
39+
mint: PublicKey,
40+
authority: PublicKey | null,
41+
groupAddress: 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(initializeGroupPointerData.span);
50+
initializeGroupPointerData.encode(
51+
{
52+
instruction: TokenInstruction.GroupPointerExtension,
53+
groupPointerInstruction: GroupPointerInstruction.Initialize,
54+
authority: authority ?? PublicKey.default,
55+
groupAddress: groupAddress ?? PublicKey.default,
56+
},
57+
data
58+
);
59+
60+
return new TransactionInstruction({ keys, programId, data: data });
61+
}
62+
63+
export const updateGroupPointerData = struct<{
64+
instruction: TokenInstruction.GroupPointerExtension;
65+
groupPointerInstruction: number;
66+
groupAddress: PublicKey;
67+
}>([
68+
// prettier-ignore
69+
u8('instruction'),
70+
u8('groupPointerInstruction'),
71+
publicKey('groupAddress'),
72+
]);
73+
74+
export function createUpdateGroupPointerInstruction(
75+
mint: PublicKey,
76+
authority: PublicKey,
77+
groupAddress: 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(updateGroupPointerData.span);
88+
updateGroupPointerData.encode(
89+
{
90+
instruction: TokenInstruction.GroupPointerExtension,
91+
groupPointerInstruction: GroupPointerInstruction.Update,
92+
groupAddress: groupAddress ?? 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+
/** GroupPointer as stored by the program */
8+
export interface GroupPointer {
9+
/** Optional authority that can set the group address */
10+
authority: PublicKey | null;
11+
/** Optional account address that holds the group */
12+
groupAddress: PublicKey | null;
13+
}
14+
15+
/** Buffer layout for de/serializing a GroupPointer extension */
16+
export const GroupPointerLayout = struct<{ authority: PublicKey; groupAddress: PublicKey }>([
17+
publicKey('authority'),
18+
publicKey('groupAddress'),
19+
]);
20+
21+
export const GROUP_POINTER_SIZE = GroupPointerLayout.span;
22+
23+
export function getGroupPointerState(mint: Mint): Partial<GroupPointer> | null {
24+
const extensionData = getExtensionData(ExtensionType.GroupPointer, mint.tlvData);
25+
if (extensionData !== null) {
26+
const { authority, groupAddress } = GroupPointerLayout.decode(extensionData);
27+
28+
// Explicity set None/Zero keys to null
29+
return {
30+
authority: authority.equals(PublicKey.default) ? null : authority,
31+
groupAddress: groupAddress.equals(PublicKey.default) ? null : groupAddress,
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 './groupPointer/index.js';
56
export * from './immutableOwner.js';
67
export * from './interestBearingMint/index.js';
78
export * from './memoTransfer/index.js';

token/js/src/instructions/types.ts

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

0 commit comments

Comments
 (0)