-
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
As mentioned in #6175, the `GroupPointer` extension is live on Token-2022 mainnet-beta, but it's not currently supported in the `@solana/spl-token`. This change adds that support!
- Loading branch information
Joe C
authored
Feb 24, 2024
1 parent
2062613
commit e3262a9
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'; |
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 = TOKEN_2022_PROGRAM_ID | ||
): 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 GroupPointer 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,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, | ||
createInitializeGroupPointerInstruction, | ||
createInitializeMintInstruction, | ||
createUpdateGroupPointerInstruction, | ||
getGroupPointerState, | ||
getMint, | ||
getMintLen, | ||
} from '../../src'; | ||
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; | ||
|
||
const TEST_TOKEN_DECIMALS = 2; | ||
const EXTENSIONS = [ExtensionType.GroupPointer]; | ||
|
||
describe('Group pointer', () => { | ||
let connection: Connection; | ||
let payer: Signer; | ||
let mint: Keypair; | ||
let mintAuthority: Keypair; | ||
let groupAddress: PublicKey; | ||
|
||
before(async () => { | ||
connection = await getConnection(); | ||
payer = await newAccountWithLamports(connection, 1000000000); | ||
mintAuthority = Keypair.generate(); | ||
}); | ||
|
||
beforeEach(async () => { | ||
mint = Keypair.generate(); | ||
groupAddress = 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, | ||
}), | ||
createInitializeGroupPointerInstruction( | ||
mint.publicKey, | ||
mintAuthority.publicKey, | ||
groupAddress, | ||
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 groupPointer = getGroupPointerState(mintInfo); | ||
|
||
expect(groupPointer).to.deep.equal({ | ||
authority: mintAuthority.publicKey, | ||
groupAddress, | ||
}); | ||
}); | ||
|
||
it('can update to new address', async () => { | ||
const newGroupAddress = PublicKey.unique(); | ||
const transaction = new Transaction().add( | ||
createUpdateGroupPointerInstruction( | ||
mint.publicKey, | ||
mintAuthority.publicKey, | ||
newGroupAddress, | ||
undefined, | ||
TEST_PROGRAM_ID | ||
) | ||
); | ||
await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); | ||
|
||
const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); | ||
const groupPointer = getGroupPointerState(mintInfo); | ||
|
||
expect(groupPointer).to.deep.equal({ | ||
authority: mintAuthority.publicKey, | ||
groupAddress: newGroupAddress, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.