Skip to content
This repository has been archived by the owner on Jan 15, 2021. It is now read-only.

gasPrice and tx.data middlewares #1013

Merged
merged 24 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"combine-reducers": "^1.0.0",
"date-fns": "^2.9.0",
"detect-browser": "^5.1.0",
"eth-json-rpc-middleware": "^4.4.1",
"json-rpc-engine": "^5.1.8",
"modali": "^1.2.0",
"node-cache": "^5.1.0",
"qrcode.react": "^1.0.0",
Expand All @@ -54,7 +56,7 @@
"react-select": "^3.0.8",
"react-toastify": "^5.5.0",
"styled-components": "^5.0.0",
"web3": "^1.2.5"
"web3": "1.2.7"
},
"devDependencies": {
"@babel/core": "^7.8.4",
Expand Down
15 changes: 3 additions & 12 deletions src/api/deposit/DepositApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,13 @@ export interface PendingFlux {

export interface Params {
web3: Web3
fetchGasPrice(): Promise<string | undefined>
}

export class DepositApiImpl implements DepositApi {
protected _contractPrototype: BatchExchangeContract
protected web3: Web3
protected static _contractsCache: { [network: number]: { [address: string]: BatchExchangeContract } } = {}

protected fetchGasPrice: Params['fetchGasPrice']

public constructor(injectedDependencies: Params) {
Object.assign(this, injectedDependencies)

Expand Down Expand Up @@ -142,9 +139,7 @@ export class DepositApiImpl implements DepositApi {
}: DepositParams): Promise<Receipt> {
const contract = await this._getContract(networkId)
// TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231
const tx = contract.methods
.deposit(tokenAddress, amount.toString())
.send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
const tx = contract.methods.deposit(tokenAddress, amount.toString()).send({ from: userAddress })

if (txOptionalParams && txOptionalParams.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand All @@ -163,9 +158,7 @@ export class DepositApiImpl implements DepositApi {
}: RequestWithdrawParams): Promise<Receipt> {
const contract = await this._getContract(networkId)
// TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231
const tx = contract.methods
.requestWithdraw(tokenAddress, amount.toString())
.send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
const tx = contract.methods.requestWithdraw(tokenAddress, amount.toString()).send({ from: userAddress })

if (txOptionalParams?.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand All @@ -179,9 +172,7 @@ export class DepositApiImpl implements DepositApi {

public async withdraw({ userAddress, tokenAddress, networkId, txOptionalParams }: WithdrawParams): Promise<Receipt> {
const contract = await this._getContract(networkId)
const tx = contract.methods
.withdraw(userAddress, tokenAddress)
.send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
const tx = contract.methods.withdraw(userAddress, tokenAddress).send({ from: userAddress })

if (txOptionalParams?.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand Down
24 changes: 24 additions & 0 deletions src/api/earmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { logDebug } from 'utils'

// 0.1 Gwei, reasonable gasPrice that would still allow flags replacement
export const MIN_GAS_PRICE = (1e8).toString(10)

export const earmarkGasPrice = (gasPrice: string, userPrint: string): string => {
if (!userPrint) return gasPrice

// don't replace 8000 -> 1201, only if most significant digit is untouched
// 80000 -> 81201
if (userPrint.length >= gasPrice.length) {
// if flags still don't fit even in MIN_GAS_PRICE
if (userPrint.length >= MIN_GAS_PRICE.length) return gasPrice
gasPrice = MIN_GAS_PRICE
}

const markedGasPrice = gasPrice.slice(0, -userPrint.length) + userPrint

logDebug('Gas price', gasPrice, '->', markedGasPrice)
return markedGasPrice
}

// simple concatetaion, with '0x' for empty data to have `0x<userPrint>` at the least
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// simple concatetaion, with '0x' for empty data to have `0x<userPrint>` at the least
// simple concatenation, with '0x' for empty data to have `0x<userPrint>` at the least

export const earmarkTxData = (data = '0x', userPrint: string): string => data + userPrint
6 changes: 0 additions & 6 deletions src/api/erc20/Erc20Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export interface Erc20Api {

export interface Params {
web3: Web3
fetchGasPrice(): Promise<string | undefined>
}

/**
Expand All @@ -93,8 +92,6 @@ export class Erc20ApiImpl implements Erc20Api {

private static _contractsCache: { [network: number]: { [address: string]: Erc20Contract } } = {}

private fetchGasPrice: Params['fetchGasPrice']

public constructor(injectedDependencies: Params) {
Object.assign(this, injectedDependencies)

Expand Down Expand Up @@ -186,7 +183,6 @@ export class Erc20ApiImpl implements Erc20Api {
// TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231
const tx = erc20.methods.approve(spenderAddress, amount.toString()).send({
from: userAddress,
gasPrice: await this.fetchGasPrice(),
})

if (txOptionalParams?.onSentTransaction) {
Expand All @@ -209,7 +205,6 @@ export class Erc20ApiImpl implements Erc20Api {
// TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231
const tx = erc20.methods.transfer(toAddress, amount.toString()).send({
from: userAddress,
gasPrice: await this.fetchGasPrice(),
})

if (txOptionalParams?.onSentTransaction) {
Expand All @@ -232,7 +227,6 @@ export class Erc20ApiImpl implements Erc20Api {

const tx = erc20.methods.transferFrom(userAddress, toAddress, amount.toString()).send({
from: fromAddress,
gasPrice: await this.fetchGasPrice(),
})

if (txOptionalParams?.onSentTransaction) {
Expand Down
8 changes: 4 additions & 4 deletions src/api/exchange/ExchangeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi {

public async addToken({ userAddress, tokenAddress, networkId, txOptionalParams }: AddTokenParams): Promise<Receipt> {
const contract = await this._getContract(networkId)
const tx = contract.methods.addToken(tokenAddress).send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
const tx = contract.methods.addToken(tokenAddress).send({ from: userAddress })

if (txOptionalParams && txOptionalParams.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand Down Expand Up @@ -223,7 +223,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi {
// TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231
const tx = contract.methods
.placeOrder(buyTokenId, sellTokenId, validUntil, buyAmount.toString(), sellAmount.toString())
.send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
.send({ from: userAddress })

if (txOptionalParams && txOptionalParams.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand Down Expand Up @@ -264,7 +264,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi {

const tx = contract.methods
.placeValidFromOrders(buyTokens, sellTokens, validFroms, validUntils, buyAmountsStr, sellAmountsStr)
.send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
.send({ from: userAddress })

if (txOptionalParams?.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand All @@ -290,7 +290,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi {
txOptionalParams,
}: CancelOrdersParams): Promise<Receipt> {
const contract = await this._getContract(networkId)
const tx = contract.methods.cancelOrders(orderIds).send({ from: userAddress, gasPrice: await this.fetchGasPrice() })
const tx = contract.methods.cancelOrders(orderIds).send({ from: userAddress })

if (txOptionalParams && txOptionalParams.onSentTransaction) {
tx.once('transactionHash', txOptionalParams.onSentTransaction)
Expand Down
25 changes: 2 additions & 23 deletions src/api/gasStation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { WalletApi } from './wallet/WalletApi'
import { logDebug } from 'utils'

const GAS_STATIONS = {
1: 'https://safe-relay.gnosis.pm/api/v1/gas-station/',
Expand Down Expand Up @@ -27,26 +26,6 @@ interface GasStationResponse {
fastest: string
}

// 0.1 Gwei, reasonable gasPrice that would still allow flags replacement
export const MIN_GAS_PRICE = (1e8).toString(10)

export const earmarkGasPrice = (gasPrice: string, userPrint: string): string => {
if (!userPrint) return gasPrice

// don't replace 8000 -> 1201, only if most significant digit is untouched
// 80000 -> 81201
if (userPrint.length >= gasPrice.length) {
// if flags still don't fit even in MIN_GAS_PRICE
if (userPrint.length >= MIN_GAS_PRICE.length) return gasPrice
gasPrice = MIN_GAS_PRICE
}

const markedGasPrice = gasPrice.slice(0, -userPrint.length) + userPrint

logDebug('Gas price', gasPrice, '->', markedGasPrice)
return markedGasPrice
}

const fetchGasPriceFactory = (walletApi: WalletApi) => async (): Promise<string | undefined> => {
const { blockchainState } = walletApi

Expand All @@ -57,7 +36,7 @@ const fetchGasPriceFactory = (walletApi: WalletApi) => async (): Promise<string
// only fetch new gasPrice when chainId or blockNumber changed
const key = constructKey({ chainId, blockNumber: blockHeader && blockHeader.number })
if (key === cacheKey) {
return earmarkGasPrice(cachedGasPrice, await walletApi.userPrintAsync)
return cachedGasPrice
}

const gasStationURL = GAS_STATIONS[chainId]
Expand All @@ -73,7 +52,7 @@ const fetchGasPriceFactory = (walletApi: WalletApi) => async (): Promise<string
cacheKey = key
cachedGasPrice = gasPrice

return earmarkGasPrice(gasPrice, await walletApi.userPrintAsync)
return gasPrice
}
} catch (error) {
console.error('[api:gasStation] Error fetching gasPrice from', gasStationURL, error)
Expand Down
6 changes: 1 addition & 5 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
} from '../../test/data'
import Web3 from 'web3'
import { ETH_NODE_URL } from 'const'
import fetchGasPriceFactory from './gasStation'

// TODO connect to mainnet if we need AUTOCONNECT at all
export const getDefaultProvider = (): string | null => (process.env.NODE_ENV === 'test' ? null : ETH_NODE_URL)
Expand Down Expand Up @@ -169,10 +168,7 @@ function createTcrApi(web3: Web3): TcrApi | undefined {
export const web3: Web3 = createWeb3Api()
export const walletApi: WalletApi = createWalletApi(web3)

const injectedDependencies = {
web3,
fetchGasPrice: fetchGasPriceFactory(walletApi),
}
const injectedDependencies = { web3 }

export const erc20Api: Erc20Api = createErc20Api(injectedDependencies)
export const depositApi: DepositApi = createDepositApi(erc20Api, injectedDependencies)
Expand Down
14 changes: 11 additions & 3 deletions src/api/wallet/WalletApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ import {
Subscriptions,
} from '@gnosis.pm/dapp-ui'

import { logDebug, toBN, gasPriceEncoder } from 'utils'
import { logDebug, toBN, txDataEncoder } from 'utils'
import { INFURA_ID } from 'const'

import { subscribeToWeb3Event } from './subscriptionHelpers'
import { getMatchingScreenSize, subscribeToScreenSizeChange } from 'utils/mediaQueries'
import { composeProvider } from './composeProvider'
import fetchGasPriceFactory from 'api/gasStation'
import { earmarkTxData } from 'api/earmark'

export interface WalletApi {
isConnected(): boolean | Promise<boolean>
Expand Down Expand Up @@ -244,8 +247,13 @@ export class WalletApiImpl implements WalletApi {

closeOpenWebSocketConnection(this._web3)

const fetchGasPrice = fetchGasPriceFactory(this)
const earmarkingFunction = async (data?: string): Promise<string> => earmarkTxData(data, await this.userPrintAsync)

const composedProvider = composeProvider(provider, { fetchGasPrice, earmarkTxData: earmarkingFunction })

// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._web3.setProvider(provider as any)
this._web3.setProvider(composedProvider as any)
logDebug('[WalletApiImpl] Connected')

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -471,7 +479,7 @@ export class WalletApiImpl implements WalletApi {
screenSize,
}

const encoded = gasPriceEncoder(flagObject)
const encoded = txDataEncoder(flagObject)

logDebug('Encoded object', flagObject)
logDebug('User Wallet print', encoded)
Expand Down
114 changes: 114 additions & 0 deletions src/api/wallet/composeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import RpcEngine, {
JsonRpcEngine,
JsonRpcMiddleware,
JsonRpcRequest,
JsonRpcResponse,
JsonRpcError,
} from 'json-rpc-engine'
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine'
import { Provider } from '@gnosis.pm/dapp-ui'
import { WebsocketProvider, TransactionConfig } from 'web3-core'
import { numberToHex } from 'web3-utils'

// custom providerAsMiddleware
function providerAsMiddleware(provider: Provider | WebsocketProvider): JsonRpcMiddleware {
// MMask provider uses sendAsync
// WS provider doesn't have sendAsync
const sendFName = 'sendAsync' in provider ? 'sendAsync' : 'send'

return (req, res, _next, end): void => {
// send request to provider

provider[sendFName](req, (err: JsonRpcError<unknown>, providerRes: JsonRpcResponse<unknown>) => {
// forward any error
if (err) return end(err)
// copy provider response onto original response
Object.assign(res, providerRes)
end()
})
}
}

const createConditionalMiddleware = <T extends unknown>(
condition: (req: JsonRpcRequest<T>) => boolean,
handle: (req: JsonRpcRequest<T>, res: JsonRpcResponse<T>) => boolean | Promise<boolean>, // handled -- true, not --false
): JsonRpcMiddleware => {
return async (req: JsonRpcRequest<T>, res: JsonRpcResponse<T>, next, end): Promise<void> => {
// if not condition, skip and got to next middleware
if (!condition(req)) return next()

// if handled fully, end here
if (await handle(req, res)) return end()
// otherwise continue to next middleware
next()
}
}

interface ExtraMiddlewareHandlers {
fetchGasPrice(): Promise<string | undefined>
earmarkTxData(data?: string): Promise<string>
}

export const composeProvider = (
provider: Provider,
{ fetchGasPrice, earmarkTxData }: ExtraMiddlewareHandlers,
): Provider => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const engine = new (RpcEngine as any)() as JsonRpcEngine

engine.push(
createConditionalMiddleware<[]>(
req => req.method === 'eth_gasPrice',
async (_req, res) => {
const fetchedPrice = await fetchGasPrice()

// got price
if (fetchedPrice) {
res.result = numberToHex(fetchedPrice)
// handled
return true
}

// not handled
return false
},
),
)

engine.push(
createConditionalMiddleware<TransactionConfig[]>(
req => req.method === 'eth_sendTransaction',
async req => {
const txConfig = req.params?.[0]
// no parameters, which shouldn't happen
if (!txConfig) return false

const earmarkedData = await earmarkTxData(txConfig.data)

txConfig.data = earmarkedData
// don't mark as handled
// pass modified tx on
return false
},
),
)

const walletMiddleware = providerAsMiddleware(provider)

engine.push(walletMiddleware)

const composedProvider: Provider = providerFromEngine(engine)

const providerProxy = new Proxy(composedProvider, {
get: function(target, prop, receiver): unknown {
if (prop === 'sendAsync' || prop === 'send') {
// composedProvider handles it
return Reflect.get(target, prop, receiver)
}
// MMask or other provider handles it
return Reflect.get(provider, prop, receiver)
},
})

return providerProxy
}
Loading