-
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
- Loading branch information
1 parent
fdd137c
commit e44642b
Showing
7 changed files
with
289 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 | ||
): 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,143 @@ | ||
import { PublicKey, TransactionInstruction } from '@solana/web3.js'; | ||
import { expect } from 'chai'; | ||
import type { Mint } from '../../src'; | ||
import { | ||
TOKEN_2022_PROGRAM_ID, | ||
createInitializeGroupMemberPointerInstruction, | ||
createUpdateGroupMemberPointerInstruction, | ||
getGroupMemberPointerState, | ||
} from '../../src'; | ||
|
||
const AUTHORITY_ADDRESS_BYTES = Buffer.alloc(32).fill(8); | ||
const GROUP_MEMBER_ADDRESS_BYTES = Buffer.alloc(32).fill(5); | ||
const NULL_OPTIONAL_NONZERO_PUBKEY_BYTES = Buffer.alloc(32).fill(0); | ||
|
||
describe('SPL Token 2022 GroupMemberPointer Extension', () => { | ||
it('can create InitializeGroupMemberPointerInstruction', () => { | ||
const mint = PublicKey.unique(); | ||
const authority = new PublicKey(AUTHORITY_ADDRESS_BYTES); | ||
const memberAddress = new PublicKey(GROUP_MEMBER_ADDRESS_BYTES); | ||
const instruction = createInitializeGroupMemberPointerInstruction( | ||
mint, | ||
authority, | ||
memberAddress, | ||
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([ | ||
41, // Token instruction discriminator | ||
0, // GroupMemberPointer instruction discriminator | ||
]), | ||
AUTHORITY_ADDRESS_BYTES, | ||
GROUP_MEMBER_ADDRESS_BYTES, | ||
]), | ||
}) | ||
); | ||
}); | ||
it('can create UpdateGroupMemberPointerInstruction', () => { | ||
const mint = PublicKey.unique(); | ||
const authority = PublicKey.unique(); | ||
const memberAddress = new PublicKey(GROUP_MEMBER_ADDRESS_BYTES); | ||
const instruction = createUpdateGroupMemberPointerInstruction(mint, authority, memberAddress); | ||
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([ | ||
41, // Token instruction discriminator | ||
1, // GroupMemberPointer instruction discriminator | ||
]), | ||
GROUP_MEMBER_ADDRESS_BYTES, | ||
]), | ||
}) | ||
); | ||
}); | ||
it('can create UpdateGroupMemberPointerInstruction to none', () => { | ||
const mint = PublicKey.unique(); | ||
const authority = PublicKey.unique(); | ||
const memberAddress = null; | ||
const instruction = createUpdateGroupMemberPointerInstruction(mint, authority, memberAddress); | ||
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([ | ||
41, // Token instruction discriminator | ||
1, // GroupMemberPointer 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 | ||
22, 0, | ||
// Extension length | ||
64, 0, | ||
]), | ||
AUTHORITY_ADDRESS_BYTES, | ||
GROUP_MEMBER_ADDRESS_BYTES, | ||
]), | ||
} as Mint; | ||
const groupPointer = getGroupMemberPointerState(mintInfo); | ||
expect(groupPointer).to.deep.equal({ | ||
authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), | ||
memberAddress: new PublicKey(GROUP_MEMBER_ADDRESS_BYTES), | ||
}); | ||
}); | ||
it('can get state with only group address', async () => { | ||
const mintInfo = { | ||
tlvData: Buffer.concat([ | ||
Buffer.from([ | ||
// Extension discriminator | ||
22, 0, | ||
// Extension length | ||
64, 0, | ||
]), | ||
NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, | ||
GROUP_MEMBER_ADDRESS_BYTES, | ||
]), | ||
} as Mint; | ||
const groupPointer = getGroupMemberPointerState(mintInfo); | ||
expect(groupPointer).to.deep.equal({ | ||
authority: null, | ||
memberAddress: new PublicKey(GROUP_MEMBER_ADDRESS_BYTES), | ||
}); | ||
}); | ||
|
||
it('can get state with only authority address', async () => { | ||
const mintInfo = { | ||
tlvData: Buffer.concat([ | ||
Buffer.from([ | ||
// Extension discriminator | ||
22, 0, | ||
// Extension length | ||
64, 0, | ||
]), | ||
AUTHORITY_ADDRESS_BYTES, | ||
NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, | ||
]), | ||
} as Mint; | ||
const groupPointer = getGroupMemberPointerState(mintInfo); | ||
expect(groupPointer).to.deep.equal({ | ||
authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), | ||
memberAddress: null, | ||
}); | ||
}); | ||
}); |