From 6dcdecd9838a8fb3e490ff0ecaa85d87e3acbd60 Mon Sep 17 00:00:00 2001 From: technojak <87927541+technojak@users.noreply.github.com> Date: Thu, 9 Sep 2021 11:50:24 -0600 Subject: [PATCH] feat: update eth adapter (#10) --- .eslintrc | 15 ++- .../generateAssetData/generateAssetData.ts | 2 +- packages/chain-adapters/src/api.ts | 59 +++++---- .../src/ethereum/EthereumChainAdapter.ts | 115 +++++++++++++----- .../market-service/src/coingecko/coingecko.ts | 4 +- 5 files changed, 135 insertions(+), 60 deletions(-) diff --git a/.eslintrc b/.eslintrc index 39394a094..5ac5ef345 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,7 +27,18 @@ "jsx-a11y/no-autofocus": "warn", "prettier/prettier": "error", "default-case": "off", - "no-console": "warn", + "no-console": [ + "warn", + { + "allow": [ + "warn", + "error", + "info", + "group", + "groupEnd" + ] + } + ], "@typescript-eslint/no-use-before-define": "error", "@typescript-eslint/member-delimiter-style": "off", "@typescript-eslint/explicit-module-boundary-types": "off", @@ -47,4 +58,4 @@ } } ] -} +} \ No newline at end of file diff --git a/packages/asset-service/src/generateAssetData/generateAssetData.ts b/packages/asset-service/src/generateAssetData/generateAssetData.ts index 75eb1c318..2d21dc4ba 100644 --- a/packages/asset-service/src/generateAssetData/generateAssetData.ts +++ b/packages/asset-service/src/generateAssetData/generateAssetData.ts @@ -23,5 +23,5 @@ const generateAssetData = async () => { } generateAssetData().then(() => { - console.log('done') + console.info('done') }) diff --git a/packages/chain-adapters/src/api.ts b/packages/chain-adapters/src/api.ts index ddb439bdc..9c534b34c 100644 --- a/packages/chain-adapters/src/api.ts +++ b/packages/chain-adapters/src/api.ts @@ -55,43 +55,49 @@ export type BroadcastTxResponse = { export type BuildSendTxInput = { to: string value: string - /** - * Optional param for eth txs indicating what ERC20 is being sent - */ - erc20ContractAddress?: string wallet: HDWallet path: string - chainId?: number + /*** In base units */ + fee?: string + /*** Optional param for eth txs indicating what ERC20 is being sent */ + erc20ContractAddress?: string + limit?: string } export type SignTxInput = { txToSign: ETHSignTx wallet: HDWallet } - export type GetAddressInput = { wallet: HDWallet path: string } -export type FeeData = { - /** - * gas (ethereum), vbytes (btc), etc - */ - units: string - /** - * price per unit - */ - price: string -} - -export type FeeEstimateInput = { - to: string +export type GetFeeDataInput = { + contractAddress?: string from: string - data: string + to: string value: string } +export enum FeeDataKey { + Slow = 'slow', + Average = 'average', + Fast = 'fast' +} + +export type FeeDataType = { + feeUnitPrice: string + networkFee: string + feeUnits: string +} + +export type FeeData = { + [FeeDataKey.Slow]: FeeDataType + [FeeDataKey.Average]: FeeDataType + [FeeDataKey.Fast]: FeeDataType +} + export enum ChainIdentifier { Ethereum = 'ethereum' } @@ -112,6 +118,13 @@ export type ValidAddressResult = { result: ValidAddressResultType } +export type FeeEstimateInput = { + to: string + from: string + data: string + value: string +} + export interface ChainAdapter { /** * Get type of adapter @@ -128,13 +141,15 @@ export interface ChainAdapter { */ getTxHistory(address: string, params?: Params): Promise - buildSendTransaction(input: BuildSendTxInput): Promise + buildSendTransaction( + input: BuildSendTxInput + ): Promise<{ txToSign: ETHSignTx; estimatedFees: FeeData }> getAddress(input: GetAddressInput): Promise signTransaction(signTxInput: SignTxInput): Promise - getFeeData(input: FeeEstimateInput): Promise + getFeeData(input: Partial): Promise broadcastTransaction(hex: string): Promise diff --git a/packages/chain-adapters/src/ethereum/EthereumChainAdapter.ts b/packages/chain-adapters/src/ethereum/EthereumChainAdapter.ts index 1db927ff2..19b15a87a 100644 --- a/packages/chain-adapters/src/ethereum/EthereumChainAdapter.ts +++ b/packages/chain-adapters/src/ethereum/EthereumChainAdapter.ts @@ -4,8 +4,8 @@ import { BuildSendTxInput, SignTxInput, GetAddressInput, + GetFeeDataInput, FeeData, - FeeEstimateInput, BalanceResponse, ChainIdentifier, ValidAddressResult, @@ -18,13 +18,43 @@ import { bip32ToAddressNList, ETHSignTx, ETHWallet } from '@shapeshiftoss/hdwall import { numberToHex } from 'web3-utils' import { Contract } from '@ethersproject/contracts' import erc20Abi from './erc20Abi.json' -import { BigNumber } from 'bignumber.js' import WAValidator from 'multicoin-address-validator' +import axios from 'axios' +import BigNumber from 'bignumber.js' export type EthereumChainAdapterDependencies = { provider: BlockchainProvider } +type ZrxFeeResult = { + fast: number + instant: number + low: number + source: + | 'ETH_GAS_STATION' + | 'ETHERSCAN' + | 'ETHERCHAIN' + | 'GAS_NOW' + | 'MY_CRYPTO' + | 'UP_VEST' + | 'GETH_PENDING' + | 'MEDIAN' + | 'AVERAGE' + standard: number + timestamp: number +} + +type ZrxGasApiResponse = { + result: ZrxFeeResult[] +} + +async function getErc20Data(to: string, value: string, contractAddress?: string) { + if (!contractAddress) return '' + const erc20Contract = new Contract(contractAddress, erc20Abi) + const { data: callData } = await erc20Contract.populateTransaction.transfer(to, value) + return callData || '' +} + export class EthereumChainAdapter implements ChainAdapter { private readonly provider: BlockchainProvider @@ -45,10 +75,7 @@ export class EthereumChainAdapter implements ChainAdapter { } } - getTxHistory = async ( - address: string, - params?: Params - ): Promise => { + getTxHistory = async (address: string, params?: Params): Promise => { try { return this.provider.getTxHistory(address, params) } catch (err) { @@ -56,42 +83,46 @@ export class EthereumChainAdapter implements ChainAdapter { } } - buildSendTransaction = async (tx: BuildSendTxInput): Promise => { + buildSendTransaction = async ( + tx: BuildSendTxInput + ): Promise<{ txToSign: ETHSignTx; estimatedFees: FeeData }> => { try { - const { to, erc20ContractAddress, path, wallet, chainId } = tx + const { to, erc20ContractAddress, path, wallet, fee, limit } = tx const value = erc20ContractAddress ? '0' : tx?.value const destAddress = erc20ContractAddress ?? to const addressNList = bip32ToAddressNList(path) - let data = '' - if (erc20ContractAddress) { - const erc20Contract = new Contract(erc20ContractAddress, erc20Abi) - const { data: callData } = await erc20Contract.populateTransaction.transfer(to, value) - data = callData || '' - } - + const data = await getErc20Data(to, value, erc20ContractAddress) const from = await this.getAddress({ wallet, path }) const nonce = await this.provider.getNonce(from) - const { price: gasPrice, units: gasLimit } = await this.getFeeData({ + let gasPrice = fee + let gasLimit = limit + const estimatedFees = await this.getFeeData({ + to, from, - to: destAddress, value, - data + contractAddress: erc20ContractAddress }) + if (!gasPrice || !gasLimit) { + // Default to average gas price if fee is not passed + !gasPrice && (gasPrice = estimatedFees.average.feeUnitPrice) + !gasLimit && (gasLimit = estimatedFees.average.feeUnits) + } + const txToSign: ETHSignTx = { addressNList, value: numberToHex(value), to: destAddress, - chainId: chainId || 1, + chainId: 1, // TODO: implement for multiple chains data, nonce: String(nonce), gasPrice: numberToHex(gasPrice), gasLimit: numberToHex(gasLimit) } - return txToSign + return { txToSign, estimatedFees } } catch (err) { return ErrorHandler(err) } @@ -100,8 +131,8 @@ export class EthereumChainAdapter implements ChainAdapter { signTransaction = async (signTxInput: SignTxInput): Promise => { try { const { txToSign, wallet } = signTxInput - const signedTx = await (wallet as ETHWallet).ethSignTx(txToSign) + if (!signedTx) throw new Error('Error signing tx') return signedTx.serialized @@ -114,21 +145,39 @@ export class EthereumChainAdapter implements ChainAdapter { return this.provider.broadcastTx(hex) } - getFeeData = async (feeEstimateInput: FeeEstimateInput): Promise => { - const [price, units] = await Promise.all([ - this.provider.getFeePrice(), - this.provider.getFeeUnits(feeEstimateInput) // Returns estimated gas for ETH - ]) + getFeeData = async ({ to, from, contractAddress, value }: GetFeeDataInput): Promise => { + const { data: responseData } = await axios.get('https://gas.api.0x.org/') + const fees = responseData.result.find((result) => result.source === 'MEDIAN') - // The node seems to be often estimating low gas price - // Hard code 1.5x multiplier to get it working for now - const adjustedPrice = new BigNumber(price).times(1.5).decimalPlaces(0) - // Hard code 2x gas limit multipiler - const adjustedGas = new BigNumber(units).times(2).decimalPlaces(0) + if (!fees) throw new TypeError('ETH Gas Fees should always exist') + + const data = await getErc20Data(to, value, contractAddress) + const feeUnits = await this.provider.getFeeUnits({ + from, + to, + value, + data + }) + + // PAD LIMIT + const gasLimit = new BigNumber(feeUnits).times(2).toString() return { - units: adjustedGas.toString(), - price: adjustedPrice.toString() + fast: { + feeUnits: gasLimit, + feeUnitPrice: String(fees.instant), + networkFee: new BigNumber(fees.instant).times(gasLimit).toPrecision() + }, + average: { + feeUnits: gasLimit, + feeUnitPrice: String(fees.fast), + networkFee: new BigNumber(fees.fast).times(gasLimit).toPrecision() + }, + slow: { + feeUnits: gasLimit, + feeUnitPrice: String(fees.low), + networkFee: new BigNumber(fees.low).times(gasLimit).toPrecision() + } } } diff --git a/packages/market-service/src/coingecko/coingecko.ts b/packages/market-service/src/coingecko/coingecko.ts index 796207da5..494a82b4d 100644 --- a/packages/market-service/src/coingecko/coingecko.ts +++ b/packages/market-service/src/coingecko/coingecko.ts @@ -35,10 +35,10 @@ export class CoinGeckoMarketService implements MarketService { ): Promise => { try { const isToken = !!contractAddress - const contractUrl = isToken ? `contract/${contractAddress}` : '' + const contractUrl = isToken ? `/contract/${contractAddress}` : '' const { data }: { data: CoinGeckoAssetData } = await axios.get( - `${this.baseUrl}/coins/${network}/${contractUrl}` + `${this.baseUrl}/coins/${network}${contractUrl}` ) // TODO: get correct localizations