solana-token-extension-boost
Version:
SDK for Solana Token Extensions with wallet adapter support
477 lines (476 loc) • 25.1 kB
JavaScript
;
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;