solana-token-extension-boost
Version:
SDK for Solana Token Extensions with wallet adapter support
474 lines (473 loc) • 22.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TokenBuilder = void 0;
const web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
const spl_token_metadata_1 = require("@solana/spl-token-metadata");
/**
* Check extension compatibility
*
* @param extensionTypes Array of extension types to check
* @returns Compatibility check result
*/
function checkExtensionCompatibility(extensionTypes) {
const incompatiblePairs = [];
if (extensionTypes.includes(spl_token_1.ExtensionType.NonTransferable)) {
if (extensionTypes.includes(spl_token_1.ExtensionType.TransferFeeConfig)) {
incompatiblePairs.push([spl_token_1.ExtensionType.NonTransferable, spl_token_1.ExtensionType.TransferFeeConfig]);
}
if (extensionTypes.includes(spl_token_1.ExtensionType.TransferHook)) {
incompatiblePairs.push([spl_token_1.ExtensionType.NonTransferable, spl_token_1.ExtensionType.TransferHook]);
}
if (extensionTypes.includes(spl_token_1.ExtensionType.ConfidentialTransferMint)) {
incompatiblePairs.push([spl_token_1.ExtensionType.NonTransferable, spl_token_1.ExtensionType.ConfidentialTransferMint]);
}
}
if (extensionTypes.includes(spl_token_1.ExtensionType.ConfidentialTransferMint)) {
if (extensionTypes.includes(spl_token_1.ExtensionType.TransferFeeConfig)) {
incompatiblePairs.push([spl_token_1.ExtensionType.ConfidentialTransferMint, spl_token_1.ExtensionType.TransferFeeConfig]);
}
if (extensionTypes.includes(spl_token_1.ExtensionType.TransferHook)) {
incompatiblePairs.push([spl_token_1.ExtensionType.ConfidentialTransferMint, spl_token_1.ExtensionType.TransferHook]);
}
if (extensionTypes.includes(spl_token_1.ExtensionType.PermanentDelegate)) {
incompatiblePairs.push([spl_token_1.ExtensionType.ConfidentialTransferMint, spl_token_1.ExtensionType.PermanentDelegate]);
}
}
if (incompatiblePairs.length > 0) {
const reasons = incompatiblePairs.map(([a, b]) => `${spl_token_1.ExtensionType[a]} and ${spl_token_1.ExtensionType[b]} cannot be used together`);
return {
isCompatible: false,
incompatiblePairs,
reason: reasons.join("; ")
};
}
return { isCompatible: true };
}
class TokenBuilder {
/**
* Initialize builder with connection
*
* @param connection - Connection to Solana cluster
*/
constructor(connection) {
this.extensions = [];
this.decimals = 9;
this.mintAuthority = null;
this.freezeAuthority = null;
this.connection = connection;
}
/**
* Set basic token information
*
* @param decimals - Token decimals
* @param mintAuthority - Mint authority of the token
* @param freezeAuthority - Freeze authority of the token (optional)
* @returns this - for method chaining
*/
setTokenInfo(decimals, mintAuthority, freezeAuthority) {
this.decimals = decimals;
this.mintAuthority = mintAuthority;
this.freezeAuthority = freezeAuthority || null;
return this;
}
/**
* Add metadata extension
*
* @param name - Token name
* @param symbol - Token symbol
* @param uri - URI to metadata
* @param additionalMetadata - Additional metadata (optional)
* @returns this - for method chaining
*/
addMetadata(name, symbol, uri, additionalMetadata) {
this.metadata = { name, symbol, uri, additionalMetadata };
this.extensions.push(spl_token_1.ExtensionType.MetadataPointer);
return this;
}
/**
* Add token metadata extension (embedded metadata)
*
* When using this extension, metadata will be stored directly in the mint account
* and does not require a separate metadata account
*
* @param name - Token name
* @param symbol - Token symbol
* @param uri - URI to metadata
* @param additionalMetadata - Additional metadata (optional)
* @returns this - for method chaining
*/
addTokenMetadata(name, symbol, uri, additionalMetadata) {
this.tokenMetadata = { name, symbol, uri, additionalMetadata };
// Metadata needs MetadataPointer extension
this.extensions.push(spl_token_1.ExtensionType.MetadataPointer);
return this;
}
/**
* Add transfer fee extension
*
* @param feeBasisPoints - Fee in basis points (1% = 100 basis points)
* @param maxFee - Maximum fee
* @param transferFeeConfigAuthority - Account with authority to update fee config
* @param withdrawWithheldAuthority - Account with authority to withdraw collected fees
* @returns this - for method chaining
*/
addTransferFee(feeBasisPoints, maxFee, transferFeeConfigAuthority, withdrawWithheldAuthority) {
this.transferFee = {
feeBasisPoints,
maxFee,
transferFeeConfigAuthority,
withdrawWithheldAuthority
};
this.extensions.push(spl_token_1.ExtensionType.TransferFeeConfig);
return this;
}
/**
* Add permanent delegate extension
*
* @param delegate - Permanent delegate address
* @returns this - for method chaining
*/
addPermanentDelegate(delegate) {
this.permanentDelegate = delegate;
this.extensions.push(spl_token_1.ExtensionType.PermanentDelegate);
return this;
}
/**
* Add interest bearing extension
*
* @param rate - Interest rate (basis points)
* @param rateAuthority - Account with authority to update interest rate
* @returns this - for method chaining
*/
addInterestBearing(rate, rateAuthority) {
this.interestBearing = {
rate,
rateAuthority
};
this.extensions.push(spl_token_1.ExtensionType.InterestBearingConfig);
return this;
}
/**
* Add transfer hook extension
*
* @param programId - Address of transfer hook program
* @param extraMetas - Additional metadata (optional)
* @returns this - for method chaining
*/
addTransferHook(programId, extraMetas) {
this.transferHook = {
programId,
extraMetas
};
this.extensions.push(spl_token_1.ExtensionType.TransferHook);
return this;
}
/**
* Add non-transferable extension
*
* @returns this - for method chaining
*/
addNonTransferable() {
this.extensions.push(spl_token_1.ExtensionType.NonTransferable);
return this;
}
/**
* Add confidential transfer extension
*
* @param autoEnable - Whether to auto-enable confidential transfers
* @returns this - for method chaining
*/
addConfidentialTransfer(autoEnable = false) {
this.confidentialTransfer = {
autoEnable
};
this.extensions.push(spl_token_1.ExtensionType.ConfidentialTransferMint);
return this;
}
/**
* Add default account state extension
*
* @param state - Default account state
* @param freezeAuthority - Freeze authority (optional)
* @returns this - for method chaining
*/
addDefaultAccountState(state, freezeAuthority) {
this.defaultAccountState = state;
this.extensions.push(spl_token_1.ExtensionType.DefaultAccountState);
return this;
}
/**
* Add mint close authority extension
*
* @param closeAuthority - Close authority
* @returns this - for method chaining
*/
addMintCloseAuthority(closeAuthority) {
this.mintCloseAuthority = closeAuthority;
this.extensions.push(spl_token_1.ExtensionType.MintCloseAuthority);
return this;
}
/**
* Create instructions for token with configured extensions
*
* This method returns instructions instead of executing transaction,
* making it easy to integrate with wallet adapter.
*
* @param payer - Public key of the transaction fee payer
* @returns Promise with instructions, required signers, and mint address
*/
async createTokenInstructions(payer) {
const hasMetadata = this.metadata || this.tokenMetadata;
const hasOtherExtensions = this.extensions.filter(ext => ext !== spl_token_1.ExtensionType.MetadataPointer).length > 0;
// Check extension compatibility
const compatibilityCheck = checkExtensionCompatibility(this.extensions);
if (!compatibilityCheck.isCompatible) {
throw new Error(`Incompatible extensions: ${compatibilityCheck.reason}`);
}
if (hasMetadata && hasOtherExtensions) {
return this.createTokenWithMetadataAndExtensionsInstructions(payer);
}
else {
return this.createTokenWithExtensionsInstructions(payer);
}
}
/**
* Create instructions for token with multiple extensions - simplified version
*
* @param payer - Public key of the transaction fee payer
* @returns Promise with instructions, required signers, and mint address
*/
async createTokenWithExtensionsInstructions(payer) {
if (!this.mintAuthority) {
throw new Error("Mint authority is required");
}
try {
console.log("Creating instructions for token with extensions...");
const mintKeypair = web3_js_1.Keypair.generate();
const mint = mintKeypair.publicKey;
console.log(`Mint address: ${mint.toString()}`);
// If tokenMetadata is provided, use MetadataHelper
if (this.tokenMetadata) {
console.log("Using MetadataHelper to create token with metadata...");
const { MetadataHelper } = require('./metadata-helper');
const result = await MetadataHelper.createTokenWithMetadataInstructions(this.connection, payer, {
decimals: this.decimals,
mintAuthority: this.mintAuthority,
name: this.tokenMetadata.name,
symbol: this.tokenMetadata.symbol,
uri: this.tokenMetadata.uri,
additionalMetadata: this.tokenMetadata.additionalMetadata,
extensions: this.extensions
});
return {
instructions: result.instructions,
signers: result.signers,
mint: result.mint
};
}
console.log("Creating mint with other extensions...");
const extensionsToUse = [...this.extensions];
const mintLen = (0, spl_token_1.getMintLen)(extensionsToUse);
console.log(`Mint size: ${mintLen} bytes`);
const lamports = await this.connection.getMinimumBalanceForRentExemption(mintLen);
const instructions = [];
// 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 extension instructions
if (this.extensions.includes(spl_token_1.ExtensionType.NonTransferable)) {
console.log("Adding NonTransferable extension...");
instructions.push((0, spl_token_1.createInitializeNonTransferableMintInstruction)(mint, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.transferFee) {
console.log("Adding TransferFee extension...");
instructions.push((0, spl_token_1.createInitializeTransferFeeConfigInstruction)(mint, this.transferFee.transferFeeConfigAuthority, this.transferFee.withdrawWithheldAuthority, this.transferFee.feeBasisPoints, this.transferFee.maxFee, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.permanentDelegate) {
console.log("Adding PermanentDelegate extension...");
instructions.push((0, spl_token_1.createInitializePermanentDelegateInstruction)(mint, this.permanentDelegate, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.interestBearing) {
console.log("Adding InterestBearing extension...");
instructions.push((0, spl_token_1.createInitializeInterestBearingMintInstruction)(mint, this.interestBearing.rateAuthority, this.interestBearing.rate, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.transferHook) {
console.log("Adding TransferHook extension...");
instructions.push((0, spl_token_1.createInitializeTransferHookInstruction)(mint, payer, this.transferHook.programId, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.confidentialTransfer) {
console.log("Warning: ConfidentialTransfer extension is not fully supported yet");
}
if (this.defaultAccountState !== undefined) {
console.log("Adding DefaultAccountState extension...");
instructions.push((0, spl_token_1.createInitializeDefaultAccountStateInstruction)(mint, this.defaultAccountState, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.mintCloseAuthority) {
console.log("Adding MintCloseAuthority extension...");
instructions.push((0, spl_token_1.createInitializeMintCloseAuthorityInstruction)(mint, this.mintCloseAuthority, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
// Initialize mint after extensions
console.log("Initializing mint after extensions...");
instructions.push((0, spl_token_1.createInitializeMintInstruction)(mint, this.decimals, this.mintAuthority, this.freezeAuthority, spl_token_1.TOKEN_2022_PROGRAM_ID));
return {
instructions,
signers: [mintKeypair],
mint
};
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to create token instructions: ${error.message}`);
}
else {
throw new Error(`Unknown error creating token instructions: ${String(error)}`);
}
}
}
/**
* Create instructions for token with metadata and other extensions
*
* @param payer - Public key of the transaction fee payer
* @returns Promise with instructions, required signers, and mint address
*/
async createTokenWithMetadataAndExtensionsInstructions(payer) {
if (!this.mintAuthority) {
throw new Error("Mint authority is required");
}
const metadata = this.metadata || this.tokenMetadata;
if (!metadata) {
throw new Error("Metadata is required for this method");
}
try {
console.log("Creating instructions for token with metadata and other extensions...");
const mintKeypair = web3_js_1.Keypair.generate();
const mint = mintKeypair.publicKey;
console.log(`Mint address: ${mint.toString()}`);
// Prepare token metadata
const tokenMetadata = {
mint: mint,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
additionalMetadata: Object.entries(metadata.additionalMetadata || {}).map(([key, value]) => [key, value]),
};
// Ensure MetadataPointer extension is included
let extensionsToUse = [...this.extensions];
if (!extensionsToUse.includes(spl_token_1.ExtensionType.MetadataPointer)) {
extensionsToUse.push(spl_token_1.ExtensionType.MetadataPointer);
}
// Check if Non-Transferable extension is included
const hasNonTransferable = extensionsToUse.includes(spl_token_1.ExtensionType.NonTransferable);
// Calculate sizes
const metadataExtension = spl_token_1.TYPE_SIZE + spl_token_1.LENGTH_SIZE;
const metadataLen = (0, spl_token_metadata_1.pack)(tokenMetadata).length;
const mintLen = (0, spl_token_1.getMintLen)(extensionsToUse);
const lamports = await this.connection.getMinimumBalanceForRentExemption(mintLen + metadataExtension + metadataLen);
console.log(`Size: mint=${mintLen}, metadata extension=${metadataExtension}, metadata=${metadataLen}`);
const instructions = [];
// Create account
instructions.push(web3_js_1.SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint,
space: mintLen,
lamports,
programId: spl_token_1.TOKEN_2022_PROGRAM_ID,
}));
// Initialize MetadataPointer extension
instructions.push((0, spl_token_1.createInitializeMetadataPointerInstruction)(mint, payer, mint, spl_token_1.TOKEN_2022_PROGRAM_ID));
// Split extensions into 3 groups:
// 1. Non-Transferable (must be initialized before mint)
// 2. Other extensions (except Non-Transferable)
// 3. Metadata (must be initialized after mint)
// 1. Initialize Non-Transferable extension first (if present)
if (hasNonTransferable) {
console.log("Adding NonTransferable extension before initializing mint...");
instructions.push((0, spl_token_1.createInitializeNonTransferableMintInstruction)(mint, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
// 2. Add other extensions (except Non-Transferable)
if (this.transferFee) {
instructions.push((0, spl_token_1.createInitializeTransferFeeConfigInstruction)(mint, this.transferFee.transferFeeConfigAuthority, this.transferFee.withdrawWithheldAuthority, this.transferFee.feeBasisPoints, this.transferFee.maxFee, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.permanentDelegate) {
instructions.push((0, spl_token_1.createInitializePermanentDelegateInstruction)(mint, this.permanentDelegate, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.transferHook) {
instructions.push((0, spl_token_1.createInitializeTransferHookInstruction)(mint, payer, this.transferHook.programId, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.confidentialTransfer) {
console.log("Warning: ConfidentialTransfer extension is not fully supported yet");
}
if (this.interestBearing) {
instructions.push((0, spl_token_1.createInitializeInterestBearingMintInstruction)(mint, this.interestBearing.rateAuthority, this.interestBearing.rate, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.defaultAccountState !== undefined) {
instructions.push((0, spl_token_1.createInitializeDefaultAccountStateInstruction)(mint, this.defaultAccountState, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
if (this.mintCloseAuthority) {
instructions.push((0, spl_token_1.createInitializeMintCloseAuthorityInstruction)(mint, this.mintCloseAuthority, spl_token_1.TOKEN_2022_PROGRAM_ID));
}
// 3. Initialize mint after extensions (important: after Non-Transferable and before Metadata)
console.log("Initializing mint after extensions...");
instructions.push((0, spl_token_1.createInitializeMintInstruction)(mint, this.decimals, this.mintAuthority, this.freezeAuthority, spl_token_1.TOKEN_2022_PROGRAM_ID));
// 4. Initialize metadata (must be after mint initialization)
console.log("Initializing metadata after initializing mint...");
instructions.push((0, spl_token_metadata_1.createInitializeInstruction)({
programId: spl_token_1.TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: payer,
mint: mint,
mintAuthority: this.mintAuthority,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
}));
// Add additional metadata if provided
if (metadata.additionalMetadata) {
for (const [key, value] of Object.entries(metadata.additionalMetadata)) {
instructions.push((0, spl_token_metadata_1.createUpdateFieldInstruction)({
programId: spl_token_1.TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: payer,
field: key,
value: value,
}));
}
}
return {
instructions,
signers: [mintKeypair],
mint
};
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to create token instructions: ${error.message}`);
}
else {
throw new Error(`Unknown error creating token instructions: ${String(error)}`);
}
}
}
/**
* Build transaction from token instructions
*
* Utility method to help users create transaction from instructions
*
* @param instructions - Instructions to include in transaction
* @param feePayer - Public key of fee payer
* @returns Configured transaction
*/
buildTransaction(instructions, feePayer) {
const transaction = new web3_js_1.Transaction();
instructions.forEach(instruction => transaction.add(instruction));
transaction.feePayer = feePayer;
return transaction;
}
}
exports.TokenBuilder = TokenBuilder;