UNPKG

solana-token-extension-boost

Version:

SDK for Solana Token Extensions with wallet adapter support

477 lines (476 loc) 25.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TokenMetadataToken = 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"); const token_1 = require("../../core/token"); class TokenMetadataToken extends token_1.Token { constructor(connection, mint, metadata) { super(connection, mint); this.metadata = metadata; } getMintAddress() { return this.mint; } static async create(connection, payer, params) { const { decimals, mintAuthority, metadata } = params; if (!metadata.name || metadata.name.length > 32) { throw new Error("Metadata name is required and must be 32 characters or less"); } if (!metadata.symbol || metadata.symbol.length > 10) { throw new Error("Metadata symbol is required and must be 10 characters or less"); } if (!metadata.uri || metadata.uri.length > 200) { throw new Error("Metadata URI is required and must be 200 characters or less"); } const mintKeypair = web3_js_1.Keypair.generate(); const mint = mintKeypair.publicKey; const tokenMetadata = { mint: mint, name: metadata.name, symbol: metadata.symbol, uri: metadata.uri, additionalMetadata: Object.entries(metadata.additionalMetadata || {}).map(([key, value]) => [key, value]), }; try { const metadataExtension = spl_token_1.TYPE_SIZE + spl_token_1.LENGTH_SIZE; // 4 bytes const metadataLen = (0, spl_token_metadata_1.pack)(tokenMetadata).length; const mintLen = (0, spl_token_1.getMintLen)([spl_token_1.ExtensionType.MetadataPointer]); const totalSize = mintLen + metadataExtension + metadataLen + 2048; console.log(`Kích thước mint: ${mintLen} bytes, metadata: ${metadataLen} bytes, extension: ${metadataExtension} bytes, tổng: ${totalSize} bytes`); const lamports = await connection.getMinimumBalanceForRentExemption(totalSize); console.log("step 1: create account..."); const createAccountTx = new web3_js_1.Transaction().add(web3_js_1.SystemProgram.createAccount({ fromPubkey: payer.publicKey, newAccountPubkey: mint, space: totalSize, lamports, programId: spl_token_1.TOKEN_2022_PROGRAM_ID, })); await connection.sendTransaction(createAccountTx, [payer, mintKeypair]); console.log("step 2: create MetadataPointer..."); const initPointerTx = new web3_js_1.Transaction().add((0, spl_token_1.createInitializeMetadataPointerInstruction)(mint, payer.publicKey, null, spl_token_1.TOKEN_2022_PROGRAM_ID)); await connection.sendTransaction(initPointerTx, [payer]); console.log("step 3: create Mint..."); const initMintTx = new web3_js_1.Transaction().add((0, spl_token_1.createInitializeMintInstruction)(mint, decimals, mintAuthority, null, spl_token_1.TOKEN_2022_PROGRAM_ID)); await connection.sendTransaction(initMintTx, [payer]); console.log("step 4: update MetadataPointer..."); const updatePointerTx = new web3_js_1.Transaction().add((0, spl_token_1.createUpdateMetadataPointerInstruction)(mint, payer.publicKey, mint, [], spl_token_1.TOKEN_2022_PROGRAM_ID)); await connection.sendTransaction(updatePointerTx, [payer]); console.log("step 5: initialize metadata..."); const initMetadataTx = new web3_js_1.Transaction().add((0, spl_token_metadata_1.createInitializeInstruction)({ programId: spl_token_1.TOKEN_2022_PROGRAM_ID, metadata: mint, updateAuthority: payer.publicKey, mint: mint, mintAuthority: payer.publicKey, name: metadata.name, symbol: metadata.symbol, uri: metadata.uri, })); await connection.sendTransaction(initMetadataTx, [payer]); // Thêm các trường metadata bổ sung if (metadata.additionalMetadata && Object.keys(metadata.additionalMetadata).length > 0) { console.log("step 6: adding additional metadata fields..."); for (const [key, value] of Object.entries(metadata.additionalMetadata)) { try { const addFieldTx = new web3_js_1.Transaction().add((0, spl_token_metadata_1.createUpdateFieldInstruction)({ programId: spl_token_1.TOKEN_2022_PROGRAM_ID, metadata: mint, updateAuthority: payer.publicKey, field: key, value: value })); await connection.sendTransaction(addFieldTx, [payer]); console.log(` ✓ Added field "${key}" successfully`); } catch (err) { console.warn(` ⚠ Unable to add field "${key}": ${err instanceof Error ? err.message : String(err)}`); } } } console.log(`🔍explorer: https://explorer.solana.com/address/${mint.toBase58()}?cluster=devnet`); return new TokenMetadataToken(connection, mint, metadata); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`❌ Lỗi khi tạo token với metadata: ${errorMessage}`); throw new Error(`Failed to create token with metadata: ${errorMessage}`); } } static async fromMint(connection, mint) { try { const tokenMetadata = await (0, spl_token_1.getTokenMetadata)(connection, mint, "confirmed", spl_token_1.TOKEN_2022_PROGRAM_ID); if (!tokenMetadata) { return null; } const additionalMetadata = {}; if (tokenMetadata.additionalMetadata) { for (const [key, value] of tokenMetadata.additionalMetadata) { additionalMetadata[key] = value; } } const metadata = { name: tokenMetadata.name, symbol: tokenMetadata.symbol, uri: tokenMetadata.uri, additionalMetadata }; return new TokenMetadataToken(connection, mint, metadata); } catch (error) { console.error("Error loading metadata token:", error); return null; } } async getTokenMetadata() { const metadata = await (0, spl_token_1.getTokenMetadata)(this.connection, this.mint, "confirmed", spl_token_1.TOKEN_2022_PROGRAM_ID); if (!metadata) { throw new Error("No metadata found for this token"); } return metadata; } async updateMetadataField(authority, field, value) { const instruction = this.createUpdateMetadataFieldInstruction(authority.publicKey, field, value); const transaction = new web3_js_1.Transaction().add(instruction); await this.connection.sendTransaction(transaction, [authority]); const metadata = await this.getTokenMetadata(); return { signature: "", metadata }; } // New method for wallet adapter compatibility createUpdateMetadataFieldInstruction(updateAuthority, field, value) { return (0, spl_token_metadata_1.createUpdateFieldInstruction)({ programId: spl_token_1.TOKEN_2022_PROGRAM_ID, metadata: this.mint, updateAuthority: updateAuthority, field: field, value: value, }); } async removeMetadataField(authority, key) { const instruction = this.createRemoveMetadataFieldInstruction(authority.publicKey, key); const transaction = new web3_js_1.Transaction().add(instruction); await this.connection.sendTransaction(transaction, [authority]); const metadata = await this.getTokenMetadata(); return { signature: "", metadata }; } // New method for wallet adapter compatibility createRemoveMetadataFieldInstruction(updateAuthority, key, idempotent = false) { return (0, spl_token_metadata_1.createRemoveKeyInstruction)({ programId: spl_token_1.TOKEN_2022_PROGRAM_ID, metadata: this.mint, updateAuthority: updateAuthority, key: key, idempotent: idempotent }); } async updateMetadataBatch(authority, fields) { const transaction = new web3_js_1.Transaction(); // Add instructions for each field for (const [field, value] of Object.entries(fields)) { transaction.add(this.createUpdateMetadataFieldInstruction(authority.publicKey, field, value)); } await this.connection.sendTransaction(transaction, [authority]); const metadata = await this.getTokenMetadata(); return { signature: "", metadata }; } // New method for wallet adapter compatibility createUpdateMetadataBatchInstructions(updateAuthority, fields) { const instructions = []; for (const [key, value] of Object.entries(fields)) { instructions.push((0, spl_token_metadata_1.createUpdateFieldInstruction)({ programId: spl_token_1.TOKEN_2022_PROGRAM_ID, metadata: this.mint, updateAuthority: updateAuthority, field: key, value, })); } return instructions; } async getNFTMetadata() { const metadata = await this.getTokenMetadata(); if (!metadata.uri) { throw new Error("Token metadata has no URI"); } const response = await fetch(metadata.uri); if (!response.ok) { throw new Error(`Failed to fetch metadata from ${metadata.uri}`); } return await response.json(); } getMetadataConfig() { return this.metadata; } async updateMetadataAuthority(currentAuthority, newAuthority) { const instruction = this.createUpdateMetadataAuthorityInstruction(currentAuthority.publicKey, newAuthority); const transaction = new web3_js_1.Transaction().add(instruction); await this.connection.sendTransaction(transaction, [currentAuthority]); return ""; } // New method for wallet adapter compatibility createUpdateMetadataAuthorityInstruction(currentAuthority, newAuthority) { return (0, spl_token_metadata_1.createUpdateAuthorityInstruction)({ programId: spl_token_1.TOKEN_2022_PROGRAM_ID, metadata: this.mint, oldAuthority: currentAuthority, newAuthority: newAuthority, }); } /** * Tạo instruction ưu tiên với mức phí thích hợp * @param priorityLevel Mức độ ưu tiên: 'low', 'medium', 'high' * @returns Instruction phí ưu tiên */ static createPriorityFeeInstruction(priorityLevel = 'medium') { let microLamports; switch (priorityLevel) { case 'low': microLamports = 5000; break; case 'high': microLamports = 20000; break; case 'medium': default: microLamports = 10000; } return web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports, }); } /** * Calculate and allocate space for metadata * @param connection - Solana connection * @param mint - Mint address * @param fieldName - Name of the field to add or update * @param fieldValue - Value to set for the field * @param payer - Payer public key * @returns Instruction to allocate additional space (or null if not needed) */ async calculateAndAllocateSpaceForField(connection, mint, fieldName, fieldValue, payer) { try { // Lấy metadata hiện tại để so sánh kích thước const currentMetadata = await (0, spl_token_1.getTokenMetadata)(connection, mint, "confirmed", spl_token_1.TOKEN_2022_PROGRAM_ID); // Tìm trường metadata hiện tại để so sánh let currentFieldValue = ""; if (fieldName === "uri" && currentMetadata) { currentFieldValue = currentMetadata.uri || ""; } else if (currentMetadata?.additionalMetadata) { for (const [key, value] of currentMetadata.additionalMetadata) { if (key === fieldName) { currentFieldValue = value; break; } } } // 1. So sánh kích thước: Chỉ cấp phát nếu giá trị mới dài hơn giá trị cũ if (fieldValue.length <= currentFieldValue.length) { console.log(`🔍 No need to allocate space for field "${fieldName}": New value (${fieldValue.length} bytes) <= old value (${currentFieldValue.length} bytes)`); return null; // No allocation needed if new value is shorter or equal } // 2. Tính toán không gian thực sự cần thêm (chỉ phần tăng thêm) const additionalSize = fieldValue.length - currentFieldValue.length; // Thêm padding cho phần mở rộng nếu cần const paddingSize = fieldName === "uri" ? 8 : 4; const totalAdditionalSize = additionalSize + paddingSize; // 3. Lấy chi phí rent exemption chính xác cho số byte bổ sung const rentPerByte = await connection.getMinimumBalanceForRentExemption(1); const requiredLamports = totalAdditionalSize * rentPerByte; console.log(`🔄 Allocating additional ${totalAdditionalSize} bytes for field "${fieldName}" (${requiredLamports / web3_js_1.LAMPORTS_PER_SOL} SOL)`); // 4. Tạo instruction chuyển SOL cho không gian bổ sung return web3_js_1.SystemProgram.transfer({ fromPubkey: payer, toPubkey: mint, lamports: requiredLamports, }); } catch (error) { console.error(`❌ Error calculating space for field "${fieldName}":`, error); // Calculate space needed for new field (fallback method) const estimatedSize = fieldName.length + fieldValue.length + 16; // Add padding const rentPerByte = await connection.getMinimumBalanceForRentExemption(1); console.log(`⚠️ Using fallback method: allocating ${estimatedSize} bytes (${(estimatedSize * rentPerByte) / web3_js_1.LAMPORTS_PER_SOL} SOL)`); return web3_js_1.SystemProgram.transfer({ fromPubkey: payer, toPubkey: mint, lamports: estimatedSize * rentPerByte, }); } } /** * Calculate and allocate space efficiently for multiple metadata fields * @param connection - Solana connection * @param mint - Mint address * @param fields - Object with field names and values * @param payer - Payer public key * @returns Instruction to allocate additional space (or null if not needed) */ async calculateAndAllocateSpaceForMultipleFields(connection, mint, fields, payer) { try { // 1. Kiểm tra metadata hiện tại để xác định trường nào mới hoặc cần thêm dung lượng const currentMetadata = await (0, spl_token_1.getTokenMetadata)(connection, mint, "confirmed", spl_token_1.TOKEN_2022_PROGRAM_ID); // Chuyển additionalMetadata hiện tại thành đối tượng để dễ so sánh const currentFields = {}; if (currentMetadata?.additionalMetadata) { for (const [key, value] of currentMetadata.additionalMetadata) { currentFields[key] = value; } } // 2. Tính toán kích thước cần thêm cho các trường mới và trường thay đổi let additionalSize = 0; const fieldChanges = {}; for (const [field, value] of Object.entries(fields)) { // Xử lý trường đặc biệt "uri" if (field === "uri" && currentMetadata) { const currentValue = currentMetadata.uri || ""; if (value.length > currentValue.length) { const diff = value.length - currentValue.length; additionalSize += diff; fieldChanges[field] = { old: currentValue.length, new: value.length, diff }; } continue; } // Xử lý các trường thông thường const currentValue = currentFields[field]; if (currentValue === undefined) { // Trường mới: cần không gian cho cả key và value additionalSize += field.length + value.length + 8; // overhead cho mỗi cặp key-value fieldChanges[field] = { old: 0, new: value.length, diff: field.length + value.length + 8 }; } else if (value.length > currentValue.length) { // Trường hiện có nhưng cần thêm không gian (giá trị mới dài hơn) const diff = value.length - currentValue.length; additionalSize += diff; fieldChanges[field] = { old: currentValue.length, new: value.length, diff }; } // Nếu giá trị mới ngắn hơn hoặc bằng, không cần thêm không gian } // 3. In thông tin chi tiết về những trường cần cấp phát thêm if (Object.keys(fieldChanges).length > 0) { console.log(`📊 Chi tiết thay đổi kích thước trường:`); for (const [field, change] of Object.entries(fieldChanges)) { console.log(` - "${field}": ${change.old} -> ${change.new} bytes (+${change.diff} bytes)`); } } // 4. Nếu không cần thêm không gian, trả về null if (additionalSize <= 0) { console.log(`✅ No additional space allocation needed for ${Object.keys(fields).length} fields`); return null; } // 5. Thêm padding để đảm bảo đủ không gian cho metadata const paddingSize = Math.min(32, additionalSize * 0.1); // Padding 10% but not more than 32 bytes additionalSize += paddingSize; // 6. Lấy chi phí rent exemption cho mỗi byte const rentPerByte = await connection.getMinimumBalanceForRentExemption(1); const requiredLamports = additionalSize * rentPerByte; // 7. Log thông tin chi phí console.log(`🔄 Allocating additional ${additionalSize} bytes (${(requiredLamports / web3_js_1.LAMPORTS_PER_SOL).toFixed(6)} SOL) for ${Object.keys(fieldChanges).length} fields`); // 8. Tạo instruction chuyển SOL cho không gian bổ sung return web3_js_1.SystemProgram.transfer({ fromPubkey: payer, toPubkey: mint, lamports: requiredLamports, }); } catch { // Do nothing // Phương pháp dự phòng: tính toán đơn giản let totalSize = 0; for (const [field, value] of Object.entries(fields)) { totalSize += field.length + value.length + 8; } // Thêm padding totalSize += 32; // Lấy chi phí rent exemption const rentPerByte = await connection.getMinimumBalanceForRentExemption(1); const backupLamports = totalSize * rentPerByte * 0.25; // Chỉ cấp phát 25% kích thước ước tính để tiết kiệm console.log(`⚠️ Sử dụng phương pháp dự phòng: cấp phát cho ${totalSize} bytes × 25% = ${(backupLamports / web3_js_1.LAMPORTS_PER_SOL).toFixed(6)} SOL`); return web3_js_1.SystemProgram.transfer({ fromPubkey: payer, toPubkey: mint, lamports: backupLamports, }); } } /** * Cập nhật metadata với tối ưu hóa * @param connection Kết nối Solana * @param wallet Đối tượng wallet-adapter * @param fieldName Tên trường cần cập nhật * @param fieldValue Giá trị mới * @param options Tùy chọn (phí ưu tiên, skipPreflight...) * @returns Thông tin giao dịch */ async updateMetadataOptimized(connection, wallet, fieldName, fieldValue, options = {}) { const { priorityLevel = 'medium', skipPreflight = true, allocateStorage = true } = options; // Tạo transaction const transaction = new web3_js_1.Transaction(); // 1. Thêm instruction phí ưu tiên transaction.add(TokenMetadataToken.createPriorityFeeInstruction(priorityLevel)); // 2. Thêm instruction cấp phát không gian nếu cần if (allocateStorage) { const storageIx = await this.calculateAndAllocateSpaceForField(connection, this.mint, fieldName, fieldValue, wallet.publicKey); if (storageIx) { transaction.add(storageIx); } } // 3. Thêm instruction cập nhật metadata const updateIx = this.createUpdateMetadataFieldInstruction(wallet.publicKey, fieldName, fieldValue); transaction.add(updateIx); // Thiết lập giao dịch transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; transaction.feePayer = wallet.publicKey; // Ký và gửi giao dịch const signedTx = await wallet.signTransaction(transaction); const signature = await connection.sendRawTransaction(signedTx.serialize(), { skipPreflight }); // Xác nhận giao dịch await connection.confirmTransaction(signature, 'confirmed'); return { signature }; } /** * Cập nhật nhiều trường metadata với tối ưu hóa * @param connection Kết nối Solana * @param wallet Đối tượng wallet-adapter * @param fields Object chứa các cặp key-value cần cập nhật * @param options Tùy chọn cấu hình * @returns Mảng chữ ký giao dịch */ async updateMetadataBatchOptimized(connection, wallet, fields, options = {}) { const { maxFieldsPerTransaction = 2, priorityLevel = 'medium', skipPreflight = true, allocateStorage = true } = options; const fieldEntries = Object.entries(fields); const signatures = []; // Chia thành các giao dịch nhỏ hơn for (let i = 0; i < fieldEntries.length; i += maxFieldsPerTransaction) { const batch = fieldEntries.slice(i, i + maxFieldsPerTransaction); const batchFields = Object.fromEntries(batch); const transaction = new web3_js_1.Transaction(); // Thêm instruction phí ưu tiên transaction.add(TokenMetadataToken.createPriorityFeeInstruction(priorityLevel)); // Thêm instruction cấp phát không gian nếu cần - PHƯƠNG PHÁP 1 & 2 if (allocateStorage) { // Sử dụng phương pháp tối ưu tính toán không gian cho tất cả trường trong batch const storageIx = await this.calculateAndAllocateSpaceForMultipleFields(connection, this.mint, batchFields, wallet.publicKey); if (storageIx) { transaction.add(storageIx); } } // Thêm instruction cập nhật cho mỗi trường for (const [field, value] of batch) { transaction.add(this.createUpdateMetadataFieldInstruction(wallet.publicKey, field, value)); } // Thiết lập giao dịch transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; transaction.feePayer = wallet.publicKey; // Ký và gửi giao dịch const signedTx = await wallet.signTransaction(transaction); const signature = await connection.sendRawTransaction(signedTx.serialize(), { skipPreflight }); await connection.confirmTransaction(signature, 'confirmed'); signatures.push(signature); } return { signatures }; } } exports.TokenMetadataToken = TokenMetadataToken;