Viewing File: /home/ubuntu/efiexchange-node-base/src/controllers/userApp/solana.controller.ts

import {
    SUPPORTED_RPC_URLS,
    CHAIN_INFO,
    SupportedTokens,
    SOL_SPL_CONTRACT_ADDRESS
} from "../../config/chains";
require('dotenv').config();
import jwt from "jsonwebtoken";
import * as bip39 from "bip39";
import * as ed25519HdKey from "ed25519-hd-key";
const {
    Connection,
    clusterApiUrl,
    Keypair,
    LAMPORTS_PER_SOL,
    Transaction,
    SystemProgram,
    sendAndConfirmTransaction,
    PublicKey,
} = require('@solana/web3.js');
const { getOrCreateAssociatedTokenAccount, transfer } = require('@solana/spl-token');
import { decrypt } from '../../utils/decrypt';
import speakeasy from 'speakeasy'
import bs58 from 'bs58'

/**
 * @method transferSolanaTokensHandler
 *
 * @description transfer Solana Tokens
 *
 * @param req
 *
 * @param res
 *
 * @returns json response
 */

export async function transferSolanaTokensHandler(req: any, res: any) {
    try {

        let inputs = req.body;
        // console.log("inputs", inputs)

        if (!inputs.receiver_wallet_address) {
            return res.sendError("Receiver Wallet Address is required");
        }
        if (!inputs.tokens) {
            return res.sendError("Amount is required");
        }
        if (!inputs.token_type) {
            return res.sendError("Token Type is required");
        }
        if (!inputs.validate_token) {
            return res.sendError("Validation Token field is required");
        }
        if (!inputs.order_id) {
            return res.sendError("Order ID field is required");
        }

        const decoded = jwt.verify(inputs.validate_token, process.env.JWT_SECRET);

        if (decoded.sub != inputs.order_id) {
            return res.sendError("Transaction validation failed");
        }

        let signature;

        // Create a connection to the Solana devnet
        const network = process.env.NODE_ENV === "production" ? "mainnet-beta" : "devnet";
        const connection = new Connection(clusterApiUrl(network));

        // const connection = new Connection(SUPPORTED_RPC_URLS[SupportedTokens["SOL"]]);

        const base58SecretKey = process.env.ADMIN_SOLANA_PRIVATE_KEY;
        const senderSecretKey = bs58.decode(base58SecretKey);

        const fromWallet = Keypair.fromSecretKey(senderSecretKey);

        if (inputs.token_type == "SOL") {

            // Replace with the recipient's public key
            const recipientPublicKey = inputs.receiver_wallet_address;

            // Amount to send in SOL (e.g., 0.1 SOL)
            const amount = inputs.tokens * LAMPORTS_PER_SOL;

            console.log('amount:', amount);

            // Create a transaction instruction for the transfer
            const transaction = new Transaction().add(
                SystemProgram.transfer({
                    fromPubkey: fromWallet.publicKey,
                    toPubkey: recipientPublicKey,
                    lamports: amount,
                })
            );

            // Sign and send the transaction
            signature = await sendAndConfirmTransaction(
                connection,
                transaction,
                [fromWallet]
            );

        } else {

            const toPublicKey = new PublicKey(inputs.receiver_wallet_address);

            // Replace with the mint address of the SPL token you want to transfer (e.g., USDC)
            const mintAddress = new PublicKey(SOL_SPL_CONTRACT_ADDRESS[inputs.token_type]);

            // Create or retrieve the associated token account for the sender
            const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
                connection,
                fromWallet,
                mintAddress,
                fromWallet.publicKey
            );

            // Create or retrieve the associated token account for the recipient
            const toTokenAccount = await getOrCreateAssociatedTokenAccount(
                connection,
                fromWallet,
                mintAddress,
                toPublicKey
            );

            // Specify the number of tokens to transfer (considering token decimals)
            const amount = inputs.tokens * Math.pow(10, 8); // e.g., transferring 100 tokens for a token with 6 decimals

            console.log('amount:', amount);

            // Transfer the tokens
            signature = await transfer(
                connection,
                fromWallet,
                fromTokenAccount.address,
                toTokenAccount.address,
                fromWallet.publicKey,
                amount
            );
        }

        const transactionDetails = await connection.getTransaction(signature, {
            commitment: 'confirmed',
            maxSupportedTransactionVersion: 0,
        });

        inputs = {};

        let responseData = {
            signature: signature,
            transferTokenResponse: transactionDetails,
            fromWalletAddress: process.env.ADMIN_SOLANA_WALLET_ADDRESS,
        };

        return res.sendResponse(responseData, "Token transfer success");

    } catch (error) {
        return res.sendError(error.message);
    }
}

