-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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`!
- Loading branch information
Joe C
authored
Feb 24, 2024
1 parent
e3262a9
commit b7949fc
Showing
8 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
98
token/js/src/extensions/groupMemberPointer/instructions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = TOKEN_2022_PROGRAM_ID | ||
): 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 }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { expect } from 'chai'; | ||
import type { Connection, Signer } from '@solana/web3.js'; | ||
import { PublicKey } from '@solana/web3.js'; | ||
import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; | ||
|
||
import { | ||
ExtensionType, | ||
createInitializeGroupMemberPointerInstruction, | ||
createInitializeMintInstruction, | ||
createUpdateGroupMemberPointerInstruction, | ||
getGroupMemberPointerState, | ||
getMint, | ||
getMintLen, | ||
} from '../../src'; | ||
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; | ||
|
||
const TEST_TOKEN_DECIMALS = 2; | ||
const EXTENSIONS = [ExtensionType.GroupMemberPointer]; | ||
|
||
describe('GroupMember pointer', () => { | ||
let connection: Connection; | ||
let payer: Signer; | ||
let mint: Keypair; | ||
let mintAuthority: Keypair; | ||
let memberAddress: PublicKey; | ||
|
||
before(async () => { | ||
connection = await getConnection(); | ||
payer = await newAccountWithLamports(connection, 1000000000); | ||
mintAuthority = Keypair.generate(); | ||
}); | ||
|
||
beforeEach(async () => { | ||
mint = Keypair.generate(); | ||
memberAddress = PublicKey.unique(); | ||
|
||
const mintLen = getMintLen(EXTENSIONS); | ||
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); | ||
|
||
const transaction = new Transaction().add( | ||
SystemProgram.createAccount({ | ||
fromPubkey: payer.publicKey, | ||
newAccountPubkey: mint.publicKey, | ||
space: mintLen, | ||
lamports, | ||
programId: TEST_PROGRAM_ID, | ||
}), | ||
createInitializeGroupMemberPointerInstruction( | ||
mint.publicKey, | ||
mintAuthority.publicKey, | ||
memberAddress, | ||
TEST_PROGRAM_ID | ||
), | ||
createInitializeMintInstruction( | ||
mint.publicKey, | ||
TEST_TOKEN_DECIMALS, | ||
mintAuthority.publicKey, | ||
null, | ||
TEST_PROGRAM_ID | ||
) | ||
); | ||
|
||
await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined); | ||
}); | ||
|
||
it('can successfully initialize', async () => { | ||
const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); | ||
const groupMemberPointer = getGroupMemberPointerState(mintInfo); | ||
|
||
expect(groupMemberPointer).to.deep.equal({ | ||
authority: mintAuthority.publicKey, | ||
memberAddress, | ||
}); | ||
}); | ||
|
||
it('can update to new address', async () => { | ||
const newGroupMemberAddress = PublicKey.unique(); | ||
const transaction = new Transaction().add( | ||
createUpdateGroupMemberPointerInstruction( | ||
mint.publicKey, | ||
mintAuthority.publicKey, | ||
newGroupMemberAddress, | ||
undefined, | ||
TEST_PROGRAM_ID | ||
) | ||
); | ||
await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); | ||
|
||
const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); | ||
const groupMemberPointer = getGroupMemberPointerState(mintInfo); | ||
|
||
expect(groupMemberPointer).to.deep.equal({ | ||
authority: mintAuthority.publicKey, | ||
memberAddress: newGroupMemberAddress, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.