Skip to content

Commit

Permalink
feat: add token group extension in token (#6295)
Browse files Browse the repository at this point in the history
* feat: add token group extension in token

* wip: add e2e tests

* fix: token group member

* feat: init group and groupMember with rent transfer

* fix: token group e2e tests

* fix: remove single pool changes

* fix: pnpm install

* fix instructions

* rework tests

* rename directory to `tokenGroup`

---------

Co-authored-by: Joe C <[email protected]>
  • Loading branch information
qiweiii and buffalojoec authored Mar 22, 2024
1 parent ce0389b commit 56b083d
Show file tree
Hide file tree
Showing 12 changed files with 3,251 additions and 1,548 deletions.
4,058 changes: 2,513 additions & 1,545 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions token-group/js/src/state/tokenGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const tokenGroupCodec = getStructCodec([
['maxSize', getU32Codec()],
]);

export const TOKEN_GROUP_SIZE = tokenGroupCodec.fixedSize;

export interface TokenGroup {
/** The authority that can sign to update the group */
updateAuthority?: PublicKey;
Expand Down
2 changes: 2 additions & 0 deletions token-group/js/src/state/tokenGroupMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const tokenGroupMemberCodec = getStructCodec([
['memberNumber', getU32Codec()],
]);

export const TOKEN_GROUP_MEMBER_SIZE = tokenGroupMemberCodec.fixedSize;

export interface TokenGroupMember {
/** The associated mint, used to counter spoofing to be sure that member belongs to a particular mint */
mint: PublicKey;
Expand Down
2 changes: 1 addition & 1 deletion token/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"@solana/buffer-layout": "^4.0.0",
"@solana/buffer-layout-utils": "^0.2.0",
"@solana/spl-token-group": "^0.0.1",
"@solana/spl-token-metadata": "^0.1.2",
"buffer": "^6.0.3"
},
Expand All @@ -82,7 +83,6 @@
"process": "^0.11.10",
"shx": "^0.3.4",
"start-server-and-test": "^2.0.3",
"tslib": "^2.3.1",
"ts-node": "^10.9.2",
"typedoc": "^0.25.12",
"typescript": "^5.4.3"
Expand Down
15 changes: 13 additions & 2 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 { TOKEN_GROUP_SIZE, TOKEN_GROUP_MEMBER_SIZE } from './tokenGroup/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';
Expand Down Expand Up @@ -43,9 +44,9 @@ export enum ExtensionType {
MetadataPointer = 18, // Remove number once above extensions implemented
TokenMetadata = 19, // Remove number once above extensions implemented
GroupPointer = 20,
// TokenGroup = 21, // Not implemented yet
TokenGroup = 21,
GroupMemberPointer = 22,
// TokenGroupMember = 23, // Not implemented yet
TokenGroupMember = 23,
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -106,6 +107,10 @@ export function getTypeLen(e: ExtensionType): number {
return GROUP_POINTER_SIZE;
case ExtensionType.GroupMemberPointer:
return GROUP_MEMBER_POINTER_SIZE;
case ExtensionType.TokenGroup:
return TOKEN_GROUP_SIZE;
case ExtensionType.TokenGroupMember:
return TOKEN_GROUP_MEMBER_SIZE;
case ExtensionType.TokenMetadata:
throw Error(`Cannot get type length for variable extension type: ${e}`);
default:
Expand All @@ -127,6 +132,8 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.TokenMetadata:
case ExtensionType.GroupPointer:
case ExtensionType.GroupMemberPointer:
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeAmount:
Expand Down Expand Up @@ -165,6 +172,8 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.TokenMetadata:
case ExtensionType.GroupPointer:
case ExtensionType.GroupMemberPointer:
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand Down Expand Up @@ -197,6 +206,8 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.TransferHookAccount:
case ExtensionType.GroupPointer:
case ExtensionType.GroupMemberPointer:
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
return ExtensionType.Uninitialized;
}
}
Expand Down
1 change: 1 addition & 0 deletions token/js/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './immutableOwner.js';
export * from './interestBearingMint/index.js';
export * from './memoTransfer/index.js';
export * from './metadataPointer/index.js';
export * from './tokenGroup/index.js';
export * from './tokenMetadata/index.js';
export * from './mintCloseAuthority.js';
export * from './nonTransferable.js';
Expand Down
281 changes: 281 additions & 0 deletions token/js/src/extensions/tokenGroup/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
import { sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js';
import {
createInitializeGroupInstruction,
createUpdateGroupMaxSizeInstruction,
createUpdateGroupAuthorityInstruction,
createInitializeMemberInstruction,
TOKEN_GROUP_SIZE,
TOKEN_GROUP_MEMBER_SIZE,
} from '@solana/spl-token-group';

import { TOKEN_2022_PROGRAM_ID } from '../../constants.js';
import { getSigners } from '../../actions/internal.js';

/**
* Initialize a new `Group`
*
* Assumes one has already initialized a mint for the group.
*
* @param connection Connection to use
* @param payer Payer of the transaction fee
* @param mint Group mint
* @param mintAuthority Group mint authority
* @param updateAuthority Group update authority
* @param maxSize Maximum number of members in the group
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function tokenGroupInitializeGroup(
connection: Connection,
payer: Signer,
mint: PublicKey,
mintAuthority: PublicKey | Signer,
updateAuthority: PublicKey | null,
maxSize: number,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<TransactionSignature> {
const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners);

const transaction = new Transaction().add(
createInitializeGroupInstruction({
programId,
group: mint,
mint,
mintAuthority: mintAuthorityPublicKey,
updateAuthority,
maxSize,
})
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Initialize a new `Group` with rent transfer.
*
* Assumes one has already initialized a mint for the group.
*
* @param connection Connection to use
* @param payer Payer of the transaction fee
* @param mint Group mint
* @param mintAuthority Group mint authority
* @param updateAuthority Group update authority
* @param maxSize Maximum number of members in the group
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function tokenGroupInitializeGroupWithRentTransfer(
connection: Connection,
payer: Signer,
mint: PublicKey,
mintAuthority: PublicKey | Signer,
updateAuthority: PublicKey | null,
maxSize: number,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<TransactionSignature> {
const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners);

const lamports = await connection.getMinimumBalanceForRentExemption(TOKEN_GROUP_SIZE);

const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: mint,
lamports,
}),
createInitializeGroupInstruction({
programId,
group: mint,
mint,
mintAuthority: mintAuthorityPublicKey,
updateAuthority,
maxSize,
})
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Update the max size of a `Group`
*
* @param connection Connection to use
* @param payer Payer of the transaction fee
* @param mint Group mint
* @param updateAuthority Group update authority
* @param maxSize Maximum number of members in the group
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function tokenGroupUpdateGroupMaxSize(
connection: Connection,
payer: Signer,
mint: PublicKey,
updateAuthority: PublicKey | Signer,
maxSize: number,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<TransactionSignature> {
const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners);

const transaction = new Transaction().add(
createUpdateGroupMaxSizeInstruction({
programId,
group: mint,
updateAuthority: updateAuthorityPublicKey,
maxSize,
})
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Update the authority of a `Group`
*
* @param connection Connection to use
* @param payer Payer of the transaction fee
* @param mint Group mint
* @param updateAuthority Group update authority
* @param newAuthority New authority for the token group, or unset
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function tokenGroupUpdateGroupAuthority(
connection: Connection,
payer: Signer,
mint: PublicKey,
updateAuthority: PublicKey | Signer,
newAuthority: PublicKey | null,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<TransactionSignature> {
const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners);

const transaction = new Transaction().add(
createUpdateGroupAuthorityInstruction({
programId,
group: mint,
currentAuthority: updateAuthorityPublicKey,
newAuthority,
})
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Initialize a new `Member` of a `Group`
*
* Assumes the `Group` has already been initialized,
* as well as the mint for the member.
*
* @param connection Connection to use
* @param payer Payer of the transaction fee
* @param mint Member mint
* @param mintAuthority Member mint authority
* @param group Group mint
* @param groupUpdateAuthority Group update authority
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function tokenGroupMemberInitialize(
connection: Connection,
payer: Signer,
mint: PublicKey,
mintAuthority: PublicKey | Signer,
group: PublicKey,
groupUpdateAuthority: PublicKey,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<TransactionSignature> {
const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners);

const transaction = new Transaction().add(
createInitializeMemberInstruction({
programId,
member: mint,
memberMint: mint,
memberMintAuthority: mintAuthorityPublicKey,
group,
groupUpdateAuthority,
})
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Initialize a new `Member` of a `Group` with rent transfer.
*
* Assumes the `Group` has already been initialized,
* as well as the mint for the member.
*
* @param connection Connection to use
* @param payer Payer of the transaction fee
* @param mint Member mint
* @param mintAuthority Member mint authority
* @param group Group mint
* @param groupUpdateAuthority Group update authority
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function tokenGroupMemberInitializeWithRentTransfer(
connection: Connection,
payer: Signer,
mint: PublicKey,
mintAuthority: PublicKey | Signer,
group: PublicKey,
groupUpdateAuthority: PublicKey,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<TransactionSignature> {
const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners);

const lamports = await connection.getMinimumBalanceForRentExemption(TOKEN_GROUP_MEMBER_SIZE);

const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: mint,
lamports,
}),
createInitializeMemberInstruction({
programId,
member: mint,
memberMint: mint,
memberMintAuthority: mintAuthorityPublicKey,
group,
groupUpdateAuthority,
})
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}
2 changes: 2 additions & 0 deletions token/js/src/extensions/tokenGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './actions.js';
export * from './state.js';
Loading

0 comments on commit 56b083d

Please sign in to comment.