Skip to content

Commit

Permalink
token-js: add GroupPointer extension
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed Feb 23, 2024
1 parent c7d1f29 commit fdd137c
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_POINTER_SIZE } from './groupPointer/state.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';
Expand Down Expand Up @@ -40,6 +41,8 @@ export enum ExtensionType {
// ConfidentialTransferFeeAmount, // Not implemented yet
MetadataPointer = 18, // Remove number once above extensions implemented
TokenMetadata = 19, // Remove number once above extensions implemented
GroupPointer = 20,
// TokenGroup = 21, // Not implemented yet
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -96,6 +99,8 @@ export function getTypeLen(e: ExtensionType): number {
return TRANSFER_HOOK_SIZE;
case ExtensionType.TransferHookAccount:
return TRANSFER_HOOK_ACCOUNT_SIZE;
case ExtensionType.GroupPointer:
return GROUP_POINTER_SIZE;
case ExtensionType.TokenMetadata:
throw Error(`Cannot get type length for variable extension type: ${e}`);
default:
Expand All @@ -115,6 +120,7 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.TransferHook:
case ExtensionType.MetadataPointer:
case ExtensionType.TokenMetadata:
case ExtensionType.GroupPointer:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeAmount:
Expand Down Expand Up @@ -151,6 +157,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.TransferHook:
case ExtensionType.MetadataPointer:
case ExtensionType.TokenMetadata:
case ExtensionType.GroupPointer:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand Down Expand Up @@ -181,6 +188,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.PermanentDelegate:
case ExtensionType.NonTransferableAccount:
case ExtensionType.TransferHookAccount:
case ExtensionType.GroupPointer:
return ExtensionType.Uninitialized;
}
}
Expand Down
2 changes: 2 additions & 0 deletions token/js/src/extensions/groupPointer/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/groupPointer/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 GroupPointerInstruction {
Initialize = 0,
Update = 1,
}

export const initializeGroupPointerData = struct<{
instruction: TokenInstruction.GroupPointerExtension;
groupPointerInstruction: number;
authority: PublicKey;
groupAddress: PublicKey;
}>([
// prettier-ignore
u8('instruction'),
u8('groupPointerInstruction'),
publicKey('authority'),
publicKey('groupAddress'),
]);

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

const data = Buffer.alloc(initializeGroupPointerData.span);
initializeGroupPointerData.encode(
{
instruction: TokenInstruction.GroupPointerExtension,
groupPointerInstruction: GroupPointerInstruction.Initialize,
authority: authority ?? PublicKey.default,
groupAddress: groupAddress ?? PublicKey.default,
},
data
);

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

export const updateGroupPointerData = struct<{
instruction: TokenInstruction.GroupPointerExtension;
groupPointerInstruction: number;
groupAddress: PublicKey;
}>([
// prettier-ignore
u8('instruction'),
u8('groupPointerInstruction'),
publicKey('groupAddress'),
]);

export function createUpdateGroupPointerInstruction(
mint: PublicKey,
authority: PublicKey,
groupAddress: 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(updateGroupPointerData.span);
updateGroupPointerData.encode(
{
instruction: TokenInstruction.GroupPointerExtension,
groupPointerInstruction: GroupPointerInstruction.Update,
groupAddress: groupAddress ?? PublicKey.default,
},
data
);

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

/** GroupPointer as stored by the program */
export interface GroupPointer {
/** Optional authority that can set the group address */
authority: PublicKey | null;
/** Optional Account Address that holds the group */
groupAddress: PublicKey | null;
}

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

export const GROUP_POINTER_SIZE = GroupPointerLayout.span;

export function getGroupPointerState(mint: Mint): Partial<GroupPointer> | null {
const extensionData = getExtensionData(ExtensionType.GroupPointer, mint.tlvData);
if (extensionData !== null) {
const { authority, groupAddress } = GroupPointerLayout.decode(extensionData);

// Explicity set None/Zero keys to null
return {
authority: authority.equals(PublicKey.default) ? null : authority,
groupAddress: groupAddress.equals(PublicKey.default) ? null : groupAddress,
};
} 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 './groupPointer/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 @@ -40,4 +40,5 @@ export enum TokenInstruction {
// ConfidentialTransferFeeExtension = 37,
// WithdrawalExcessLamports = 38,
MetadataPointerExtension = 39,
GroupPointerExtension = 40,
}
143 changes: 143 additions & 0 deletions token/js/test/unit/groupPointer.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,
createInitializeGroupPointerInstruction,
createUpdateGroupPointerInstruction,
getGroupPointerState,
} from '../../src';

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

describe('SPL Token 2022 GroupPointer Extension', () => {
it('can create InitializeGroupPointerInstruction', () => {
const mint = PublicKey.unique();
const authority = new PublicKey(AUTHORITY_ADDRESS_BYTES);
const groupAddress = new PublicKey(GROUP_ADDRESS_BYTES);
const instruction = createInitializeGroupPointerInstruction(
mint,
authority,
groupAddress,
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([
40, // Token instruction discriminator
0, // GroupPointer instruction discriminator
]),
AUTHORITY_ADDRESS_BYTES,
GROUP_ADDRESS_BYTES,
]),
})
);
});
it('can create UpdateGroupPointerInstruction', () => {
const mint = PublicKey.unique();
const authority = PublicKey.unique();
const groupAddress = new PublicKey(GROUP_ADDRESS_BYTES);
const instruction = createUpdateGroupPointerInstruction(mint, authority, groupAddress);
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([
40, // Token instruction discriminator
1, // GroupPointer instruction discriminator
]),
GROUP_ADDRESS_BYTES,
]),
})
);
});
it('can create UpdateGroupPointerInstruction to none', () => {
const mint = PublicKey.unique();
const authority = PublicKey.unique();
const groupAddress = null;
const instruction = createUpdateGroupPointerInstruction(mint, authority, groupAddress);
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([
40, // Token instruction discriminator
1, // GroupPointer 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
20, 0,
// Extension length
64, 0,
]),
AUTHORITY_ADDRESS_BYTES,
GROUP_ADDRESS_BYTES,
]),
} as Mint;
const groupPointer = getGroupPointerState(mintInfo);
expect(groupPointer).to.deep.equal({
authority: new PublicKey(AUTHORITY_ADDRESS_BYTES),
groupAddress: new PublicKey(GROUP_ADDRESS_BYTES),
});
});
it('can get state with only group address', async () => {
const mintInfo = {
tlvData: Buffer.concat([
Buffer.from([
// Extension discriminator
20, 0,
// Extension length
64, 0,
]),
NULL_OPTIONAL_NONZERO_PUBKEY_BYTES,
GROUP_ADDRESS_BYTES,
]),
} as Mint;
const groupPointer = getGroupPointerState(mintInfo);
expect(groupPointer).to.deep.equal({
authority: null,
groupAddress: new PublicKey(GROUP_ADDRESS_BYTES),
});
});

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

0 comments on commit fdd137c

Please sign in to comment.