Viewing File: /home/ubuntu/efiexchange-node-base/src/controllers/userApp/user.controller.ts
import Web3 from "web3";
import Token from "../../utils/abi.json";
import { BigNumber, ethers, providers, utils } from "ethers";
import {
SUPPORTED_RPC_URLS,
CHAIN_INFO,
SupportedChainId,
SupportedTokens,
SPECIAL_CONTRACT_ADDRESS,
} from "../../config/chains";
import { decrypt } from "@utils/decrypt";
import speakeasy from 'speakeasy';
require('dotenv').config();
// Helper: normalize input based on token decimals (max 8)
function normalizeAmount(value: string, tokenDecimals: number): string {
const allowedDecimals = tokenDecimals > 8 ? 8 : tokenDecimals;
if (value.includes(".")) {
const [intPart, decPart] = value.split(".");
const trimmedDecPart = decPart.slice(0, allowedDecimals);
return trimmedDecPart.length > 0 ? `${intPart}.${trimmedDecPart}` : intPart;
}
return value;
}
/**
* @method transferTokensHandler
*
* @description transfer Tokens
*
* @param req
*
* @param res
*
* @returns json response
*/
export async function transferTokensHandler(req: any, res: any) {
try {
let inputs = req.body;
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.network_type) {
return res.sendError("Network Type is required");
}
if (!inputs.order_id) {
return res.sendError("Order ID field is required");
}
const rpcUrl = SUPPORTED_RPC_URLS[SupportedTokens[inputs.network_type]];
if (!rpcUrl) {
return res.sendError("Invalid Network Type");
}
const privateKey = process.env.ADMIN_PRIVATE_KEY;
let provider, wallet, contract_address, transactionReponse;
let receiver_wallet_address = inputs.receiver_wallet_address;
let tokens = inputs.tokens;
let gasLimit: any = 21620;
let gasPrice: any = ethers.utils.parseUnits("40", "gwei");
const web3 = new Web3(rpcUrl);
// Native token transfer (ETH/BNB/Polygon)
if (["ETH", "POLYGON_AMOY", "BNB", "POL"].includes(inputs.token_type)) {
console.log(`Initiating ${inputs.token_type} transfer`);
if (!web3.utils.isAddress(receiver_wallet_address)) {
return res.sendError("Invalid receiver wallet address");
}
const senderBalance: any = await web3.eth.getBalance(process.env.ADMIN_WALLET_ADDRESS);
if (web3.utils.toBN(web3.utils.toWei(tokens, "ether")).gt(web3.utils.toBN(senderBalance))) {
return res.sendError("Insufficient balance in sender's wallet");
}
const createTransaction = await web3.eth.accounts.signTransaction(
{
from: process.env.ADMIN_WALLET_ADDRESS,
to: receiver_wallet_address,
value: web3.utils.toWei(tokens, "ether"),
gasPrice: gasPrice,
gasLimit: gasLimit,
},
privateKey
);
await web3.eth.sendSignedTransaction(createTransaction.rawTransaction, function (error, hash) {
if (!error) {
transactionReponse = hash;
} else {
return res.sendError(`Transaction failed: ${error.message}`);
}
});
} else {
// ERC-20 token transfer
const abi = [
"function balanceOf(address owner) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)",
];
contract_address = SPECIAL_CONTRACT_ADDRESS[inputs.network_type][inputs.token_type];
provider = new ethers.providers.JsonRpcProvider(rpcUrl);
wallet = new ethers.Wallet(privateKey, provider);
const ethContract = new ethers.Contract(contract_address, abi, wallet);
const tokenDecimals: number = await ethContract.decimals();
const normalizedValue = normalizeAmount(tokens, tokenDecimals);
const amountInBigNumber = ethers.utils.parseUnits(normalizedValue, tokenDecimals);
if (!ethers.utils.isAddress(receiver_wallet_address)) {
return res.sendError("Invalid receiver wallet address");
}
// Check sender token balance
const senderTokenBalance = await ethContract.balanceOf(process.env.ADMIN_WALLET_ADDRESS);
if (amountInBigNumber.gt(senderTokenBalance)) {
return res.sendError("Insufficient token balance in sender's wallet");
}
// Check native currency for gas
const senderNativeBalance = await provider.getBalance(process.env.ADMIN_WALLET_ADDRESS);
const transactionCost = ethers.BigNumber.from(100000).mul(ethers.utils.parseUnits("50", "gwei"));
if (ethers.BigNumber.from(senderNativeBalance).lt(transactionCost)) {
return res.sendError(
`Insufficient native currency balance for gas. Requires at least ${ethers.utils.formatEther(transactionCost)}`
);
}
// Proceed with token transfer
const transferTokenResponse = await ethContract.transfer(receiver_wallet_address, amountInBigNumber, {
gasLimit: 100000,
gasPrice: ethers.utils.parseUnits("50", "gwei"),
});
transactionReponse = transferTokenResponse.hash;
let transactionReceipt = await web3.eth.getTransactionReceipt(transferTokenResponse.hash);
while (transactionReceipt === null) {
transactionReceipt = await web3.eth.getTransactionReceipt(transferTokenResponse.hash);
}
}
if (!transactionReponse) {
return res.sendError("Transaction failed, please try again later.");
}
inputs = {};
const responseData = {
transferTokenResponse: transactionReponse,
fromWalletAddress: process.env.ADMIN_WALLET_ADDRESS,
};
return res.sendResponse(responseData, "Token transfer success");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
/**
* @method getTransactionDetails
*
* @description getTransactionDetails
*
* @param req
*
* @param res
*
* @returns json response
*/
export async function getTransactionDetails(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");
}
if (!inputs.network_type) {
return res.sendError("Network Type is required");
}
const rpcUrl = SUPPORTED_RPC_URLS[SupportedTokens[inputs.network_type]];
if (!rpcUrl) {
return res.sendError("Invalid Network Type");
}
const web3 = new Web3(rpcUrl);
const provider = new providers.JsonRpcProvider(rpcUrl);
let transactionResponse = await web3.eth.getTransaction(inputs.transaction_hash);
let transReceipt = await web3.eth.getTransactionReceipt(inputs.transaction_hash);
let transaction_fee, value, token_value, receiver_wallet_address, from_wallet_address;
if (transactionResponse && transReceipt) {
let gasPrice:any = transReceipt.effectiveGasPrice ? transReceipt.effectiveGasPrice : 0;
gasPrice = gasPrice > 0 ? Web3.utils.fromWei(transReceipt.effectiveGasPrice.toString(), "ether") : 0;
let gasUsed = transReceipt?.gasUsed || 0;
let gasUsedWei = transactionResponse?.value ? Web3.utils.fromWei(transactionResponse.value.toString(), "ether") : 0;
from_wallet_address = transactionResponse.from;
if (["ETH", "POLYGON_AMOY", "BNB", "POL"].includes(inputs.token_type)) {
// Native currency transfer
receiver_wallet_address = transactionResponse.to;
value = gasUsedWei;
} else {
// Token Transfer (ERC-20)
if (transReceipt.logs.length > 0) {
let contract_address = SPECIAL_CONTRACT_ADDRESS[inputs.network_type][inputs.token_type];
if(contract_address){
for (let log of transReceipt.logs) {
if (log.address.toLowerCase() === contract_address.toLowerCase()) {
if (log.topics.length >= 3) {
// Extract receiver from logs
receiver_wallet_address = `0x${log.topics[2].slice(26)}`;
}
let tokenHexString = log.data ?? "";
let tokenAmountInWei = web3.utils.hexToNumberString(tokenHexString);
let abi = [
"function balanceOf(address owner) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)",
];
let ethContract = new ethers.Contract(contract_address, abi, provider);
let decimals = await ethContract.decimals();
let tokenAmount = parseFloat(tokenAmountInWei) / Math.pow(10, decimals);
token_value = tokenAmount > 0 ? tokenAmount : token_value;
break; // We found the correct log, no need to continue looping
} else {
return res.sendError(`Invalid Token`);
}
}
} else {
return res.sendError(`Invalid Token`);
}
}
}
transaction_fee = gasPrice * gasUsed;
}
let explorer_url = CHAIN_INFO[SupportedTokens[inputs.network_type]].explorer + "tx/" + inputs.transaction_hash;
let responseData = {
transactionResponse,
transReceipt,
value: token_value || value,
transaction_fee,
explorer_url,
token_value: token_value || value,
from_wallet_address,
receiver_wallet_address,
};
return res.sendResponse(responseData, "Transfer Details Fetched");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
export async function generateWalletAddress(req: any, res: any) {
try {
let inputs = req.body;
if (!inputs.user_id) {
return res.sendError("User Id is required");
}
let user_id = inputs.user_id;
const masterMnemonic = process.env.MASTER_MNEMONIC;
const masterWallet = ethers.Wallet.fromMnemonic(masterMnemonic);
const privateKey = masterWallet.privateKey;
const publicKey = masterWallet.address;
const hdNode = ethers.utils.HDNode.fromMnemonic(masterMnemonic);
const childAccountPath = "m/44'/60'/0'/0";
const childNode = hdNode.derivePath(`${childAccountPath}/${user_id}`);
const childWallet = new ethers.Wallet(childNode.privateKey);
let responseData = {
wallet_address: childWallet.address,
// privateKey: childWallet.privateKey,
};
return res.sendResponse(responseData, "Wallet address generated successfully");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
export async function generateHDWalletAddress(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;
const masterWallet = ethers.Wallet.fromMnemonic(masterMnemonic);
const privateKey = masterWallet.privateKey;
const publicKey = masterWallet.address;
const hdNode = ethers.utils.HDNode.fromMnemonic(masterMnemonic);
const childAccountPath = `m/44'/60'/${account_number}'/0`;
const childNode = hdNode.derivePath(`${childAccountPath}/${user_id}`);
const childWallet = new ethers.Wallet(childNode.privateKey);
let responseData = {
wallet_address: childWallet.address,
// privateKey: childWallet.privateKey,
};
return res.sendResponse(responseData, "Wallet address generated successfully");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
/**
* @method transferTokensToAdmin
*
* @description Transfers tokens to the admin wallet using an HD wallet derived from a userId.
*
* @param req
*
* @param res
*
* @returns json response
*/
export async function transferTokensToAdmin(req: any, res: any) {
try {
let inputs = req.body;
if (!inputs.user_id) {
return res.sendError("Receiver Wallet Address is required");
}
if (!inputs.amount) {
return res.sendError("Amount is required");
}
if (!inputs.token_type) {
return res.sendError("Token Type is required");
}
if (!inputs.network_type) {
return res.sendError("Network Type is required");
}
const rpcUrl = SUPPORTED_RPC_URLS[SupportedTokens[inputs.network_type]];
if (!rpcUrl) {
return res.sendError("Invalid Token Type");
}
let masterMnemonic = process.env.MASTER_MNEMONIC;
const masterWallet = ethers.Wallet.fromMnemonic(masterMnemonic);
const receiver_wallet_address = masterWallet.address;
const receiver_private_key = masterWallet.privateKey;
let childNode = ethers.utils.HDNode.fromMnemonic(masterMnemonic).derivePath(`m/44'/60'/0'/0/${inputs.user_id}`);
let privateKey = childNode.privateKey;
let childAddress = childNode.address;
let provider, wallet, contract_address, transactionReponse, estimatedTransactionCost: any;
let gasLimit = 21000;
// let gasLimit = 100000;
let tokens = inputs.amount;
const web3 = new Web3(rpcUrl);
let gasPrice: any = ethers.utils.parseUnits(String(40), "gwei");
let gP = gasPrice.toString();
const gPAsNumber = parseFloat(gP);
estimatedTransactionCost = ((gPAsNumber / Math.pow(10, 18)) * gasLimit).toFixed(5).toString();
console.log("Estimated Transaction Cost", estimatedTransactionCost)
if (["ETH", "POLYGON_AMOY", "BNB", "POL"].includes(inputs.token_type)) {
console.log(`Initiating ${inputs.token_type} transfer`);
let maxTransferableTokens: any = (tokens - estimatedTransactionCost).toFixed(5).toString();
console.log("Max Transferable Amount", maxTransferableTokens)
if (maxTransferableTokens < 0) {
return res.sendError("Transaction fee overshot (fee > amount)");
}
// Check if sender has sufficient balance
const senderBalance:any = await web3.eth.getBalance(childAddress);
if (web3.utils.toWei(tokens, "ether") > senderBalance) {
return res.sendError("Insufficient balance in child wallet");
}
const createTransaction = await web3.eth.accounts.signTransaction(
{
from: childAddress,
to: receiver_wallet_address,
value: web3.utils.toWei(maxTransferableTokens, "ether"),
gasPrice: gasPrice,
gasLimit: gasLimit,
},
privateKey
);
await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction,
function (error, hash) {
if (!error) {
console.log("hash", hash);
transactionReponse = hash;
} else {
return res.sendError(`Transaction failed: ${error.message}`);
}
}
);
}
else {
let abi = [
"function balanceOf(address owner) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)",
];
let contract_address = SPECIAL_CONTRACT_ADDRESS[inputs.network_type][inputs.token_type];
provider = new providers.JsonRpcProvider(rpcUrl);
wallet = new ethers.Wallet(privateKey, provider);
let ethContract = await new ethers.Contract(contract_address, abi, wallet);
let decimals = await ethContract.decimals();
const amountInString = ethers.utils.parseUnits(tokens, decimals).toString();
// Check if receiver wallet address is valid
if (!utils.isAddress(receiver_wallet_address)) {
return res.sendError("Invalid receiver wallet address");
}
// Check sender's token balance
const senderTokenBalance = await ethContract.balanceOf(childAddress);
const tokenBalanceString = ethers.utils.formatUnits(senderTokenBalance, decimals)
if (parseFloat(tokens) > parseFloat(tokenBalanceString)) {
return res.sendError("Insufficient token balance in child's wallet");
}
// Check sender's native currency balance for gas
const senderNativeBalance = await provider.getBalance(childAddress);
const transactionCost = BigNumber.from(100000).mul(ethers.utils.parseUnits(String(50), "gwei"));
let maxTransactionCost = ethers.utils.formatUnits(transactionCost, 18);
if (BigNumber.from(senderNativeBalance).lt(transactionCost)) {
return res.sendError(
`Insufficient native currency balance for gas. Requires at least ${ethers.utils.formatEther(transactionCost)}`
);
}
// Proceed with token transfer
let transferTokenResponse = await ethContract.transfer(
receiver_wallet_address,
amountInString,
{ gasLimit: 100000, gasPrice: ethers.utils.parseUnits(String(50), "gwei") }
);
transactionReponse = transferTokenResponse.hash;
console.log("transferTokenResponse hash", transferTokenResponse.hash)
let transactionReceipt = await web3.eth.getTransactionReceipt(
transferTokenResponse.hash,
function (error, result) {
return result;
}
);
while (transactionReceipt === null) {
transactionReceipt = await web3.eth.getTransactionReceipt(
transferTokenResponse.hash,
function (error, result) {
return result;
}
);
}
}
if (!transactionReponse) {
return res.sendError("Transaction failed, please try again later.");
}
inputs = {};
let responseData = {
transferTokenResponse: transactionReponse,
toWalletAddress: receiver_wallet_address,
fromWalletAddress: childAddress,
};
return res.sendResponse(responseData, "Token transfer success");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
function convertToValidJson(str: string) {
const validJsonStr = str.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');
try {
return JSON.parse(validJsonStr);
} catch (error) {
console.error("Invalid JSON format:", error);
return null;
}
}
export async function fetchChildWallets(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 rpcUrl = SUPPORTED_RPC_URLS[SupportedTokens[inputs.network_type]];
if (!rpcUrl) {
return res.sendError("Invalid Token Type");
}
let masterMnemonic = process.env.MASTER_MNEMONIC;
const masterWallet = ethers.Wallet.fromMnemonic(masterMnemonic);
let provider = new providers.JsonRpcProvider(rpcUrl);
const web3 = new Web3(rpcUrl);
const users = inputs.users.split(",").map((num: any) => (num.trim()));
let userWallets = [];
for (let user of users) {
try {
const hdNode = ethers.utils.HDNode.fromMnemonic(masterMnemonic);
const childAccountPath = "m/44'/60'/0'/0";
const childNode = hdNode.derivePath(`${childAccountPath}/${user}`);
const childWallet = new ethers.Wallet(childNode.privateKey);
let childAddress = childWallet.address;
let childKey = childNode.privateKey;
const wallet = new ethers.Wallet(childKey, provider);
const balanceWei = await provider.getBalance(childAddress);
const senderBalance = await web3.eth.getBalance(childAddress);
const balance = ethers.utils.formatEther(senderBalance);
userWallets.push({
user_id: user.user_id,
wallet_address: childAddress,
// private_key: childKey,
balance: Number(balance),
symbol: inputs.network_type,
});
} catch (error) {
console.error(`Error processing user ${user.user_id}:`, error);
}
}
return res.sendResponse(userWallets, "HD Wallets details fetched successfully.");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
export async function fetchChildWalletsRange(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 rpcUrl = SUPPORTED_RPC_URLS[SupportedTokens[inputs.network_type]];
if (!rpcUrl) {
return res.sendError("Invalid Token Type");
}
let masterMnemonic = process.env.MASTER_MNEMONIC;
const masterWallet = ethers.Wallet.fromMnemonic(masterMnemonic);
let provider = new providers.JsonRpcProvider(rpcUrl);
const web3 = new Web3(rpcUrl);
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 = [];
const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)",
"function decimals() view returns (uint8)"
];
let usdt_contract_address = SPECIAL_CONTRACT_ADDRESS[inputs.network_type]["USDT"];
let usdc_contract_address = SPECIAL_CONTRACT_ADDRESS[inputs.network_type]["USDC"];
let usdt_contract = new ethers.Contract(usdt_contract_address, ERC20_ABI, provider);
let usdc_contract = new ethers.Contract(usdc_contract_address, ERC20_ABI, provider);
for (let user of users) {
try {
const hdNode = ethers.utils.HDNode.fromMnemonic(masterMnemonic);
const childAccountPath = "m/44'/60'/0'/0";
const childNode = hdNode.derivePath(`${childAccountPath}/${user}`);
const childWallet = new ethers.Wallet(childNode.privateKey);
let childAddress = childWallet.address;
let childKey = childNode.privateKey;
// const wallet = new ethers.Wallet(childKey, provider);
const balanceWei = await provider.getBalance(childAddress);
const senderBalance = await web3.eth.getBalance(childAddress);
const balance = ethers.utils.formatEther(senderBalance);
let usdt_balance = 0, usdc_balance = 0;
let TOKEN_DECIMALS = 6;
if(usdc_contract){
const rawBalance = await usdc_contract.balanceOf(childAddress);
usdc_balance = Number(ethers.utils.formatUnits(rawBalance, TOKEN_DECIMALS));
}
if(usdt_contract){
const rawBalance = await usdt_contract.balanceOf(childAddress);
usdt_balance = Number(ethers.utils.formatUnits(rawBalance, TOKEN_DECIMALS));
}
userWallets.push({
user_id: user,
wallet_address: childAddress,
// private_key: childKey,
balance: Number(balance),
usdt_balance: Number(usdt_balance),
usdc_balance: Number(usdc_balance),
});
} catch (error) {
console.error(`Error processing user ${user}:`, error);
}
}
return res.sendResponse(userWallets, "HD Wallets details fetched successfully.");
} catch (error) {
return res.sendError(`Error: ${error.message}`);
}
}
async function transferWithGasEstimation(sender:any, recipient:any, amount:any, privateKey:any, web3Provider: any) {
try {
const web3 = new Web3(web3Provider);
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(account);
// Estimate gas
const gasPrice = await web3.eth.getGasPrice();
const gasLimit = await web3.eth.estimateGas({
from: sender,
to: recipient,
value: web3.utils.toWei(amount.toString(), 'ether')
});
// Calculate gas fee
const gasFee = BigInt(gasPrice) * BigInt(gasLimit);
// Get sender balance
const balance = BigInt(await web3.eth.getBalance(sender));
// Ensure there is enough balance for transaction after gas deduction
if (balance < gasFee) {
throw new Error('Insufficient balance to cover gas fees');
}
// Calculate final transfer amount
const finalAmount = balance - gasFee;
if (finalAmount <= 0) {
throw new Error('Insufficient balance after gas deduction');
}
// Create and send transaction
const tx = {
from: sender,
to: recipient,
value: finalAmount.toString(),
gas: gasLimit,
gasPrice
};
const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
console.log('Transaction successful:', receipt.transactionHash);
return receipt;
} catch (error) {
console.error('Transaction failed:', error.message);
throw error;
}
}
Back to Directory
File Manager