eth


import {
    Account,
    BlockchainFamily,
    IBlockchain,
    Network,
    SendStatus,
} from "../interface";
import { getDerivedWallet } from "../lib/utils";
import Web3 from "web3";
import config from "../config";
import axios from "axios";

/** An Ethereum implementation of IBlockchain. */
export class Ethereum implements IBlockchain {
    /** Chain's name */
    static chainName = "Ethereum";
    /** Maine website */
    static website = "<https://ethereum.org/>";
    /** Description */
    static description: "Open source platform to write and distribute decentralized applications.";
    /** Explorer */
    static explorer = "<https://etherscan.io/>";
    /** Symbol. Uppercased. */
    static symbol = "ETH";
    /** Decimals */
    static decimals = 18;
    /** Github */
    static github = "<https://github.com/ethereum>";
    /** Twitter */
    static twitter = "<https://twitter.com/ethereum>";
    /** Reddit */
    static reddit = "<https://reddit.com/r/ethereum>";
    /** Whitepaper */
    static whitepaper = "<https://github.com/ethereum/wiki/wiki/White-Paper>";
    /** Faucet */
    static faucet = "<https://faucets.chain.link/rinkeby>";
    /** Blockchain family */
    static blochainFamily = BlockchainFamily.EVMBased;

    /**
     * BIP44 Ethereum mainnet derivation path.
     *
     * ```
     *     44         Ethereum      first      external  first
     * m / purpose' / coin_type' / account' / change  / address_index
     * ```
     *
     * @see <https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki>
     * @see <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>
     */
    static derivationPathMainnet = "m/44'/60'/0'/0/0";

    /** {@inheritDoc Ethereum.derivationPathMainnet} */
    static derivationPathTestnet = Ethereum.derivationPathMainnet;

    readonly symbol = Ethereum.symbol;
    readonly derivationPathMainnet = Ethereum.derivationPathMainnet;
    readonly derivationPathTestnet = Ethereum.derivationPathTestnet;

    async createAccountFromMnemonic(
        mnemonic: string,
        network: Network = Network.MAINNET
    ): Promise<Account> {
        const wallet = getDerivedWallet(
            mnemonic,
            network === Network.TESTNET
                ? Ethereum.derivationPathTestnet
                : Ethereum.derivationPathMainnet
        );

        const web3EthAccount = this.getWeb3(
            network
        ).eth.accounts.privateKeyToAccount(wallet.privateKey!.toString("hex"));

        const account: Account = {
            mnemonic,
            privateKey: web3EthAccount.privateKey,
            address: web3EthAccount.address,
            publicKey: "0x" + wallet.publicKey.toString("hex"),
        };

        return account;
    }

    // <https://ethereum.org/tr/developers/docs/gas>
    // A standard ETH transfer requires a gas limit of 21,000 units of gas
    async estimateSendNativeTokenFee(
        network: Network = Network.MAINNET,
        addressFrom?: string,
        addressTo?: string,
        amount?: number
    ): Promise<string> {
        const web3 = this.getWeb3(network);
        const standardTransferUnits = 21000;
        let transferUnits = standardTransferUnits;

        if (addressTo !== undefined) {
            const estimatedTransferUnits = await web3.eth.estimateGas({
                from: addressFrom,
                to: addressTo,
                value: amount,
            });
            transferUnits = estimatedTransferUnits;
        }

        const gasPrice = await web3.eth.getGasPrice();
        const gasPriceInWei = Web3.utils.toBN(gasPrice);
        const feeInWei = gasPriceInWei.mul(Web3.utils.toBN(transferUnits));
        return feeInWei.toString();
    }

    async getNativeTokenBalance(
        account: string,
        network: Network = Network.MAINNET
    ): Promise<string> {
        return this.getWeb3(network).eth.getBalance(account);
    }

    async getNowBlockNumber(
        network: Network = Network.MAINNET
    ): Promise<number> {
        return this.getWeb3(network).eth.getBlockNumber();
    }

    // <https://docs.etherscan.io/api-endpoints/accounts#get-a-list-of-normal-transactions-by-address>
    async getTransactions(
        account: string,
        network: Network = Network.MAINNET
    ): Promise<object[]> {
        const net = network === Network.TESTNET ? "rinkeby" : network;
        const response = await axios.get(
            `${config.ethereum[net].txScanProvider}/api?module=account&action=txlist&address=${account}&apikey=${config.ethereum[net].txScanApiKey}`
        );

        return response.data.result;
    }

