@elephant-xyz/cli
Version: 
CLI tool for Elephant Network
165 lines • 8.46 kB
JavaScript
import { writeFileSync } from 'fs';
import { dirname } from 'path';
import { mkdir } from 'fs/promises';
import { ethers } from 'ethers';
import { SUBMIT_CONTRACT_ABI_FRAGMENTS, SUBMIT_CONTRACT_METHODS, GAS_ESTIMATION_BUFFER, FALLBACK_GAS_LIMIT, DEFAULT_MAX_FEE_PER_GAS_GWEI, DEFAULT_MAX_PRIORITY_FEE_PER_GAS_GWEI, MIN_PRIORITY_FEE_GWEI, } from '../config/constants.js';
import { extractHashFromCID } from '../utils/validation.js';
import { logger } from '../utils/logger.js';
export class UnsignedTransactionJsonService {
    jsonPath;
    contractAddress;
    gasPrice;
    startingNonce;
    constructor(jsonPath, contractAddress, gasPrice = 'auto', _chainId = 137, // Polygon mainnet - kept for backward compatibility but not used
    startingNonce = 0) {
        this.jsonPath = jsonPath;
        this.contractAddress = contractAddress;
        this.gasPrice = gasPrice;
        this.startingNonce = startingNonce;
    }
    /**
     * Prepares DataItem for contract call by converting CIDs to hashes.
     */
    prepareDataItemForContract(item) {
        // Strip leading dots from CIDs if present and convert to hashes
        const cleanPropertyCid = item.propertyCid.startsWith('.')
            ? item.propertyCid.substring(1)
            : item.propertyCid;
        const cleanDataGroupCID = item.dataGroupCID.startsWith('.')
            ? item.dataGroupCID.substring(1)
            : item.dataGroupCID;
        const cleanDataCID = item.dataCID.startsWith('.')
            ? item.dataCID.substring(1)
            : item.dataCID;
        return {
            propertyHash: extractHashFromCID(cleanPropertyCid),
            dataGroupHash: extractHashFromCID(cleanDataGroupCID),
            dataHash: extractHashFromCID(cleanDataCID),
        };
    }
    /**
     * Creates EIP-1474 compliant transaction object for a batch of items
     */
    async createEIP1474Transaction(batchItems, userAddress, nonce, provider) {
        const preparedBatch = batchItems.map((item) => this.prepareDataItemForContract(item));
        // Create contract interface to encode function data
        const contractInterface = new ethers.Interface(SUBMIT_CONTRACT_ABI_FRAGMENTS);
        const functionData = contractInterface.encodeFunctionData(SUBMIT_CONTRACT_METHODS.SUBMIT_BATCH_DATA, [preparedBatch]);
        // Estimate gas using direct eth_estimateGas RPC call
        let gasLimit;
        try {
            const gasEstimateParams = [
                {
                    from: userAddress,
                    to: this.contractAddress,
                    data: functionData,
                    value: '0x0',
                },
                'latest',
            ];
            const estimatedGasHex = await provider.send('eth_estimateGas', gasEstimateParams);
            const estimatedGas = BigInt(estimatedGasHex);
            // Add buffer for safety
            const gasWithBuffer = estimatedGas +
                BigInt(Math.floor(Number(estimatedGas) * GAS_ESTIMATION_BUFFER));
            gasLimit = `0x${gasWithBuffer.toString(16)}`;
            logger.technical(`Gas estimated: ${estimatedGas}, with ${GAS_ESTIMATION_BUFFER * 100}% buffer: ${gasWithBuffer}`);
        }
        catch (error) {
            logger.warn(`Gas estimation via eth_estimateGas failed: ${error instanceof Error ? error.message : String(error)}`);
            // Fallback to reasonable default
            gasLimit = `0x${BigInt(FALLBACK_GAS_LIMIT).toString(16)}`; // Fallback gas limit with buffer included
        }
        // Create EIP-1474 compliant transaction object
        // Always use EIP-1559 Type 2 transactions (no legacy support)
        const transaction = {
            from: userAddress,
            to: this.contractAddress,
            gas: gasLimit,
            value: '0x0',
            data: functionData,
            nonce: `0x${nonce.toString(16)}`,
            type: '0x2', // Always EIP-1559 Type 2
        };
        // Set gas pricing for EIP-1559 transaction
        if (this.gasPrice === 'auto') {
            // Fetch dynamic fee data from provider
            try {
                const feeData = await provider.getFeeData();
                if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
                    transaction.maxFeePerGas = `0x${feeData.maxFeePerGas.toString(16)}`;
                    transaction.maxPriorityFeePerGas = `0x${feeData.maxPriorityFeePerGas.toString(16)}`;
                }
                else {
                    // Fallback to reasonable defaults if provider doesn't support EIP-1559
                    transaction.maxFeePerGas = `0x${ethers.parseUnits(DEFAULT_MAX_FEE_PER_GAS_GWEI, 'gwei').toString(16)}`;
                    transaction.maxPriorityFeePerGas = `0x${ethers.parseUnits(DEFAULT_MAX_PRIORITY_FEE_PER_GAS_GWEI, 'gwei').toString(16)}`;
                }
            }
            catch (error) {
                logger.warn(`Failed to fetch fee data from provider: ${error instanceof Error ? error.message : String(error)}`);
                // Fallback to defaults
                transaction.maxFeePerGas = `0x${ethers.parseUnits(DEFAULT_MAX_FEE_PER_GAS_GWEI, 'gwei').toString(16)}`;
                transaction.maxPriorityFeePerGas = `0x${ethers.parseUnits(DEFAULT_MAX_PRIORITY_FEE_PER_GAS_GWEI, 'gwei').toString(16)}`;
            }
        }
        else {
            // Convert numeric gas price to EIP-1559 format
            const gasPrice = ethers.parseUnits(this.gasPrice.toString(), 'gwei');
            transaction.maxFeePerGas = `0x${gasPrice.toString(16)}`;
            // Use 10% of maxFeePerGas as priority fee, minimum MIN_PRIORITY_FEE_GWEI
            const priorityFee = BigInt(Math.max(Number(gasPrice) * 0.1, Number(ethers.parseUnits(MIN_PRIORITY_FEE_GWEI, 'gwei'))));
            transaction.maxPriorityFeePerGas = `0x${priorityFee.toString(16)}`;
        }
        return transaction;
    }
    /**
     * Generates unsigned transactions for batches of items and returns them
     */
    async generateUnsignedTransactions(batches, rpcUrl, userAddress) {
        // Create provider - required for gas estimation and nonce fetching
        const provider = new ethers.JsonRpcProvider(rpcUrl);
        // Get starting nonce from provider with better error handling
        let currentNonce = this.startingNonce;
        try {
            currentNonce = await provider.getTransactionCount(userAddress, 'pending');
            logger.info(`Starting nonce from provider: ${currentNonce}`);
        }
        catch (error) {
            logger.warn(`Failed to get nonce from provider, using default: ${currentNonce}. Error: ${error}`);
            // Continue with default nonce but keep provider for gas estimation
        }
        const transactions = [];
        // Process each batch
        for (let i = 0; i < batches.length; i++) {
            const batch = batches[i];
            logger.info(`Generating unsigned transaction for batch ${i + 1} of ${batches.length} (${batch.length} items)`);
            // Create EIP-1474 compliant transaction (gas estimation is now internal)
            const transaction = await this.createEIP1474Transaction(batch, userAddress, currentNonce + i, provider);
            transactions.push(transaction);
        }
        return transactions;
    }
    /**
     * Generates unsigned transactions JSON for batches of items
     */
    async generateUnsignedTransactionsJson(batches, rpcUrl, userAddress) {
        try {
            // Ensure directory exists
            await mkdir(dirname(this.jsonPath), { recursive: true });
            // Generate the transactions
            const transactions = await this.generateUnsignedTransactions(batches, rpcUrl, userAddress);
            // Write JSON file
            const jsonOutput = JSON.stringify(transactions, null, 2);
            writeFileSync(this.jsonPath, jsonOutput, 'utf-8');
            logger.success(`Unsigned transactions JSON written to: ${this.jsonPath}`);
            logger.info(`Generated ${transactions.length} unsigned transactions for ${batches.reduce((sum, batch) => sum + batch.length, 0)} total items`);
        }
        catch (error) {
            const errorMsg = `Failed to generate unsigned transactions JSON: ${error instanceof Error ? error.message : String(error)}`;
            logger.error(errorMsg);
            throw new Error(errorMsg);
        }
    }
}
//# sourceMappingURL=unsigned-transaction-json.service.js.map