/**
 * @method getSolanaTransactionDetails
 *
 * @description getSolanaTransactionDetails
 *
 * @param req
 *
 * @param res
 *
 * @returns json response
 */

export async function getSolanaTransactionDetails(req: any, res: any) {
    try {

        let inputs = req.body;
        if (!inputs.transaction_hash) {
            return res.sendError("Transaction Hash is required");
        }
        if (!inputs.token_type) {
            return res.sendError("Token Type is required");
        }

        let transaction_fee; let value; let token_value; let receiver_wallet_address; let from_wallet_address;

        const network = process.env.NODE_ENV === "production" ? "mainnet-beta" : "devnet";

        let connection;

        if(network == "mainnet-beta") {
            connection = new Connection(clusterApiUrl(network));
        } else {
            connection = new Connection(SUPPORTED_RPC_URLS[SupportedTokens["SOL"]]);
        }

        let explorer_url = CHAIN_INFO[SupportedTokens["SOL"]].explorer + 'tx/' + inputs.transaction_hash + (process.env.NODE_ENV == "production" ? "" : "?cluster=devnet");

        const transactionDetails = await connection.getTransaction(inputs.transaction_hash, {
            commitment: 'confirmed',
            maxSupportedTransactionVersion: 0,
        });

        // Fetch the confirmed transaction
        const transaction = await connection.getParsedTransaction(inputs.transaction_hash, {
            maxSupportedTransactionVersion: 0,
        });

        if (!transaction) {
            console.log('Transaction not found');
            return res.sendError('Transaction not found');
        }

        if (inputs.token_type == "SOL") {

            // Parse the transaction details
            const transferInstructions = transaction.transaction.message.instructions.filter(
                (instruction:any) =>
                    instruction.program === 'system' &&
                    instruction.parsed &&
                    instruction.parsed.type === 'transfer'
            );

            if (transferInstructions.length === 0) {
                console.log('No transfer instruction found in the transaction.');
                return res.sendError('No transfer instruction found in the transaction.');
            }

            // Extract the transfer amount
            const transferAmount = transferInstructions[0].parsed.info.lamports;

            // Convert from lamports to SOL (1 SOL = 1,000,000,000 lamports)
            token_value = transferAmount / 1_000_000_000;

            console.log(`Transfer Amount: ${token_value} SOL`);

            from_wallet_address = transferInstructions[0].parsed.info.source;

            receiver_wallet_address = transferInstructions[0].parsed.info.destination;

        } else {

            // Parse the transaction details
            const transferInstructions = transaction.transaction.message.instructions.filter(
                (instruction:any) =>
                    instruction.parsed &&
                    ['transfer', 'transferChecked'].includes(instruction.parsed.type)
            );

            if (transferInstructions.length === 0) {
                console.log('No transfer instruction found in the transaction.');
                return res.sendError('No transfer instruction found in the transaction.');
            }

            from_wallet_address = transaction.meta.postTokenBalances[0].owner;
            
            receiver_wallet_address = transaction.meta.postTokenBalances[1].owner;

            transferInstructions.forEach((transfer:any) => {
                const { info } = transfer.parsed;
                const source = info.source;
                const destination = info.destination;
                const amount = info.tokenAmount ? info.tokenAmount.amount : info.amount; // The transferred amount in raw token units (may need conversion based on token decimals)

                console.log(`Token transfer found:
                - Source: ${source}
                - Destination: ${destination}
                - Amount: ${amount}`);

                token_value = amount / Math.pow(10, 6);
            });

        }

        const transactionFeeInLamports = transactionDetails.meta.fee;

        // Convert from lamports to SOL (1 SOL = 1,000,000,000 lamports)
        transaction_fee = transactionFeeInLamports / 1_000_000_000;

        inputs = {};

        let responseData = {
            transactionReponse: transactionDetails,
            transReceipt: transaction,
            value: token_value,
            transaction_fee: transaction_fee,
            explorer_url: explorer_url,
            token_value: token_value,
            from_wallet_address: from_wallet_address,
            receiver_wallet_address: receiver_wallet_address,
        };

        return res.sendResponse(responseData, "Token transfer success");

    } catch (error) {
        return res.sendError(error.message);
    }
}

