diff --git a/frontend/package.json b/frontend/package.json index 37b56f7d..f74ac385 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -73,7 +73,7 @@ "showdown": "^2.1.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "typescript": "^4.9.5", + "typescript": "^5.0.4", "viem": "2", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/solanaRPC.ts b/frontend/src/solanaRPC.ts index 4d881e63..4b33a9eb 100644 --- a/frontend/src/solanaRPC.ts +++ b/frontend/src/solanaRPC.ts @@ -1,8 +1,9 @@ import { TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, createTransferInstruction, + getAssociatedTokenAddress, getMint, - getOrCreateAssociatedTokenAccount, } from "@solana/spl-token"; import { Connection, @@ -328,14 +329,6 @@ export default class SolanaRpc { } }; - getPrivateKey = async (): Promise => { - const privateKey = await this.provider.request({ - method: "solanaPrivateKey", - }); - - return privateKey as string; - }; - async getTokenBalance( walletAddress: string, tokenAddress: string @@ -452,10 +445,30 @@ export default class SolanaRpc { recipientWallet: string, tokenMintAddress: string, amount: number - ) { + ): Promise { try { + // Input validation + if (!recipientWallet || !tokenMintAddress) { + throw new Error("Invalid recipient or token mint address"); + } + if (amount <= 0) { + throw new Error("Amount must be greater than 0"); + } + const solanaWallet = new SolanaWallet(this.provider); const accounts = await solanaWallet.requestAccounts(); + const senderPublicKey = new PublicKey(accounts[0]); + + // Validate addresses + try { + new PublicKey(recipientWallet); + new PublicKey(tokenMintAddress); + } catch (e) { + throw new Error("Invalid Solana address format"); + } + + const recipientPublicKey = new PublicKey(recipientWallet); + const tokenMintPublicKey = new PublicKey(tokenMintAddress); const connectionConfig = await solanaWallet.request< string[], @@ -466,61 +479,91 @@ export default class SolanaRpc { }); const connection = new Connection(connectionConfig.rpcTarget); - const senderPublicKey = new PublicKey(accounts[0]); - const recipientPublicKey = new PublicKey(recipientWallet); - const tokenMintPublicKey = new PublicKey(tokenMintAddress); - - // Fetch the mint information to get decimals + // Get token mint info to validate decimals const mintInfo = await getMint(connection, tokenMintPublicKey); - const decimals = mintInfo.decimals; + const adjustedAmount = amount * Math.pow(10, mintInfo.decimals); + + if (!Number.isInteger(adjustedAmount)) { + throw new Error(`Amount precision exceeds token decimals (${mintInfo.decimals})`); + } - // Convert human-readable amount to raw token units - const rawAmount = amount * Math.pow(10, decimals); - const privateKey = await this.getPrivateKey(); - const privateKeyArray = Uint8Array.from(Buffer.from(privateKey, "hex")); + // Get the associated token account addresses + const senderTokenAccountAddress = await getAssociatedTokenAddress( + tokenMintPublicKey, + senderPublicKey + ); + const recipientTokenAccountAddress = await getAssociatedTokenAddress( + tokenMintPublicKey, + recipientPublicKey + ); - if (privateKeyArray.length !== 64) { - console.log("privatekey", privateKey); - throw new Error("Invalid private key length"); + // Get account infos to check if they exist + const [senderTokenAccountInfo, recipientTokenAccountInfo] = + await connection.getMultipleAccountsInfo([ + senderTokenAccountAddress, + recipientTokenAccountAddress, + ]); + + // Check sender's token balance + if (senderTokenAccountInfo) { + const balance = await connection.getTokenAccountBalance(senderTokenAccountAddress); + const uiAmount = balance.value.uiAmount; + if (uiAmount === null || uiAmount < amount) { + throw new Error("Insufficient token balance or invalid balance state"); + } + } + + const recentBlockhash = await connection.getLatestBlockhash(); + const transaction = new Transaction({ + feePayer: senderPublicKey, + recentBlockhash: recentBlockhash.blockhash, + }); + + // If the sender's token account doesn't exist, throw error as they can't send tokens + if (!senderTokenAccountInfo) { + throw new Error("Sender token account does not exist"); + } + + // If the recipient's token account doesn't exist, add an instruction to create it + if (!recipientTokenAccountInfo) { + transaction.add( + createAssociatedTokenAccountInstruction( + senderPublicKey, // payer + recipientTokenAccountAddress, + recipientPublicKey, + tokenMintPublicKey + ) + ); } - const senderKeypair = Keypair.fromSecretKey(privateKeyArray); - - const [senderTokenAccount, recipientTokenAccount] = await Promise.all([ - getOrCreateAssociatedTokenAccount( - connection, - senderKeypair, - tokenMintPublicKey, - senderPublicKey - ), - getOrCreateAssociatedTokenAccount( - connection, - senderKeypair, - tokenMintPublicKey, - recipientPublicKey - ), - ]); - const transaction = new Transaction().add( + // Add transfer instruction + transaction.add( createTransferInstruction( - senderTokenAccount.address, - recipientTokenAccount.address, + senderTokenAccountAddress, + recipientTokenAccountAddress, senderPublicKey, - rawAmount, - [], - TOKEN_PROGRAM_ID + BigInt(adjustedAmount) ) ); - const signature = await sendAndConfirmTransaction( - connection, - transaction, - [senderKeypair] - ); + // Sign and send transaction using wallet provider + const { signature } = await solanaWallet.signAndSendTransaction(transaction); + + // Wait for confirmation + const confirmation = await connection.confirmTransaction({ + signature, + blockhash: recentBlockhash.blockhash, + lastValidBlockHeight: recentBlockhash.lastValidBlockHeight, + }); + + if (confirmation.value.err) { + throw new Error(`Transaction failed: ${confirmation.value.err}`); + } return signature; } catch (error: any) { - console.log(error); - throw new Error(`Failed to send token: ${error.message}`); + console.error("Token transfer failed:", error); + throw new Error(`Failed to send tokens: ${error.message}`); } } }