Skip to content

Commit

Permalink
Make infura setup work with https
Browse files Browse the repository at this point in the history
I was trying to use infura-like setup with `ws` (Web sockets). Since
`ws` sends plain-text, it does not work in prouduction. We cannot use
`wss` since it is not supported natively by geth
 - ethereum/go-ethereum#16423
Even K8s only supports https. To avoid this issue, I shifted to https.
Subscriptions don't work with https, so, a crude polling mechanism to
check for transaction confirmations.

Also, in this commit, change the nonce count to include "pending"
transactions, so that, multiple transactions can be sent without waiting
for completion.
  • Loading branch information
ashishb committed Sep 18, 2019
1 parent b8a8d19 commit cefa35b
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 59 deletions.
33 changes: 13 additions & 20 deletions packages/mobile/src/web3/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addLocalAccount as web3utilsAddLocalAccount, StaticNodeUtils } from '@celo/walletkit'
import { addLocalAccount as web3utilsAddLocalAccount } from '@celo/walletkit'
import { DocumentDirectoryPath } from 'react-native-fs'
import * as net from 'react-native-tcp'
import { isGethFreeMode } from 'src/geth/consts'
Expand Down Expand Up @@ -50,15 +50,15 @@ const getIpcProvider = (testnet: Testnets) => {
}

const getWebSocketProvider = (url: string) => {
Logger.debug(tag, 'creating WebsocketProvider...')
const wsProvider = new Web3.providers.WebsocketProvider(url)
Logger.debug(tag, 'created WebsocketProvider')
Logger.debug(tag, 'creating HttpProvider...')
const provider = new Web3.providers.HttpProvider(url)
Logger.debug(tag, 'created HttpProvider')

// In the future, we might decide to over-ride the error handler via the following code.
// wsProvider.on('error', () => {
// provider.on('error', () => {
// Logger.showError('Error occurred')
// })
return wsProvider
return provider
}

