-
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
GroupPointer
extension
- Loading branch information
1 parent
c7d1f29
commit fdd137c
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'; |
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 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 }); | ||
} |
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'; | ||
|
||
/** 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; | ||
} | ||
} |
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, | ||
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, | ||
}); | ||
}); | ||
}); |