    async isAccountActive(
        address: string,
        network: Network = Network.MAINNET
    ): Promise<boolean> {
        return true;
    }

    async isTransactionSucceeded(
        tx: string,
        network: Network = Network.MAINNET
    ): Promise<boolean> {
        const web3 = this.getWeb3(network);

        const [transaction, currentBlock] = await Promise.all([
            web3.eth.getTransaction(tx),
            web3.eth.getBlockNumber(),
        ]);

        // When transaction is unconfirmed, its block number is null.
        const numberOfconfirmations =
            transaction.blockNumber === null
                ? 0
                : currentBlock - transaction.blockNumber;

        return numberOfconfirmations >= config.ethereum.confirmationsToSucceed;
    }

    // Sending without gas trows:
    // Error: Signer Error: Signer Error: gasLimit is too low. given 0, need at least 21000.
    async sendNativeToken(
        privateKey: string,
        recipient: string,
        amount: string,
        network: Network = Network.MAINNET
    ): Promise<SendStatus> {
        const status: SendStatus = {
            ok: false,
            error: null,
            txHash: "",
        };

        const web3 = this.getWeb3(network);

        const web3EthAccount =
            web3.eth.accounts.privateKeyToAccount(privateKey);

        const wallet = web3.eth.accounts.wallet;
        wallet.add(web3EthAccount);

        const estimatedGasNecessaryToCompleteTransaction =
            await web3.eth.estimateGas({
                from: web3EthAccount.address,
                to: recipient,
                value: amount,
            });

        try {
            const receipt = await web3.eth.sendTransaction({
                from: web3EthAccount.address,
                to: recipient,
                value: amount,
                gas: estimatedGasNecessaryToCompleteTransaction,
            });
            status.ok = receipt.status;
            status.txHash = receipt.transactionHash;
        } catch (error) {
            if (error instanceof Error) {
                status.error = error;
            } else if (typeof error === "string") {
                status.error = new Error(error);
            } else {
                status.error = new Error("Send native token failed");
            }
        }

        return status;
    }

    private getWeb3(network: Network = Network.MAINNET) {
        const provider = new Web3.providers.HttpProvider(
            network === Network.TESTNET
                ? config.ethereum.rinkeby.httpProviderHost
                : config.ethereum.mainnet.httpProviderHost
        );
        const web3 = new Web3(provider);
        return web3;
    }

    async sendToken(
        privateKey: string,
        to: string,
        amount: string,
        contractAddress: string,
        decimals: number,
        network: Network = Network.MAINNET
    ): Promise<SendStatus> {
        throw new Error("Method not implemented.");
    }

    async getTokenBalance(
        account: string,
        contractAddress: string,
        decimals: number,
        network: Network = Network.MAINNET
    ): Promise<string> {
        throw new Error("Method not implemented.");
    }
}

interface

export enum Network {
    // eslint-disable-next-line no-unused-vars
    MAINNET = "mainnet",
    // eslint-disable-next-line no-unused-vars
    TESTNET = "testnet",
}

export enum BlockchainFamily {
    // eslint-disable-next-line no-unused-vars
    BitcoinBased,
    // eslint-disable-next-line no-unused-vars
    EVMBased,
    // eslint-disable-next-line no-unused-vars
    Custom,
}

export type TransactionStatus = {
    isConfirmed: boolean;
    isSucceded: boolean;
    transaction: any;
};

export type SendStatus = {
    ok: boolean;
    error: Error | null;
    txHash: string;
};

export type Account = {
    mnemonic: string;
    privateKey: string | Uint8Array; // for ALGO
    publicKey: string;
    address: string;
};

/**
 * An interface describing a blockchain.
 *
 * @public
 */
export interface IBlockchain {
    /**
     * Chain's symbol. Uppercased.
     *
     * @example Bitcoin:
     * ```
     * BTC
     * ```
     */
    readonly symbol: string;

    /**
     * Chain's mainnet derivation path.
     *
     * ```
     * m / purpose' / coin_type' / account' / change  / address_index
     * ```
     *
     * @example Bitcoin derivation path:
     * ```
     * m/44'/0'/0'/0/0
     * ```
     */
    readonly derivationPathMainnet: string;

    /**
     * Chain's testnet derivation path.
     *
     * ```
     * m / purpose' / coin_type' / account' / change  / address_index
     * ```
     *
     * @example Bitcoin Testnet derivation path:
     * ```
     * m/44'/1'/0'/0/0
     * ```
     */
    readonly derivationPathTestnet: string;

