diff --git a/packages/cli/src/commands/mint.ts b/packages/cli/src/commands/mint.ts new file mode 100644 index 0000000..4712d1c --- /dev/null +++ b/packages/cli/src/commands/mint.ts @@ -0,0 +1,119 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { + createMintToTransaction, + getMintInfo, + decimalAmountToRaw, +} from '@mosaic/sdk'; +import { createSolanaClient } from '../utils/rpc.js'; +import { loadKeypair } from '../utils/solana.js'; +import { signTransactionMessageWithSigners, type Address } from 'gill'; + +interface MintOptions { + mintAddress: string; + recipient: string; + amount: string; + rpcUrl?: string; + keypair?: string; +} + +export const mintCommand = new Command('mint') + .description('Mint tokens to a recipient address') + .requiredOption( + '-m, --mint-address ', + 'The mint address of the token' + ) + .requiredOption( + '-r, --recipient ', + 'The recipient wallet address (ATA owner)' + ) + .requiredOption( + '-a, --amount ', + 'The decimal amount to mint (e.g., 1.5)' + ) + .action(async (options: MintOptions, command) => { + const spinner = ora('Minting tokens...').start(); + + try { + // Get global options from parent command + const parentOpts = command.parent?.opts() || {}; + const rpcUrl = options.rpcUrl || parentOpts.rpcUrl; + const keypairPath = options.keypair || parentOpts.keypair; + + // Create Solana client + const { rpc, sendAndConfirmTransaction } = createSolanaClient(rpcUrl); + + // Load mint authority keypair (assuming it's the configured keypair) + const mintAuthorityKeypair = await loadKeypair(keypairPath); + + spinner.text = 'Getting mint information...'; + + // Get mint info to determine decimals + const mintInfo = await getMintInfo(rpc, options.mintAddress as Address); + const decimals = mintInfo.decimals; + + // Parse and validate amount + const decimalAmount = parseFloat(options.amount); + if (isNaN(decimalAmount) || decimalAmount <= 0) { + throw new Error('Amount must be a positive number'); + } + + // Convert decimal amount to raw amount + const rawAmount = decimalAmountToRaw(decimalAmount, decimals); + + spinner.text = 'Building mint transaction...'; + + // Create mint transaction + const transaction = await createMintToTransaction( + rpc, + options.mintAddress as Address, + options.recipient as Address, + rawAmount, + mintAuthorityKeypair, + mintAuthorityKeypair // Use same keypair as fee payer + ); + + spinner.text = 'Signing transaction...'; + + // Sign the transaction + const signedTransaction = + await signTransactionMessageWithSigners(transaction); + + spinner.text = 'Sending transaction...'; + + // Send and confirm transaction + const signature = await sendAndConfirmTransaction(signedTransaction); + + spinner.succeed('Tokens minted successfully!'); + + // Display results + console.log(chalk.green('āœ… Mint Transaction Successful')); + console.log(chalk.cyan('šŸ“‹ Details:')); + console.log(` ${chalk.bold('Mint Address:')} ${options.mintAddress}`); + console.log(` ${chalk.bold('Recipient:')} ${options.recipient}`); + console.log( + ` ${chalk.bold('Amount:')} ${decimalAmount} (${rawAmount.toString()} raw units)` + ); + console.log(` ${chalk.bold('Decimals:')} ${decimals}`); + console.log(` ${chalk.bold('Transaction:')} ${signature}`); + console.log( + ` ${chalk.bold('Mint Authority:')} ${mintAuthorityKeypair.address}` + ); + + console.log(chalk.cyan('\\nšŸŽÆ Result:')); + console.log( + ` ${chalk.green('āœ“')} Associated Token Account created/updated` + ); + console.log( + ` ${chalk.green('āœ“')} ${decimalAmount} tokens minted to recipient` + ); + } catch (error) { + spinner.fail('Failed to mint tokens'); + console.error( + chalk.red('\\nāŒ Error:'), + error instanceof Error ? error : 'Unknown error' + ); + process.exit(1); + } + }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 60825dc..e90e6d5 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,6 +3,7 @@ import { Command } from 'commander'; import { createStablecoinCommand } from './commands/create/stablecoin.js'; import { createArcadeTokenCommand } from './commands/create/arcade-token.js'; +import { mintCommand } from './commands/mint.js'; const program = new Command(); @@ -20,6 +21,9 @@ const createCommand = program createCommand.addCommand(createStablecoinCommand); createCommand.addCommand(createArcadeTokenCommand); +// Add token management commands +program.addCommand(mintCommand); + // Global options program .option('--rpc-url ', 'Solana RPC URL', 'https://api.devnet.solana.com') diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 26ab0c6..8fb895f 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,2 +1,4 @@ export { Token } from './issuance'; export * from './templates'; +export * from './management/mint'; +export * from './transactionUtil'; diff --git a/packages/sdk/src/management/mint.ts b/packages/sdk/src/management/mint.ts new file mode 100644 index 0000000..1742289 --- /dev/null +++ b/packages/sdk/src/management/mint.ts @@ -0,0 +1,100 @@ +import type { + Address, + Rpc, + SolanaRpcApi, + FullTransaction, + TransactionMessageWithFeePayer, + TransactionVersion, + TransactionSigner, + TransactionWithBlockhashLifetime, +} from 'gill'; +import { createNoopSigner, createTransaction } from 'gill'; +import { + getAssociatedTokenAccountAddress, + TOKEN_2022_PROGRAM_ADDRESS, + getMintTokensInstructions, +} from 'gill/programs/token'; + +/** + * Creates a transaction to mint tokens to a recipient's associated token account. + * Will create the ATA if it doesn't exist. + * + * @param rpc - The Solana RPC client instance + * @param mint - The mint address + * @param recipient - The recipient's wallet address (owner of the ATA) + * @param amount - The raw token amount (already adjusted for decimals) + * @param mintAuthority - The mint authority signer + * @param feePayer - The fee payer signer + * @returns A promise that resolves to a FullTransaction object for minting tokens + */ +export const createMintToTransaction = async ( + rpc: Rpc, + mint: Address, + recipient: Address, + amount: bigint, + mintAuthority: Address | TransactionSigner, + feePayer: Address | TransactionSigner +): Promise< + FullTransaction< + TransactionVersion, + TransactionMessageWithFeePayer, + TransactionWithBlockhashLifetime + > +> => { + const feePayerSigner = + typeof feePayer === 'string' ? createNoopSigner(feePayer) : feePayer; + const mintAuthoritySigner = + typeof mintAuthority === 'string' + ? createNoopSigner(mintAuthority) + : mintAuthority; + const instructions = getMintTokensInstructions({ + mint, + destination: recipient, + amount, + mintAuthority: mintAuthoritySigner, + feePayer: feePayerSigner, + ata: await getAssociatedTokenAccountAddress( + mint, + recipient, + TOKEN_2022_PROGRAM_ADDRESS + ), + tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + }); + + // Get latest blockhash for transaction + const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); + + return createTransaction({ + feePayer: feePayerSigner, + version: 'legacy', + latestBlockhash, + instructions, + }); +}; + +/** + * Gets mint information including decimals + * + * @param rpc - The Solana RPC client instance + * @param mint - The mint address + * @returns Promise with mint information including decimals + */ +export async function getMintInfo(rpc: Rpc, mint: Address) { + const accountInfo = await rpc + .getAccountInfo(mint, { encoding: 'base64' }) + .send(); + + if (!accountInfo.value) { + throw new Error(`Mint account ${mint} not found`); + } + + // Parse mint data to get decimals (simplified parsing) + // In Token-2022, decimals are at offset 44 in the mint account data + const data = Buffer.from(accountInfo.value.data[0], 'base64'); + const decimals = data[44]; + + return { + decimals, + data: accountInfo.value, + }; +} diff --git a/packages/sdk/src/transactionUtil.ts b/packages/sdk/src/transactionUtil.ts index 08814e7..1f56a6c 100644 --- a/packages/sdk/src/transactionUtil.ts +++ b/packages/sdk/src/transactionUtil.ts @@ -43,3 +43,28 @@ export const transactionToB64 = ( const compiledTransaction = compileTransaction(transaction); return getBase64Decoder().decode(compiledTransaction.messageBytes); }; + +/** + * Converts a decimal amount to raw token amount based on mint decimals + * + * @param decimalAmount - The decimal amount (e.g., 1.5) + * @param decimals - The number of decimals the token has + * @returns The raw token amount as bigint + */ +export function decimalAmountToRaw( + decimalAmount: number, + decimals: number +): bigint { + if (decimals < 0 || decimals > 9) { + throw new Error('Decimals must be between 0 and 9'); + } + + const multiplier = Math.pow(10, decimals); + const rawAmount = Math.floor(decimalAmount * multiplier); + + if (rawAmount < 0) { + throw new Error('Amount must be positive'); + } + + return BigInt(rawAmount); +}