mintme-sdk
Version:
A simple and powerful SDK for creating and managing tokens on the Solana blockchain. MintMe SDK simplifies the process of minting SPL tokens with metadata, managing token authorities, and interacting with the Solana token ecosystem.
488 lines (433 loc) • 18.2 kB
JavaScript
/**
* Functions for creating tokens on Solana
* @module solana-token-utils
*/
const solanaWeb3 = require("@solana/web3.js")
const splToken = require("@solana/spl-token")
const anchor = require("@project-serum/anchor")
const fs = require("fs")
const constants = require("../constants")
const walletUtils = require("../utils/wallet")
const pdaUtils = require("../utils/pda")
const idlUtils = require("../utils/idl")
// const tokenAuthority = require("./authority")
const conversionUtils = require("../utils/conversion")
const validationUtils = require("../utils/validation")
// Variable for custom logger
let customLogger = console.log
/**
* Sets a custom logger function
* @param {Function} loggerFunction - Function that will receive log messages
*/
function setCustomLogger(loggerFunction) {
customLogger = loggerFunction || console.log
}
/**
* Creates a token on Solana
* @param {Object} options - Options for creating the token
* @param {solanaWeb3.Connection} options.connection - Solana connection
* @param {solanaWeb3.Keypair|Object} options.payer - Payer's keypair or compatible wallet
* @param {string} options.name - Token name
* @param {string} options.symbol - Token symbol
* @param {string} options.uniqueKey - Unique Key
* @param {number} options.decimals - Token decimals (default 9)
* @param {number|string|BN} options.initialSupply - Initial supply (default 1000000000)
* @param {string} options.uri - Metadata URI (default "https://example.com/metadata.json")
* @param {boolean} options.revokeMint - Revoke mint authority (default false)
* @param {boolean} options.revokeFreeze - Revoke freeze authority (default false)
* @param {string|PublicKey} options.partnerWallet - Partner wallet public key
* @param {number|string|BN} [options.partnerAmount=0] - Amount for partner wallet
* @param {string|solanaWeb3.PublicKey} options.programId - Program ID (optional)
* @param {string|Object} options.idl - Program IDL (URL, object, or path, optional)
* @param {Function} options.logger - Custom logger function (optional)
* @returns {Promise<Object>} - Result of token creation
*/
function createToken(options) {
// Use custom logger if provided in options for this call
const logger = options.logger || customLogger
// Validate required options
if (!options.connection) {
return Promise.reject(new Error("A Solana connection is required"))
}
if (!options.payer) {
return Promise.reject(new Error("A payer (wallet or keypair) is required"))
}
// Validate token parameters
const validation = validationUtils.validateTokenCreationParams({
name: options.name,
symbol: options.symbol,
decimals: options.decimals || 9,
initialSupply: options.initialSupply || 1,
uri: options.uri || "https://ipfs.mintme.dev/metadata.json",
partnerWallet: options.partnerWallet,
})
if (!validation.isValid) {
logger("Token validation failed:")
validation.errors.forEach((error) => logger(`- ${error}`))
logger(`Maximum supply allowed: ${validation.maxSupply}`)
return Promise.reject(new Error(`Token validation failed: ${validation.errors.join(", ")}`))
}
// Registrar el valor máximo de suministro para referencia
logger(`Maximum supply allowed: ${validation.maxSupply}`)
// Set default values
const decimals = options.decimals || 9
const initialSupply = options.initialSupply || 1
const uri = options.uri || "https://ipfs.mintme.dev/metadata.json"
const revokeMint = options.revokeMint || false
const revokeFreeze = options.revokeFreeze || false
// Validate uniqueKey
if (!options.uniqueKey) {
logger("Warning: No uniqueKey provided, using default 'mintme.dev'")
}
const uniqueKey = options.uniqueKey || "mintme.dev"
// Handle partner wallet correctly
let partnerWallet
try {
partnerWallet = options.partnerWallet
? typeof options.partnerWallet === "string"
? new solanaWeb3.PublicKey(options.partnerWallet)
: options.partnerWallet
: options.payer.publicKey
} catch (error) {
logger(`Error parsing partner wallet: ${error.message}`)
return Promise.reject(new Error(`Invalid partner wallet: ${error.message}`))
}
const partnerAmount = options.partnerAmount || 0
// Validate programId
let programId
try {
programId = options.programId
? typeof options.programId === "string"
? new solanaWeb3.PublicKey(options.programId)
: options.programId
: new solanaWeb3.PublicKey(constants.DEFAULT_PROGRAM_ID)
} catch (error) {
logger(`Error parsing program ID: ${error.message}`)
return Promise.reject(new Error(`Invalid program ID: ${error.message}`))
}
// Use the provided IDL or the default
const idlSource = options.idl || null
// Determine the wallet/payer type
let wallet
if (options.payer.constructor && options.payer.constructor.name === "Keypair") {
wallet = new anchor.Wallet(options.payer)
} else if (options.payer.publicKey && options.payer.signTransaction) {
// Anchor-compatible wallet (like Phantom)
wallet = options.payer
} else {
return Promise.reject(new Error("Unsupported wallet format"))
}
// Configure the Anchor provider
const provider = new anchor.AnchorProvider(options.connection, wallet, {
commitment: "confirmed",
})
anchor.setProvider(provider)
// Load the IDL and configure the program
return idlUtils
.loadIDL(idlSource)
.then((idl) => {
const program = new anchor.Program(idl, programId, provider)
// Convert initialSupply to BN if necessary
let initialSupplyBN = initialSupply
if (typeof initialSupply === "number" || typeof initialSupply === "string") {
try {
const supplyString = initialSupply.toString()
// Validate that the string represents a valid number
if (!/^\d+$/.test(supplyString)) {
throw new Error(`Invalid initial supply format: ${initialSupply}`)
}
// Check if the number is within safe BigInt range before converting to BN
const supplyBigInt = BigInt(supplyString)
const MAX_BN_VALUE = BigInt("18446744073709551615") // 2^64 - 1
if (supplyBigInt > MAX_BN_VALUE) {
throw new Error(`Initial supply ${supplyString} exceeds maximum allowed value ${MAX_BN_VALUE.toString()}`)
}
// Convert to BN only after validation
initialSupplyBN = new anchor.BN(supplyString)
// Registrar el valor para depuración
logger(`Initial Supply (raw): ${initialSupply}`)
logger(`Initial Supply (BN): ${initialSupplyBN.toString()}`)
} catch (error) {
logger(`Error converting initial supply: ${error.message}`)
return Promise.reject(new Error(`Invalid initial supply: ${error.message}`))
}
}
// Get current timestamp
const timestamp = Math.floor(Date.now() / 1000)
// Derive the necessary addresses
return Promise.all([
pdaUtils.derivePaymentPDA(programId),
pdaUtils.deriveMintPDA(programId, wallet.publicKey, options.name, options.symbol, options.uniqueKey),
pdaUtils.deriveNetworkFeeConfigPDA(programId),
])
.then((results) => {
const paymentPDA = results[0]
const mintPDA = results[1]
const configPDA = results[2]
return Promise.all([
paymentPDA,
mintPDA,
pdaUtils.deriveTokenAccount(mintPDA.mintPDA, wallet.publicKey),
pdaUtils.deriveMetadataAccount(mintPDA.mintPDA),
configPDA,
])
})
.then((results) => {
const paymentPDA = results[0]
const mintPDA = results[1]
const tokenAccount = results[2]
const metadataAccount = results[3]
const configPDA = results[4]
logger(`Payment PDA: ${paymentPDA.pda.toString()}`)
logger(`Mint PDA: ${mintPDA.mintPDA.toString()}`)
logger(`Token Account: ${tokenAccount.toString()}`)
logger(`Metadata Account: ${metadataAccount.metadataAddress.toString()}`)
logger(`Network Fee Config: ${configPDA.pda.toString()}`)
logger(`Partner: ${partnerWallet?.toString() || "Not defined"}`)
// Call the create_token instruction
return program.methods
.createToken(
options.name,
options.symbol,
uniqueKey,
decimals,
initialSupplyBN,
uri,
revokeMint,
revokeFreeze,
partnerWallet,
new anchor.BN(partnerAmount),
)
.accounts({
payer: wallet.publicKey,
config: configPDA.pda,
mint: mintPDA.mintPDA,
tokenAccount: tokenAccount,
metadata: metadataAccount.metadataAddress,
tokenPda: paymentPDA.pda, // Use tokenPda instead of paymentPda according to the new IDL
tokenProgram: splToken.TOKEN_PROGRAM_ID,
metadataProgram: constants.TOKEN_METADATA_PROGRAM_ID,
systemProgram: solanaWeb3.SystemProgram.programId,
partnerWallet: partnerWallet,
rent: solanaWeb3.SYSVAR_RENT_PUBKEY,
associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
})
.rpc()
.then((txSignature) => ({
success: true,
mint: mintPDA.mintPDA.toString(),
tokenAccount: tokenAccount.toString(),
metadata: metadataAccount.metadataAddress.toString(),
txSignature: txSignature,
tokenName: options.name,
tokenSymbol: options.symbol,
}))
})
})
.catch((error) => {
logger("Error creating token: " + error.message)
return {
success: false,
error: error.message,
details: error,
}
})
}
/**
* Creates a token with simplified configuration
* @param {Object} config - Simplified configuration
* @param {string} config.tokenName - Token name
* @param {string} config.tokenSymbol - Token symbol
* @param {string} config.uniqueKey - Unique Key
* @param {number} config.decimals - Token decimals (default 9)
* @param {number} config.initialSupply - Initial supply (default 1000000000)
* @param {string} config.uri - Metadata URI (default "https://example.com/metadata.json")
* @param {boolean} config.revokeMint - Revoke mint authority (default false)
* @param {boolean} config.revokeFreeze - Revoke freeze authority after creating the token (default false)
* @param {string|PublicKey} config.partnerWallet - Partner wallet receiving funds
* @param {number} [config.partnerAmount=0] - Amount for partner wallet
* @param {string} [config.walletPath] - Path to wallet.json file (default "./wallet.json")
* @param {Object} [config.wallet] - Web3 wallet object with publicKey and signTransaction methods
* @param {string|solanaWeb3.Connection} config.connection - Solana RPC endpoint or Connection object (default "https://api.devnet.solana.com")
* @param {string} config.cluster - Solana cluster (default "devnet")
* @param {Function} [config.logger] - Custom logger function
* @returns {Promise<Object>} - Result of token creation
*/
function createTokenSimple(config) {
// Default configuration
const defaultConfig = {
tokenName: "MINTME.DEV",
tokenSymbol: "MTM",
uniqueKey: "mintme.dev",
decimals: 9,
initialSupply: 1000000000,
uri: "https://ipfs.mintme.dev/metadata.json",
revokeMint: false,
revokeFreeze: false,
partnerWallet: "7viHj1u6aQS9Nmc55FokX3B9NbDJUPwMYQvKgBfWeYXE",
partnerAmount: 0,
walletPath: "./wallet.json",
connection: "https://api.devnet.solana.com",
cluster: "devnet",
}
// Merge the provided configuration with the default
const mergedConfig = Object.assign({}, defaultConfig, config || {})
// Use provided logger or default
const logger = mergedConfig.logger || customLogger
// Create Solana connection if a string was provided
const connection =
typeof mergedConfig.connection === "string"
? new solanaWeb3.Connection(mergedConfig.connection, "confirmed")
: mergedConfig.connection
// Determine the wallet to use (web3 wallet or file-based wallet)
let payer
const useWeb3Wallet = !!(mergedConfig.wallet && mergedConfig.wallet.publicKey)
if (useWeb3Wallet) {
// Use the provided web3 wallet
payer = mergedConfig.wallet
} else {
// Use file-based wallet
// Verify that the wallet file exists
if (!walletUtils.verifyWalletFile(mergedConfig.walletPath)) {
return Promise.reject(
new Error(`Wallet file not found at ${mergedConfig.walletPath}. Generate a wallet with generate-wallet.js`),
)
}
// Load wallet from file
payer = walletUtils.loadWalletFromFile(mergedConfig.walletPath)
if (!payer) {
return Promise.reject(new Error(`Error loading wallet from ${mergedConfig.walletPath}`))
}
}
let tokenResult
// Verify balance and create token
return connection
.getBalance(payer.publicKey)
.then((balance) => {
logger(`Address: ${payer.publicKey.toString()}`)
logger(`Balance: ${balance / 1000000000} SOL`)
if (balance < 10000000) {
if (mergedConfig.cluster === "devnet") {
throw new Error(
`Insufficient balance. You need at least 0.03 SOL. Get SOL with: solana airdrop 2 ${payer.publicKey.toString()} --url devnet`,
)
} else {
throw new Error(`Insufficient balance. You need at least 0.03 SOL.`)
}
}
// Registrar el valor para depuración
logger(`Initial Supply (from form): ${mergedConfig.initialSupply}`)
// Calcular el valor real considerando los decimales
const decimals = mergedConfig.decimals || 9
let adjustedSupply
try {
adjustedSupply = calculateAdjustedSupply(mergedConfig.initialSupply, decimals)
logger(`Adjusted Supply (with decimals): ${adjustedSupply}`)
} catch (error) {
logger(`Error calculating adjusted supply: ${error.message}`)
throw new Error(`Supply calculation failed: ${error.message}`)
}
// Create token
return createToken({
connection: connection,
payer: payer,
name: mergedConfig.tokenName,
symbol: mergedConfig.tokenSymbol,
uniqueKey: mergedConfig.uniqueKey,
decimals: mergedConfig.decimals,
initialSupply: adjustedSupply, // Usar el valor ajustado
uri: mergedConfig.uri,
revokeMint: mergedConfig.revokeMint,
revokeFreeze: mergedConfig.revokeFreeze,
partnerWallet: mergedConfig.partnerWallet,
partnerAmount: conversionUtils.solToLamports(mergedConfig.partnerAmount),
logger: logger, // Send Logger
})
})
.then((result) => {
tokenResult = result
if (result.success && mergedConfig.revokeMint) {
logger("Revoked Mint authority...")
}
if (result.success && mergedConfig.revokeFreeze) {
logger("Revoked Freeze authority...")
}
return null
})
.then((revokeResult) => {
if (tokenResult.success) {
// Save token information to a file if we're in Node.js and not using a web3 wallet
if (fs && !useWeb3Wallet) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const tokenInfo = {
name: mergedConfig.tokenName,
symbol: mergedConfig.tokenSymbol,
mint: tokenResult.mint,
tokenAccount: tokenResult.tokenAccount,
metadata: tokenResult.metadata,
txSignature: tokenResult.txSignature,
createdAt: new Date().toISOString(),
freezeRevoked: revokeResult ? revokeResult.success : false,
freezeRevokeTx: revokeResult ? revokeResult.txSignature : null,
}
// Note: The original code doesn't actually save the file,
// just prepares the tokenInfo object
} catch (error) {
logger("Error saving token information: " + error.message)
}
}
// Add information about the freeze revocation to the result
if (revokeResult) {
tokenResult.freezeRevoked = revokeResult.success
tokenResult.freezeRevokeTx = revokeResult.txSignature
}
}
return tokenResult
})
.catch((error) => {
logger(`Error in createTokenSimple: ${error.message}`)
return {
success: false,
error: error.message,
details: error,
}
})
}
/**
* Calcula el suministro ajustado considerando los decimales
* @param {number|string} supply - Suministro inicial sin ajustar
* @param {number} decimals - Número de decimales del token
* @returns {string} - Suministro ajustado como string
*/
function calculateAdjustedSupply(supply, decimals) {
try {
// Convert supply to BigInt for safe arithmetic
const supplyBigInt = BigInt(supply)
// Calculate the factor using BigInt
const factorBigInt = BigInt(10) ** BigInt(decimals)
// Calculate the adjusted supply using BigInt arithmetic
const adjustedSupplyBigInt = supplyBigInt * factorBigInt
// Define the maximum allowed value for Solana tokens (2^64 - 1)
const MAX_SUPPLY = BigInt("18446744073709551615")
// Validate that the adjusted supply doesn't exceed the maximum
if (adjustedSupplyBigInt > MAX_SUPPLY) {
throw new Error(
`Adjusted supply (${adjustedSupplyBigInt.toString()}) exceeds maximum allowed value (${MAX_SUPPLY.toString()})`,
)
}
// Return as string to avoid precision issues
return adjustedSupplyBigInt.toString()
} catch (error) {
if (error.message.includes("Cannot convert")) {
throw new Error(`Invalid supply value: ${supply}. Must be a valid number.`)
}
throw error
}
}
module.exports = {
createToken,
createTokenSimple,
setCustomLogger,
calculateAdjustedSupply, // Exportar la nueva función
}