    /**
     * Returns Account details generated from the `menmonic` on the `network`.
     *
     * @privateRemarks
     *
     * <https://bitaps.com/mnemonic>
     * <https://iancoleman.io/bip39>
     * <https://medium.com/mycrypto/the-journey-from-mnemonic-phrase-to-address-6c5e86e11e14>
     *
     * @param mnemonic - The mnemonic to use.
     * @param network - Network to use. {@link Network.MAINNET} by default.
     * @returns Account.
     */
    createAccountFromMnemonic(
        mnemonic: string,
        network?: Network
    ): Promise<Account>;

    /**
     * Returns estimated transaction fee on the `network`.
     *
     * @param network - Network to check. {@link Network.MAINNET} by default.
     * @param addressFrom - addressFrom.
     * @param addressTo - addressTo.
     * @param amount - amount.
     * @returns estimated transaction fee.
     */
    estimateSendNativeTokenFee(
        network?: Network,
        addressFrom?: string,
        addressTo?: string,
        amount?: number
    ): Promise<string>;

    /**
     * Returns native balance for an address.
     *
     * @param account - The address on the network you're querying.
     * @param network - Network to query. {@link Network.MAINNET} by default.
     * @returns Balance.
     */
    getNativeTokenBalance(account: string, network?: Network): Promise<string>;

    /**
     * Returns last block number on the `network`.
     *
     * @param network - Network to query. {@link Network.MAINNET} by default.
     * @returns Last block number.
     */
    getNowBlockNumber(network?: Network): Promise<number>;

    /**
     * Returns a list of transaction objects for an `account`.
     *
     * @param account The address on the network you're querying.
     * @param network - Network to query. {@link Network.MAINNET} by default.
     * @param filter - object of options.
     * @returns List of transactions.
     */
    getTransactions(
        account: string,
        network?: Network,
        filter?: {
            timestampFrom?: number;
            blockFrom?: number;
            limit?: number;
        }
    ): Promise<object[]>;

    /**
     * Returns whether the `account` is active.
     *
     *  @privateRemarks
     *
     * Bitcoin account is always active 'by design'.
     *
     * @param account The account to check.
     * @param network Network to check. {@link Network.MAINNET} by default.
     * @returns `true` if accoint is active, `false` otherwise.
     */
    isAccountActive(address: string, network?: Network): Promise<boolean>;

    /**
     * Returns whether a transaction is confirmed.
     *
     * @param tx - The transaction on the network you're querying.
     * @param network - to query. {@link Network.MAINNET} by default.
     * @returns `true` if transaction is confirmed, `false` otherwise.
     */
    isTransactionSucceeded(tx: string, network?: Network): Promise<boolean>;

    /**
     * Sends `amount` to the specified `recipient` using `privateKey` on the `network`.
     * Returns transaction id.
     *
     * @param privateKey - The private key to sign the transaction.
     * @param recipient - The recipient address.
     * @param amount - The amount to send.
     * @param network - Network to use. {@link Network.MAINNET} by default.
     * @returns Send status.
     */
    sendNativeToken(
        privateKey: string,
        recipient: string,
        amount: string,
        network?: Network
    ): Promise<SendStatus>;

    /**
     *
     * @param privateKey
     * @param to
     * @param amount
     * @param contractAddress
     * @param decimals
     * @param network
     */
    sendToken(
        privateKey: string,
        to: string,
        amount: string,
        contractAddress: string,
        decimals: number,
        network: Network
    ): Promise<SendStatus>;

    /**
     *
     * @param account – Address of user to get balance.
     * @param contractAddress – Token smart-contract address.
     * @param decimals – Decimals of token. For example, if you want to send 1 TRON-USDT token (amount = 1) 6 decimals => contract.transfer(to, 1000000).
     *                   if !decimals (0 or null or undefined) => get decimals from contract
     *                   if you don't need decimals => decimals = 1
     * @param network – Network to use. {@link Network.MAINNET} by default.
     */

    /**
     * Returns trc-20 token balance for an address.
     *
     * @param account - The address on the network you're querying.
     * @param contractAddress – Token smart-contract address.
     * @param decimals – Decimals of token. For example, if you want to send 1 TRON-USDT token (amount = 1) 6 decimals => contract.transfer(to, 1000000).
     *           if !decimals (0 or null or undefined) => get decimals from contract
     *           if you don't need decimals => decimals = 1
     * @param network – Network to use. {@link Network.MAINNET} by default.
     * @returns Balance.
     */
    getTokenBalance(
        account: string,
        contractAddress: string,
        decimals: number,
        network: Network
    ): Promise<string>;
}