UNPKG

solana-token-extension-boost

Version:

SDK for Solana Token Extensions with wallet adapter support

474 lines (473 loc) 22.1 kB
"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;