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>;
}