export async function generateSolanaWalletAddress(req: any, res: any) {
    try {
      let inputs = req.body;
  
      if (!inputs.user_id) {
        return res.sendError("User Id is required");
      }
  
      let user_id = parseInt(inputs.user_id); // Ensure it's a number
      if (isNaN(user_id)) {
        return res.sendError("Invalid user_id");
      }
  
      const masterMnemonic = process.env.MASTER_MNEMONIC;
  
      if (!masterMnemonic) {
        return res.sendError("Master Mnemonic not found in environment variables.");
      }
  
      // Generate seed from mnemonic
      const seed = bip39.mnemonicToSeedSync(masterMnemonic);
  
      // Solana HD Wallet Path: m/44'/501'/{user_id}'/0'
      const derivationPath = `m/44'/501'/${user_id}'/0'`;
  
      // Derive the private key using ed25519-hd-key
      const derivedSeed = ed25519HdKey.derivePath(derivationPath, seed.toString("hex")).key;
  
      // Generate Keypair from derived seed
      const keypair = Keypair.fromSeed(derivedSeed);
  
      let responseData = {
        wallet_address: keypair.publicKey.toBase58(),
        // privateKey: bs58.encode(keypair.secretKey), // Encoded in Base58
      };
  
      return res.sendResponse(responseData, "Solana wallet address generated successfully");
  
    } catch (error) {
      return res.sendError(`Error: ${error.message}`);
    }
  }

export async function generateSolanaHDWalletAddress(req: any, res: any) {
try {
    let inputs = req.body;

    if (!inputs.user_id) {
        return res.sendError("User Id is required");
    }

    if (!inputs.account_number) {
        return res.sendError("Account Number is required");
    }
  
    let { user_id, account_number } = inputs;

    const masterMnemonic = process.env.MASTER_MNEMONIC;

    if (!masterMnemonic) {
    return res.sendError("Master Mnemonic not found in environment variables.");
    }

    // Generate seed from mnemonic
    const seed = bip39.mnemonicToSeedSync(masterMnemonic);

    // Solana HD Wallet Path: m/44'/501'/{user_id}'/0'
    const derivationPath = `m/44'/501'/${account_number}'/0'/${user_id}'`;

    // Derive the private key using ed25519-hd-key
    const derivedSeed = ed25519HdKey.derivePath(derivationPath, seed.toString("hex")).key;

    // Generate Keypair from derived seed
    const keypair = Keypair.fromSeed(derivedSeed);

    let responseData = {
    wallet_address: keypair.publicKey.toBase58(),
    // privateKey: bs58.encode(keypair.secretKey), // Encoded in Base58
    };

    return res.sendResponse(responseData, "Solana wallet address generated successfully");

} catch (error) {
    return res.sendError(`Error: ${error.message}`);
}
}

export async function fetchSolanaChildWallets(req: any, res: any) {
    try {
        let inputs = req.body;

        if (!inputs.users) {
        return res.sendError("Users list is required");
        }
        if (!inputs.network_type) {
        return res.sendError("Network Type is required");
        }
        if (inputs.password) {
            const decryptedPassword = await decrypt(process.env.ADMIN_PASSWORD_ENCRYPTED!, process.env.ADMIN_PASSWORD_KEY!, process.env.ADMIN_PASSWORD_IV!);
            if (inputs.password !== decryptedPassword) {
                return res.sendError("Invalid Password");
            }
        } else {
            return res.sendError("Password is required");
        }
        if (inputs.verification_code) {
            const decryptedGoogleKey = await decrypt(process.env.GOOGLE_SECRET_ENCRYPTED!, process.env.GOOGLE_SECRET_KEY!, process.env.GOOGLE_SECRET_IV!);

            const is2faValid = speakeasy.totp.verify({
                secret: decryptedGoogleKey,
                encoding: 'base32',
                token: req.body.verification_code,
                window: 1
            });
            if (!is2faValid) {
                return res.sendError("Invalid verification code");
            }
        } else {
            return res.sendError("Verification Code is required");
        }

        const network = process.env.NODE_ENV === "production" ? "mainnet-beta" : "devnet";
        const connection = new Connection(clusterApiUrl(network));

        const masterMnemonic = process.env.MASTER_MNEMONIC;
        if (!masterMnemonic) {
        return res.sendError("Master Mnemonic not found in environment variables.");
        }

        // Convert mnemonic to seed
        const seed = bip39.mnemonicToSeedSync(masterMnemonic);

        const users = inputs.users.split(",").map((num: any) => num.trim());
        let userWallets = [];

        for (let user of users) {
        try {
            let user_id = parseInt(user);
            if (isNaN(user_id)) {
            console.error(`Skipping invalid user_id: ${user}`);
            continue;
            }

            // Derivation path for Solana wallets: m/44'/501'/{user_id}'/0'
            const derivationPath = `m/44'/501'/${user_id}'/0'`;
            const derivedSeed = ed25519HdKey.derivePath(derivationPath, seed.toString("hex")).key;

            // Generate Keypair
            const keypair = Keypair.fromSeed(derivedSeed);
            const walletAddress = keypair.publicKey.toBase58();

            // Fetch wallet balance
            const balanceLamports = await connection.getBalance(new PublicKey(walletAddress));
            const balanceSOL = balanceLamports / 1e9; // Convert lamports to SOL

            userWallets.push({
            user_id: user_id,
            wallet_address: walletAddress,
            // private_key: bs58.encode(keypair.secretKey),
            balance: balanceSOL,
            symbol: "SOL",
            });
        } catch (error) {
            console.error(`Error processing user ${user}:`, error);
        }
    }
        return res.sendResponse(userWallets, "Solana HD Wallets details fetched successfully.");
    } catch (error) {
    return res.sendError(`Error: ${error.message}`);
}
}

