Skip to content

Commit

Permalink
feat: update eth adapter (shapeshift#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
technojak authored Sep 9, 2021
1 parent 9a9e8aa commit 6dcdecd
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 60 deletions.
15 changes: 13 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -47,4 +58,4 @@
}
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ const generateAssetData = async () => {
}

generateAssetData().then(() => {
console.log('done')
console.info('done')
})
59 changes: 37 additions & 22 deletions packages/chain-adapters/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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
Expand All @@ -128,13 +141,15 @@ export interface ChainAdapter {
*/
getTxHistory(address: string, params?: Params): Promise<TxHistoryResponse>

buildSendTransaction(input: BuildSendTxInput): Promise<any>
buildSendTransaction(
input: BuildSendTxInput
): Promise<{ txToSign: ETHSignTx; estimatedFees: FeeData }>

getAddress(input: GetAddressInput): Promise<string>

signTransaction(signTxInput: SignTxInput): Promise<string>

getFeeData(input: FeeEstimateInput): Promise<FeeData>
getFeeData(input: Partial<GetFeeDataInput>): Promise<FeeData>

broadcastTransaction(hex: string): Promise<string>

Expand Down
115 changes: 82 additions & 33 deletions packages/chain-adapters/src/ethereum/EthereumChainAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
BuildSendTxInput,
SignTxInput,
GetAddressInput,
GetFeeDataInput,
FeeData,
FeeEstimateInput,
BalanceResponse,
ChainIdentifier,
ValidAddressResult,
Expand All @@ -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

Expand All @@ -45,53 +75,54 @@ export class EthereumChainAdapter implements ChainAdapter {
}
}

getTxHistory = async (
address: string,
params?: Params
): Promise<TxHistoryResponse> => {
getTxHistory = async (address: string, params?: Params): Promise<TxHistoryResponse> => {
try {
return this.provider.getTxHistory(address, params)
} catch (err) {
return ErrorHandler(err)
}
}

buildSendTransaction = async (tx: BuildSendTxInput): Promise<ETHSignTx> => {
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)
}
Expand All @@ -100,8 +131,8 @@ export class EthereumChainAdapter implements ChainAdapter {
signTransaction = async (signTxInput: SignTxInput): Promise<string> => {
try {
const { txToSign, wallet } = signTxInput

const signedTx = await (wallet as ETHWallet).ethSignTx(txToSign)

if (!signedTx) throw new Error('Error signing tx')

return signedTx.serialized
Expand All @@ -114,21 +145,39 @@ export class EthereumChainAdapter implements ChainAdapter {
return this.provider.broadcastTx(hex)
}

getFeeData = async (feeEstimateInput: FeeEstimateInput): Promise<FeeData> => {
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<FeeData> => {
const { data: responseData } = await axios.get<ZrxGasApiResponse>('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()
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/market-service/src/coingecko/coingecko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export class CoinGeckoMarketService implements MarketService {
): Promise<AssetMarketData | null> => {
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
Expand Down

0 comments on commit 6dcdecd

Please sign in to comment.