solana-token-extension-boost
Version:
SDK for Solana Token Extensions with wallet adapter support
440 lines (439 loc) • 20.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TokenGroupExtension = exports.TokenGroupMemberStatus = exports.TokenGroupMemberExtensionType = exports.TokenGroupExtensionType = exports.GroupPointerExtensionType = void 0;
exports.createInitializeGroupPointerInstruction = createInitializeGroupPointerInstruction;
exports.createInitializeTokenGroupInstruction = createInitializeTokenGroupInstruction;
exports.createUpdateTokenGroupInstruction = createUpdateTokenGroupInstruction;
exports.createInitializeTokenGroupMemberInstruction = createInitializeTokenGroupMemberInstruction;
exports.createUpdateTokenGroupMemberInstruction = createUpdateTokenGroupMemberInstruction;
const web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
// Define GroupPointer extension type (not yet available in standard ExtensionType)
exports.GroupPointerExtensionType = 20; // Assumed ID for GroupPointer
// Define TokenGroup and TokenGroupMember extension types
// These are custom extension types for Token-2022 that may not be in the standard ExtensionType enum yet
exports.TokenGroupExtensionType = 23; // ID for TokenGroup
exports.TokenGroupMemberExtensionType = 24; // ID for TokenGroupMember
/**
* Token Group Member Status Flags
*/
var TokenGroupMemberStatus;
(function (TokenGroupMemberStatus) {
TokenGroupMemberStatus[TokenGroupMemberStatus["NONE"] = 0] = "NONE";
TokenGroupMemberStatus[TokenGroupMemberStatus["ACTIVE"] = 1] = "ACTIVE";
TokenGroupMemberStatus[TokenGroupMemberStatus["FROZEN"] = 2] = "FROZEN";
})(TokenGroupMemberStatus || (exports.TokenGroupMemberStatus = TokenGroupMemberStatus = {}));
/**
* Create instruction to initialize group pointer for a token
* @param mint - Mint address
* @param groupMint - Mint address of the token group
* @param programId - Token Extension Program ID
* @returns Instruction to initialize group pointer
*/
function createInitializeGroupPointerInstruction(mint, groupMint, programId = spl_token_1.TOKEN_2022_PROGRAM_ID) {
return {
programId,
keys: [
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: groupMint, isSigner: false, isWritable: false },
],
data: Buffer.from([exports.GroupPointerExtensionType]), // Mock data
};
}
/**
* Create instruction to initialize a mint as a token group
* @param mint - Public key of the mint account
* @param payer - Public key of the payer
* @param groupAuthority - Public key of the authority for managing group
* @param updateAuthority - (Optional) Public key of the authority for updating metadata
* @param name - Group name (max 32 bytes)
* @param symbol - Group symbol (max 10 bytes)
* @param programId - SPL Token program ID
* @returns TransactionInstruction
*/
function createInitializeTokenGroupInstruction(mint, payer, groupAuthority, updateAuthority = null, name, symbol, programId = spl_token_1.TOKEN_2022_PROGRAM_ID) {
const keys = [
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: groupAuthority, isSigner: false, isWritable: false },
];
if (updateAuthority) {
keys.push({ pubkey: updateAuthority, isSigner: false, isWritable: false });
}
// Validate inputs
if (name.length > 32) {
throw new Error('Group name must be 32 characters or less');
}
if (symbol.length > 10) {
throw new Error('Group symbol must be 10 characters or less');
}
// Create data buffer
const nameBuffer = Buffer.alloc(32);
const symbolBuffer = Buffer.alloc(10);
Buffer.from(name).copy(nameBuffer);
Buffer.from(symbol).copy(symbolBuffer);
const dataLayout = Buffer.concat([
Buffer.from([
exports.TokenGroupExtensionType & 0xff,
(exports.TokenGroupExtensionType >> 8) & 0xff,
(exports.TokenGroupExtensionType >> 16) & 0xff,
(exports.TokenGroupExtensionType >> 24) & 0xff,
]),
nameBuffer,
symbolBuffer,
Buffer.from([updateAuthority ? 1 : 0]),
]);
return new web3_js_1.TransactionInstruction({
programId,
keys,
data: dataLayout,
});
}
/**
* Create instruction to update a token group
* @param mint - Public key of the mint account
* @param updateAuthority - Public key of the update authority
* @param name - New group name (max 32 bytes)
* @param symbol - New group symbol (max 10 bytes)
* @param programId - SPL Token program ID
* @returns TransactionInstruction
*/
function createUpdateTokenGroupInstruction(mint, updateAuthority, name, symbol, programId = spl_token_1.TOKEN_2022_PROGRAM_ID) {
// Validate inputs
if (name.length > 32) {
throw new Error('Group name must be 32 characters or less');
}
if (symbol.length > 10) {
throw new Error('Group symbol must be 10 characters or less');
}
// Create data buffer
const nameBuffer = Buffer.alloc(32);
const symbolBuffer = Buffer.alloc(10);
Buffer.from(name).copy(nameBuffer);
Buffer.from(symbol).copy(symbolBuffer);
const dataLayout = Buffer.concat([
Buffer.from([
exports.TokenGroupExtensionType & 0xff,
(exports.TokenGroupExtensionType >> 8) & 0xff,
(exports.TokenGroupExtensionType >> 16) & 0xff,
(exports.TokenGroupExtensionType >> 24) & 0xff,
1, // Update operation
]),
nameBuffer,
symbolBuffer,
]);
return new web3_js_1.TransactionInstruction({
programId,
keys: [
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: updateAuthority, isSigner: true, isWritable: false },
],
data: dataLayout,
});
}
/**
* Create instruction to initialize a mint as a token group member
* @param memberMint - Public key of the member mint account
* @param groupMint - Public key of the group mint account
* @param payer - Public key of the payer
* @param memberAuthority - Public key of the authority for managing member status
* @param programId - SPL Token program ID
* @returns TransactionInstruction
*/
function createInitializeTokenGroupMemberInstruction(memberMint, groupMint, payer, memberAuthority, programId = spl_token_1.TOKEN_2022_PROGRAM_ID) {
return new web3_js_1.TransactionInstruction({
programId,
keys: [
{ pubkey: memberMint, isSigner: false, isWritable: true },
{ pubkey: groupMint, isSigner: false, isWritable: false },
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: memberAuthority, isSigner: false, isWritable: false },
],
data: Buffer.from([
exports.TokenGroupMemberExtensionType & 0xff,
(exports.TokenGroupMemberExtensionType >> 8) & 0xff,
(exports.TokenGroupMemberExtensionType >> 16) & 0xff,
(exports.TokenGroupMemberExtensionType >> 24) & 0xff,
]),
});
}
/**
* Create instruction to update a token group member's status
* @param memberMint - Public key of the member mint account
* @param memberAuthority - Public key of the member authority
* @param status - New status for the member
* @param programId - SPL Token program ID
* @returns TransactionInstruction
*/
function createUpdateTokenGroupMemberInstruction(memberMint, memberAuthority, status, programId = spl_token_1.TOKEN_2022_PROGRAM_ID) {
return new web3_js_1.TransactionInstruction({
programId,
keys: [
{ pubkey: memberMint, isSigner: false, isWritable: true },
{ pubkey: memberAuthority, isSigner: true, isWritable: false },
],
data: Buffer.from([
exports.TokenGroupMemberExtensionType & 0xff,
(exports.TokenGroupMemberExtensionType >> 8) & 0xff,
(exports.TokenGroupMemberExtensionType >> 16) & 0xff,
(exports.TokenGroupMemberExtensionType >> 24) & 0xff,
1, // Update operation
status & 0xff,
(status >> 8) & 0xff,
(status >> 16) & 0xff,
(status >> 24) & 0xff,
]),
});
}
/**
* Class for managing token groups
*/
class TokenGroupExtension {
/**
* Create a new TokenGroupExtension instance
* @param connection - Connection to Solana cluster
* @param mint - Public key of the token group mint
*/
constructor(connection, mint) {
this.connection = connection;
this.mint = mint;
}
/**
* Get token group information
* @returns Promise resolving to token group information
*/
async getTokenGroupInfo() {
try {
const mintInfo = await (0, spl_token_1.getMint)(this.connection, this.mint, 'confirmed', spl_token_1.TOKEN_2022_PROGRAM_ID);
// Extract token group data from TLV
if (mintInfo.tlvData) {
let offset = 0;
while (offset < mintInfo.tlvData.length) {
if (offset + 8 > mintInfo.tlvData.length)
break;
const type = mintInfo.tlvData.readUInt32LE(offset);
const length = mintInfo.tlvData.readUInt32LE(offset + 4);
if (type === exports.TokenGroupExtensionType) {
if (offset + 8 + length > mintInfo.tlvData.length)
break;
// Parse name (32 bytes)
const nameBytes = mintInfo.tlvData.slice(offset + 8, offset + 8 + 32);
const name = nameBytes.toString('utf8').replace(/\0/g, '');
// Parse symbol (10 bytes)
const symbolBytes = mintInfo.tlvData.slice(offset + 8 + 32, offset + 8 + 42);
const symbol = symbolBytes.toString('utf8').replace(/\0/g, '');
// Parse authorities if they exist
let groupAuthority = null;
let updateAuthority = null;
if (length >= 42 + 32) {
const groupAuthorityBytes = mintInfo.tlvData.slice(offset + 8 + 42, offset + 8 + 42 + 32);
groupAuthority = new web3_js_1.PublicKey(groupAuthorityBytes);
}
if (length >= 42 + 64) {
const updateAuthorityFlag = mintInfo.tlvData.readUInt8(offset + 8 + 42);
if (updateAuthorityFlag === 1 && length >= 42 + 65) {
const updateAuthorityBytes = mintInfo.tlvData.slice(offset + 8 + 43, offset + 8 + 43 + 32);
updateAuthority = new web3_js_1.PublicKey(updateAuthorityBytes);
}
}
return {
name,
symbol,
groupAuthority,
updateAuthority,
};
}
offset += 8 + length;
}
}
throw new Error('Token group data not found');
}
catch (error) {
console.error('Error getting token group info:', error);
throw error;
}
}
/**
* Check if a mint is a member of this token group
* @param memberMint - Public key of the potential member mint
* @returns Promise resolving to boolean indicating membership
*/
async isMember(memberMint) {
try {
const mintInfo = await (0, spl_token_1.getMint)(this.connection, memberMint, 'confirmed', spl_token_1.TOKEN_2022_PROGRAM_ID);
// Check the TLV data for TokenGroupMember extension
if (mintInfo.tlvData) {
let offset = 0;
while (offset < mintInfo.tlvData.length) {
if (offset + 8 > mintInfo.tlvData.length)
break;
const type = mintInfo.tlvData.readUInt32LE(offset);
const length = mintInfo.tlvData.readUInt32LE(offset + 4);
if (type === exports.TokenGroupMemberExtensionType) {
// Member found, check if it belongs to this group
if (offset + 8 + 32 > mintInfo.tlvData.length)
break;
const groupMintBytes = mintInfo.tlvData.slice(offset + 8, offset + 8 + 32);
const groupMint = new web3_js_1.PublicKey(groupMintBytes);
return groupMint.equals(this.mint);
}
offset += 8 + length;
}
}
return false; // Not a member
}
catch (error) {
console.error('Error checking group membership:', error);
return false;
}
}
/**
* Get member status
* @param memberMint - Public key of the member mint
* @returns Promise resolving to member status
*/
async getMemberStatus(memberMint) {
try {
const mintInfo = await (0, spl_token_1.getMint)(this.connection, memberMint, 'confirmed', spl_token_1.TOKEN_2022_PROGRAM_ID);
// Check the TLV data for TokenGroupMember extension
if (mintInfo.tlvData) {
let offset = 0;
while (offset < mintInfo.tlvData.length) {
if (offset + 8 > mintInfo.tlvData.length)
break;
const type = mintInfo.tlvData.readUInt32LE(offset);
const length = mintInfo.tlvData.readUInt32LE(offset + 4);
if (type === exports.TokenGroupMemberExtensionType) {
// Member found, check if it belongs to this group
if (offset + 8 + 32 > mintInfo.tlvData.length)
break;
const groupMintBytes = mintInfo.tlvData.slice(offset + 8, offset + 8 + 32);
const groupMint = new web3_js_1.PublicKey(groupMintBytes);
if (groupMint.equals(this.mint)) {
// This is a member of our group, get status
if (offset + 8 + 32 + 4 > mintInfo.tlvData.length)
break;
const status = mintInfo.tlvData.readUInt32LE(offset + 8 + 32);
return status;
}
}
offset += 8 + length;
}
}
return TokenGroupMemberStatus.NONE; // Not a member or no status
}
catch (error) {
console.error('Error getting member status:', error);
return TokenGroupMemberStatus.NONE;
}
}
/**
* Create instructions to create a new token group
* @param connection - Connection to Solana cluster
* @param payer - Public key of the fee payer
* @param mintAuthority - Mint authority
* @param groupAuthority - Group authority
* @param updateAuthority - (Optional) Update authority
* @param name - Group name
* @param symbol - Group symbol
* @param decimals - Number of decimals
* @returns Instructions, signers, and mint address
*/
static async createTokenGroupInstructions(connection, payer, mintAuthority, groupAuthority, updateAuthority, name, symbol, decimals = 6) {
// Generate a new keypair for the mint
const mintKeypair = web3_js_1.Keypair.generate();
const mint = mintKeypair.publicKey;
// Calculate the space needed for the mint with TokenGroup extension
const mintLen = (0, spl_token_1.getMintLen)([exports.TokenGroupExtensionType]);
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
// Create instructions array
const instructions = [];
// Add instruction to create account
instructions.push(web3_js_1.SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint,
space: mintLen,
lamports,
programId: spl_token_1.TOKEN_2022_PROGRAM_ID,
}));
// Add instruction to initialize token group
instructions.push(createInitializeTokenGroupInstruction(mint, payer, groupAuthority, updateAuthority, name, symbol, spl_token_1.TOKEN_2022_PROGRAM_ID));
// Add instruction to initialize mint
instructions.push((0, spl_token_1.createInitializeMintInstruction)(mint, decimals, mintAuthority, null, // freeze authority
spl_token_1.TOKEN_2022_PROGRAM_ID));
return {
instructions,
signers: [mintKeypair],
mint,
};
}
/**
* Create instructions to create a new token group member
* @param connection - Connection to Solana cluster
* @param payer - Public key of the fee payer
* @param mintAuthority - Mint authority
* @param memberAuthority - Member authority
* @param decimals - Number of decimals
* @returns Instructions, signers, and mint address
*/
async createTokenGroupMemberInstructions(payer, mintAuthority, memberAuthority, decimals = 6) {
// Generate a new keypair for the member mint
const mintKeypair = web3_js_1.Keypair.generate();
const memberMint = mintKeypair.publicKey;
// Calculate the space needed for the mint with TokenGroupMember extension
const mintLen = (0, spl_token_1.getMintLen)([exports.TokenGroupMemberExtensionType]);
const lamports = await this.connection.getMinimumBalanceForRentExemption(mintLen);
// Create instructions array
const instructions = [];
// Add instruction to create account
instructions.push(web3_js_1.SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: memberMint,
space: mintLen,
lamports,
programId: spl_token_1.TOKEN_2022_PROGRAM_ID,
}));
// Add instruction to initialize token group member
instructions.push(createInitializeTokenGroupMemberInstruction(memberMint, this.mint, payer, memberAuthority, spl_token_1.TOKEN_2022_PROGRAM_ID));
// Add instruction to initialize mint
instructions.push((0, spl_token_1.createInitializeMintInstruction)(memberMint, decimals, mintAuthority, null, // freeze authority
spl_token_1.TOKEN_2022_PROGRAM_ID));
return {
instructions,
signers: [mintKeypair],
mint: memberMint,
};
}
/**
* Create instruction to update token group information
* @param updateAuthority - Public key of the update authority
* @param name - New group name
* @param symbol - New group symbol
* @returns Instruction to update token group
*/
createUpdateTokenGroupInstruction(updateAuthority, name, symbol) {
return createUpdateTokenGroupInstruction(this.mint, updateAuthority, name, symbol, spl_token_1.TOKEN_2022_PROGRAM_ID);
}
/**
* Create instruction to update a member's status
* @param memberMint - Public key of the member mint
* @param memberAuthority - Public key of the member authority
* @param status - New status for the member
* @returns Instruction to update member status
*/
createUpdateMemberStatusInstruction(memberMint, memberAuthority, status) {
return createUpdateTokenGroupMemberInstruction(memberMint, memberAuthority, status, spl_token_1.TOKEN_2022_PROGRAM_ID);
}
/**
* List all members of this token group
* @returns Promise resolving to array of member mints
*/
async listMembers() {
// This is a simplified implementation that would need to be enhanced
// with a proper query mechanism to find all members efficiently
// In a real implementation, you would use an indexer or RPC method to find all members
// For now, return an empty array as this requires specific chain indexing
return [];
}
}
exports.TokenGroupExtension = TokenGroupExtension;