let web3: Web3
Expand All @@ -67,20 +67,13 @@ export async function getWeb3() {
if (web3 === undefined) {
Logger.info(`Initializing web3, geth free mode: ${isGethFreeMode()}`)
if (isGethFreeMode()) {
Logger.debug('contracts@getWeb3', `Default testnet is ${DEFAULT_TESTNET}`)
const statipNodeIps: string = await StaticNodeUtils.getStaticNodesAsync(DEFAULT_TESTNET)
Logger.debug('contracts@getWeb3', `Static node IPs are ${statipNodeIps}`)
const enodeWithIp: string = JSON.parse(statipNodeIps)[0]
// Extract IP from "enode://<enode>@<IP>:<port>"
const staticNodeIp: string = enodeWithIp.split('@')[1].split(':')[0]
Logger.debug('contracts@getWeb3', `Static node IP is ${staticNodeIp}`)
if (staticNodeIp === undefined) {
throw new Error('Static node IP is undefined')
}
if (staticNodeIp === null) {
throw new Error('Static node IP is null')
}
const provider = getWebSocketProvider(`ws://${staticNodeIp}:8546`)
// Warning: This hostname is not yet enabled for all the networks.
// It is only enabled for "integration" and "alfajores" networks as of now.
// It is being enabled here for all the networks
// https://github.com/celo-org/celo-monorepo/pull/1034/
const url = `https://${DEFAULT_TESTNET}-infura.celo-testnet.org/`
Logger.debug('contracts@getWeb3', `Connecting to url ${url}`)
const provider = getWebSocketProvider(url)
web3 = new Web3(provider)
} else {
const provider = getIpcProvider(DEFAULT_TESTNET)
Expand Down
126 changes: 87 additions & 39 deletions packages/walletkit/src/contract-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { GasPriceMinimum as GasPriceMinimumType } from '../types/GasPriceMinimum
import { GoldToken } from '../types/GoldToken'
import { StableToken } from '../types/StableToken'
import { getGasPriceMinimumContract } from './contracts'
import { Logger } from './logger'
import { getGoldTokenAddress } from './erc20-utils'
import { Logger, LogLevel } from './logger'

Logger.setLogLevel(LogLevel.DEBUG)
const gasInflateFactor = 1.3

export function selectContractByAddress(contracts: Contract[], address: string) {
Expand Down Expand Up @@ -223,7 +225,7 @@ async function getGasPrice(
}
const gasPriceMinimum: GasPriceMinimumType = await getGasPriceMinimumContract(web3)
const gasPrice: string = await gasPriceMinimum.methods.getGasPriceMinimum(gasCurrency).call()
console.info(`Gas price is ${gasPrice}`)
Logger.debug('contract-utils@getGasPrice',`Gas price is ${gasPrice}`)
return String(parseInt(gasPrice, 10) * 10)
}

Expand Down Expand Up @@ -290,10 +292,14 @@ export async function sendTransactionAsync<T>(
// Ideally, we should fill these fields in CeloProvider but as of now,
// we don't have access to web3 inside it, so, in the short-term
// fill the fields here.
const gasCurrency = gasCurrencyContract._address
let gasCurrency = gasCurrencyContract._address
const gasFeeRecipient = await web3.eth.getCoinbase()
const gasPrice = await getGasPrice(web3, gasCurrency)
Logger.debug('contract-utils@sendTransactionAsync', `Gas fee recipient is ${gasFeeRecipient}`)
const gasPrice = await getGasPrice(web3, gasCurrency)
if (gasCurrency === undefined) {
gasCurrency = await getGoldTokenAddress(web3)
}
Logger.debug('contract-utils@sendTransactionAsync', `Gas currency: ${gasCurrency}`)

let recievedTxHash: string | null = null
let alreadyInformedResolversAboutConfirmation = false
Expand All @@ -315,55 +321,97 @@ export async function sendTransactionAsync<T>(
}
}

const nonce: number = await web3.eth.getTransactionCount(account)
// Use pending transaction count, so that, multiple transactions can be sent without
// waiting for the earlier ones to be confirmed.
const nonce: number = await web3.eth.getTransactionCount(account, 'pending')
Logger.debug('contract-utils@sendTransactionAsync', `sendTransactionAsync@nonce is ${nonce}`)
Logger.debug(
'contract-utils@sendTransactionAsync',
`sendTransactionAsync@sending from ${account}`
)

tx.send({
const celoTx = {
from: account,
nonce,
// @ts-ignore
gasCurrency: gasCurrencyContract._address,
gasCurrency,
gas: estimatedGas,
// Hack to prevent web3 from adding the suggested gold gas price, allowing geth to add
// the suggested price in the selected gasCurrency.
gasPrice,
gasFeeRecipient,
})
.on('receipt', (r: TransactionReceipt) => {
logger(ReceiptReceived(r))
if (resolvers.receipt) {
resolvers.receipt(r)
}
})
.on('transactionHash', (txHash: string) => {
recievedTxHash = txHash
logger(TransactionHashReceived(txHash))

if (resolvers.transactionHash) {
resolvers.transactionHash(txHash)
}
})
.on('confirmation', (confirmationNumber: number) => {
if (confirmationNumber > 1) {
console.debug(`Confirmation number is ${confirmationNumber} > 1, ignored...`)
// "confirmation" event is called for 24 blocks.
// if check to avoid polluting the logs and trying to remove the standby notification more than once
return
}
informAboutConfirmation()
})
.on('error', (error: Error) => {
Logger.info(
'contract-utils@sendTransactionAsync',
`Txn failed: txn ${util.inspect(error)} `
)
logger(Failed(error))
rejectAll(error)
})
}
try {
await tx.send(celoTx)
// TODO: disable this only in infura mode.
// .on('receipt', (r: TransactionReceipt) => {
// logger(ReceiptReceived(r))
// if (resolvers.receipt) {
// resolvers.receipt(r)
// }
// })
// .on('transactionHash', (txHash: string) => {
// recievedTxHash = txHash
// logger(TransactionHashReceived(txHash))

// if (resolvers.transactionHash) {
// resolvers.transactionHash(txHash)
// }
// })
// .on('confirmation', (confirmationNumber: number) => {
// if (confirmationNumber > 1) {
// console.debug(`Confirmation number is ${confirmationNumber} > 1, ignored...`)
// // "confirmation" event is called for 24 blocks.
// // if check to avoid polluting the logs and trying to remove the standby notification more than once
// return
// }
// informAboutConfirmation()
// })
// .on('error', (error: Error) => {
// Logger.info(
// 'contract-utils@sendTransactionAsync',
// `Txn failed: txn ${util.inspect(error)} `
// )
// logger(Failed(error))
// rejectAll(error)
// })
} catch (e) {
Logger.debug('contract-utils@sendTransactionAsync',`Ignoring error: ${util.inspect(e)}`)
Logger.debug('contract-utils@sendTransactionAsync',`error message: ${e.message}`)
// Ideally, I want to only ignore error whose messsage contains
// "Failed to subscribe to new newBlockHeaders" but seems like another wrapped
// error (listed below) gets thrown and there is no way to catch that.
// "Failed to subscribe to new newBlockHeaders" is thrown when the wallet kit is connected
// to a remote node via https.
// { [Error: [object Object]]
// line: 352771,
// column: 24,
// sourceURL: 'http://localhost:8081/index.delta?platform=android&dev=true&minify=false' }
// contract-utils@sendTransactionAsync: error: { [Error: Failed to subscribe to new newBlockHeaders to confi
// rm the transaction receipts.
// {
// "line": 352771,
// "column": 24,
// "sourceURL": "http://localhost:8081/index.delta?platform=android&dev=true&minify=false"
// }]
// line: 157373,
// column: 24,
// sourceURL: 'http://localhost:8081/index.delta?platform=android&dev=true&minify=false' }
if (e.message.indexOf('Failed to subscribe to new newBlockHeaders') >= 0) {
// Ignore this error
Logger.warn('contract-utils@sendTransactionAsync', `Expected error ignored: ${JSON.stringify(e)}`)
} else {
// TODO(ashishb): testing only
Logger.debug('contract-utils@sendTransactionAsync',`Unexpected error ignored: ${util.inspect(e)}`)
}
const signedTxn = await web3.eth.signTransaction(celoTx)
recievedTxHash = web3.utils.sha3(signedTxn.raw)
Logger.info('contract-utils@sendTransactionAsync', `Locally calculated recievedTxHash is ${recievedTxHash}`)
logger(TransactionHashReceived(recievedTxHash))
if (resolvers.transactionHash) {
resolvers.transactionHash(recievedTxHash)
}
}

// This code is required for infura-like setup.
// When mobile client directly connects to the remote full node then
Expand Down

0 comments on commit cefa35b

Please sign in to comment.