@dojima-wallet/connection
Version:
Initialise and connection for layer 1&2 blockchain
594 lines (551 loc) • 15 kB
text/typescript
import { Balance, FeeType, Fees, Network, Tx, TxType } from "../client";
import {
Address,
Asset,
AssetETH,
BaseAmount,
EthChain,
assetAmount,
assetFromString,
assetToBase,
assetToString,
baseAmount,
eqAsset,
} from "@dojima-wallet/utils";
import { Signer, ethers, providers } from "ethers";
import { parseUnits } from "ethers/lib/utils";
import erc20ABI from "./data/erc20.json";
import {
ETHTransactionInfo,
EthNetwork,
FeesWithGasPricesAndLimits,
GasPrices,
TokenBalance,
TokenTransactionInfo,
TransactionInfo,
TransactionOperation,
} from "./types";
import { ETH_DECIMAL } from "./const";
export const ETHPLORER_FREEKEY = "freekey";
// from https://github.com/MetaMask/metamask-extension/blob/ee205b893fe61dc4736efc576e0663189a9d23da/ui/app/pages/send/send.constants.js#L39
// and based on recommendations of https://ethgasstation.info/blog/gas-limit/
export const SIMPLE_GAS_COST: ethers.BigNumber = ethers.BigNumber.from(21000);
export const BASE_TOKEN_GAS_COST: ethers.BigNumber =
ethers.BigNumber.from(100000);
// default gas price in gwei
export const DEFAULT_GAS_PRICE = 50;
export const ETHAddress = "0x0000000000000000000000000000000000000000";
export const MAX_APPROVAL: ethers.BigNumber = ethers.BigNumber.from(2)
.pow(256)
.sub(1);
/**
* Network -> EthNetwork
*
* @param {Network} network
* @returns {EthNetwork}
*/
export const chainNetworkToEths = (network: Network): EthNetwork => {
switch (network) {
case Network.Mainnet:
case Network.Stagenet:
return EthNetwork.Main;
case Network.Testnet:
return EthNetwork.Test;
}
};
/**
* EthNetwork -> Network
*
* @param {EthNetwork} network
* @returns {Network}
*/
export const ethNetworkTochains = (network: EthNetwork): Network => {
switch (network) {
case EthNetwork.Main:
return Network.Mainnet;
case EthNetwork.Test:
return Network.Testnet;
}
};
/**
* Validate the given address.
*
* @param {Address} address
* @returns {boolean} `true` or `false`
*/
export const validateAddress = (address: Address): boolean => {
try {
ethers.utils.getAddress(address);
return true;
} catch (error) {
return false;
}
};
/**
* Get token address from asset.
*
* @param {Asset} asset
* @returns {Address|null} The token address.
*/
export const getTokenAddress = (asset: Asset): Address | null => {
try {
// strip 0X only - 0x is still valid
return ethers.utils.getAddress(
asset.symbol.slice(asset.ticker.length + 1).replace(/^0X/, "")
);
} catch (err) {
return null;
}
};
/**
* Checks whether an `Asset` is `AssetETH` or not
*
* @param {Asset} asset
* @returns {boolean} Result of check if an asset is ETH or not
*/
export const isEthAsset = (asset: Asset): boolean => eqAsset(AssetETH, asset);
/**
* Parses asset address from `Asset`
*
* @param {Asset} asset
* @returns {Address|null} Asset address
*/
export const getAssetAddress = (asset: Asset): Address | null => {
if (isEthAsset(asset)) return ETHAddress;
return getTokenAddress(asset);
};
/**
* Check if the symbol is valid.
*
* @param {string|null|undefined} symbol
* @returns {boolean} `true` or `false`.
*/
export const validateSymbol = (symbol?: string | null): boolean =>
symbol ? symbol.length >= 3 : false;
/**
* Get transactions from token tx
*
* @param {TokenTransactionInfo} tx
* @returns {Tx|null} The parsed transaction.
*/
export const getTxFromTokenTransaction = (
tx: TokenTransactionInfo
): Tx | null => {
const decimals = parseInt(tx.tokenDecimal) || ETH_DECIMAL;
const symbol = tx.tokenSymbol;
const address = tx.contractAddress;
if (validateSymbol(symbol) && validateAddress(address)) {
const tokenAsset = assetFromString(
`${EthChain.ticker}.${symbol}-${address}`
);
if (tokenAsset) {
return {
asset: tokenAsset,
from: [
{
from: tx.from,
amount: baseAmount(tx.value, decimals),
},
],
to: [
{
to: tx.to,
amount: baseAmount(tx.value, decimals),
},
],
date: new Date(parseInt(tx.timeStamp) * 1000),
type: TxType.Transfer,
hash: tx.hash,
};
}
}
return null;
};
/**
* Get transactions from ETH transaction
*
* @param {ETHTransactionInfo} tx
* @returns {Tx} The parsed transaction.
*/
export const getTxFromEthTransaction = (tx: ETHTransactionInfo): Tx => {
return {
asset: AssetETH,
from: [
{
from: tx.from,
amount: baseAmount(tx.value, ETH_DECIMAL),
},
],
to: [
{
to: tx.to,
amount: baseAmount(tx.value, ETH_DECIMAL),
},
],
date: new Date(parseInt(tx.timeStamp) * 1000),
type: TxType.Transfer,
hash: tx.hash,
};
};
/**
* Get transactions from operation
*
* @param {TransactionOperation} operation
* @returns {Tx|null} The parsed transaction.
*/
export const getTxFromEthplorerTokenOperation = (
operation: TransactionOperation
): Tx | null => {
const decimals = parseInt(operation.tokenInfo.decimals) || ETH_DECIMAL;
const { symbol, address } = operation.tokenInfo;
if (validateSymbol(symbol) && validateAddress(address)) {
const tokenAsset = assetFromString(
`${EthChain.ticker}.${symbol}-${address}`
);
if (tokenAsset) {
return {
asset: tokenAsset,
from: [
{
from: operation.from,
amount: baseAmount(operation.value, decimals),
},
],
to: [
{
to: operation.to,
amount: baseAmount(operation.value, decimals),
},
],
date: new Date(operation.timestamp * 1000),
type: operation.type === "transfer" ? TxType.Transfer : TxType.Unknown,
hash: operation.transactionHash,
};
}
}
return null;
};
/**
* Get transactions from ETH transaction
*
* @param {TransactionInfo} txInfo
* @returns {Tx} The parsed transaction.
*/
export const getTxFromEthplorerEthTransaction = (
txInfo: TransactionInfo
): Tx => {
return {
asset: AssetETH,
from: [
{
from: txInfo.from,
amount: assetToBase(assetAmount(txInfo.value, ETH_DECIMAL)),
},
],
to: [
{
to: txInfo.to,
amount: assetToBase(assetAmount(txInfo.value, ETH_DECIMAL)),
},
],
date: new Date(txInfo.timestamp * 1000),
type: TxType.Transfer,
hash: txInfo.hash,
};
};
/**
* Calculate fees by multiplying .
*
* @returns {Fees} The default gas price.
*/
export const getFee = ({
gasPrice,
gasLimit,
}: {
gasPrice: BaseAmount;
gasLimit: ethers.BigNumber;
}) =>
baseAmount(gasPrice.amount().multipliedBy(gasLimit.toString()), ETH_DECIMAL);
export const estimateDefaultFeesWithGasPricesAndLimits = (
asset?: Asset
): FeesWithGasPricesAndLimits => {
const gasPrices = {
average: baseAmount(
parseUnits(DEFAULT_GAS_PRICE.toString(), "gwei").toString(),
ETH_DECIMAL
),
fast: baseAmount(
parseUnits((DEFAULT_GAS_PRICE * 2).toString(), "gwei").toString(),
ETH_DECIMAL
),
fastest: baseAmount(
parseUnits((DEFAULT_GAS_PRICE * 3).toString(), "gwei").toString(),
ETH_DECIMAL
),
};
const { fast: fastGP, fastest: fastestGP, average: averageGP } = gasPrices;
let assetAddress;
if (asset && assetToString(asset) !== assetToString(AssetETH)) {
assetAddress = getTokenAddress(asset);
}
let gasLimit;
if (assetAddress && assetAddress !== ETHAddress) {
gasLimit = ethers.BigNumber.from(BASE_TOKEN_GAS_COST);
} else {
gasLimit = ethers.BigNumber.from(SIMPLE_GAS_COST);
}
return {
gasPrices,
gasLimit,
fees: {
type: FeeType.PerByte,
average: getFee({ gasPrice: averageGP, gasLimit }),
fast: getFee({ gasPrice: fastGP, gasLimit }),
fastest: getFee({ gasPrice: fastestGP, gasLimit }),
},
};
};
/**
* Get the default fees.
*
* @returns {Fees} The default gas price.
*/
export const getDefaultFees = (asset?: Asset): Fees => {
const { fees } = estimateDefaultFeesWithGasPricesAndLimits(asset);
return fees;
};
/**
* Get the default gas price.
*
* @returns {Fees} The default gas prices.
*/
export const getDefaultGasPrices = (asset?: Asset): GasPrices => {
const { gasPrices } = estimateDefaultFeesWithGasPricesAndLimits(asset);
return gasPrices;
};
/**
* Get address prefix based on the network.
*
* @returns {string} The address prefix based on the network.
*
**/
export const getPrefix = () => "0x";
/**
* Filter self txs
*
* @returns {T[]}
*
**/
export const filterSelfTxs = <
T extends { from: string; to: string; hash: string }
>(
txs: T[]
): T[] => {
const filterTxs = txs.filter((tx) => tx.from !== tx.to);
let selfTxs = txs.filter((tx) => tx.from === tx.to);
while (selfTxs.length) {
const selfTx = selfTxs[0];
filterTxs.push(selfTx);
selfTxs = selfTxs.filter((tx) => tx.hash !== selfTx.hash);
}
return filterTxs;
};
/**
* Returns approval amount
*
* If given amount is not set or zero, `MAX_APPROVAL` amount is used
*/
export const getApprovalAmount = (amount?: BaseAmount): ethers.BigNumber =>
amount && amount.gt(baseAmount(0, amount.decimal))
? ethers.BigNumber.from(amount.amount().toFixed())
: MAX_APPROVAL;
/**
* Call a contract function.
*
* @param {Provider} provider Provider to interact with the contract.
* @param {Address} contractAddress The contract address.
* @param {ContractInterface} abi The contract ABI json.
* @param {string} funcName The function to be called.
* @param {unknown[]} funcParams The parameters of the function.
* @returns {BigNumber} The result of the contract function call.
*/
export const estimateCall = async ({
provider,
contractAddress,
abi,
funcName,
funcParams = [],
}: {
provider: providers.Provider;
contractAddress: Address;
abi: ethers.ContractInterface;
funcName: string;
funcParams?: unknown[];
}): Promise<ethers.BigNumber> => {
const contract: ethers.Contract = new ethers.Contract(
contractAddress,
abi,
provider
);
return await contract.estimateGas[funcName](...funcParams);
};
/**
* Calls a contract function.
*
* @param {Provider} provider Provider to interact with the contract.
* @param {signer} Signer of the transaction (optional - needed for sending transactions only)
* @param {Address} contractAddress The contract address.
* @param {ContractInterface} abi The contract ABI json.
* @param {string} funcName The function to be called.
* @param {unknown[]} funcParams (optional) The parameters of the function.
*
* @returns {T} The result of the contract function call.
*/
export const call = async <T>({
provider,
signer,
contractAddress,
abi,
funcName,
funcParams = [],
}: {
provider: providers.Provider;
signer?: Signer;
contractAddress: Address;
abi: ethers.ContractInterface;
funcName: string;
funcParams?: unknown[];
}): Promise<T> => {
let contract = new ethers.Contract(contractAddress, abi, provider);
if (signer) {
// For sending transactions a signer is needed
contract = contract.connect(signer);
}
return contract[funcName](...funcParams);
};
/**
* Estimate gas for calling `approve`.
*
* @param {Provider} provider Provider to interact with the contract.
* @param {Address} contractAddress The contract address.
* @param {Address} spenderAddress The spender address.
* @param {Address} fromAddress The address a transaction is sent from.
* @param {BaseAmount} amount (optional) The amount of token. By default, it will be unlimited token allowance.
*
* @returns {BigNumber} Estimated gas
*/
export const estimateApprove = async ({
provider,
contractAddress,
spenderAddress,
fromAddress,
abi,
amount,
}: {
provider: providers.Provider;
contractAddress: Address;
spenderAddress: Address;
fromAddress: Address;
abi: ethers.ContractInterface;
amount?: BaseAmount;
}): Promise<ethers.BigNumber> => {
const txAmount = getApprovalAmount(amount);
return await estimateCall({
provider,
contractAddress,
abi,
funcName: "approve",
funcParams: [spenderAddress, txAmount, { from: fromAddress }],
});
};
/**
* Get Decimals
*
* @param {Asset} asset
* @returns {Number} the decimal of a given asset
*
* @throws {"Invalid asset"} Thrown if the given asset is invalid
*/
export const getDecimal = async (
asset: Asset,
provider: providers.Provider
): Promise<number> => {
if (assetToString(asset) === assetToString(AssetETH)) return ETH_DECIMAL;
const assetAddress = getTokenAddress(asset);
if (!assetAddress) throw new Error(`Invalid asset ${assetToString(asset)}`);
const contract: ethers.Contract = new ethers.Contract(
assetAddress,
erc20ABI,
provider
);
const decimal: ethers.BigNumberish = await contract.decimals();
return ethers.BigNumber.from(decimal).toNumber();
};
/**
* Check allowance.
*
* @param {Provider} provider Provider to interact with the contract.
* @param {Address} contractAddress The contract (ERC20 token) address.
* @param {Address} spenderAddress The spender address (router).
* @param {Address} fromAddress The address a transaction is sent from.
* @param {BaseAmount} amount The amount to check if it's allowed to spend or not (optional).
* @param {number} walletIndex (optional) HD wallet index
* @returns {boolean} `true` or `false`.
*/
export const isApproved = async ({
provider,
contractAddress,
spenderAddress,
fromAddress,
amount,
}: {
provider: providers.Provider;
contractAddress: Address;
spenderAddress: Address;
fromAddress: Address;
amount?: BaseAmount;
}): Promise<boolean> => {
const txAmount = ethers.BigNumber.from(amount?.amount().toFixed() ?? 1);
const contract: ethers.Contract = new ethers.Contract(
contractAddress,
erc20ABI,
provider
);
const allowance: ethers.BigNumberish = await contract.allowance(
fromAddress,
spenderAddress
);
return txAmount.lte(allowance);
};
/**
* Get Token Balances
*
* @param {TokenBalance[]} tokenBalances
* @returns {Balance[]} the parsed balances
*
*/
export const getTokenBalances = (tokenBalances: TokenBalance[]): Balance[] => {
return tokenBalances.reduce((acc, cur) => {
const { symbol, address: tokenAddress } = cur.tokenInfo;
if (
validateSymbol(symbol) &&
validateAddress(tokenAddress) &&
cur?.tokenInfo?.decimals !== undefined
) {
const decimals = parseInt(cur.tokenInfo.decimals, 10);
const tokenAsset = assetFromString(
`${EthChain.ticker}.${symbol}-${ethers.utils.getAddress(tokenAddress)}`
);
if (tokenAsset) {
return [
...acc,
{
asset: tokenAsset,
amount: baseAmount(cur.balance, decimals),
},
];
}
}
return acc;
}, [] as Balance[]);
};