export async function fetchSolanaChildWalletsRange(req: any, res: any) {
    try {
        let inputs = req.body;

        if (!inputs.skip) {
            return res.sendError("Skip is required");
        }
        if (!inputs.take) {
            return res.sendError("Take is required");
        }
        if (!inputs.network_type) {
        return res.sendError("Network Type is required");
        }
        if (inputs.password) {
            const decryptedPassword = await decrypt(process.env.ADMIN_PASSWORD_ENCRYPTED!, process.env.ADMIN_PASSWORD_KEY!, process.env.ADMIN_PASSWORD_IV!);
            if (inputs.password !== decryptedPassword) {
              return res.sendError("Invalid Password");
            }
          } else {
            return res.sendError("Password is required");
          }
          if (inputs.verification_code) {
            const decryptedGoogleKey = await decrypt(process.env.GOOGLE_SECRET_ENCRYPTED!, process.env.GOOGLE_SECRET_KEY!, process.env.GOOGLE_SECRET_IV!);
      
            const is2faValid = speakeasy.totp.verify({
              secret: decryptedGoogleKey,
              encoding: 'base32',
              token: req.body.verification_code,
              window: 1
            });
            if (!is2faValid) {
              return res.sendError("Invalid verification code");
            }
          } else {
            return res.sendError("Verification Code is required");
          }

        const network = process.env.NODE_ENV === "production" ? "mainnet-beta" : "devnet";
        const connection = new Connection(clusterApiUrl(network));

        const masterMnemonic = process.env.MASTER_MNEMONIC;
        if (!masterMnemonic) {
        return res.sendError("Master Mnemonic not found in environment variables.");
        }

        // Convert mnemonic to seed
        const seed = bip39.mnemonicToSeedSync(masterMnemonic);

        const rangeArray = (skip:any, take:any) => Array.from({ length: Number(take) }, (_, i) => Number(skip) + i + 1);
        const users = rangeArray(inputs.skip, inputs.take);            
        let userWallets = [];

        for (let user of users) {
        try {

            // Derivation path for Solana wallets: m/44'/501'/{user_id}'/0'
            const derivationPath = `m/44'/501'/${user}'/0'`;
            const derivedSeed = ed25519HdKey.derivePath(derivationPath, seed.toString("hex")).key;

            // Generate Keypair
            const keypair = Keypair.fromSeed(derivedSeed);
            const walletAddress = keypair.publicKey.toBase58();

            // Fetch wallet balance
            // const balanceLamports = await connection.getBalance(new PublicKey(walletAddress));
            // const balanceSOL = balanceLamports / 1e9; // Convert lamports to SOL

            userWallets.push({
            user_id: user,
            wallet_address: walletAddress,
            // private_key: bs58.encode(keypair.secretKey),
            // balance: balanceSOL,
            symbol: "SOL",
            });
        } catch (error) {
            console.error(`Error processing user ${user}:`, error);
        }
    }
        return res.sendResponse(userWallets, "Solana HD Wallets details fetched successfully.");
    } catch (error) {
    return res.sendError(`Error: ${error.message}`);
}
}
Back to Directory File Manager