Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API client & function to calculate max gas price #4

Merged
merged 31 commits into from
Jun 22, 2020
Merged

Conversation

andreogle
Copy link
Member

@andreogle andreogle commented Jun 16, 2020

In the interests of starting small and keeping PRs easy to review, here's a small change that will get gas prices from 2 APIs and calculate the maximum "fastest" gas price of these.

Added

  1. Added axios for making HTTP requests. Axios is extremely configurable, well maintained and has a promise-like API.
  2. Added a Go style helper for working with promises. This removes the ugliness of having to try/catch everything. You get a tuple that has 2 elements - an error if one was thrown and the result if the function succeeded.
  3. Added a getMaxGasPrice function that makes multiple requests and then gets the maximum gas price from all of the successful responses. Failed responses are discarded.

Currently this will get gas prices from:
a. Etherchain
b. Eth Gas Station
c. (disabled) Honeycomb Gas Price Feed

I see the Honeycomb Gas Price Feed on Ropsten has not been updated in around 2 months so I've left that out for now, but the parts are there to enable it again.

TODO

I'll add tests if you're happy with what I've got so far

@andreogle andreogle self-assigned this Jun 16, 2020
@bbenligiray
Copy link
Member

Looks great so far. Do you think we would be calling the gas price APIs through the same interface we call the APIs (i.e. external adapter) in the future? Or would these calls go directly to axios as they do now?

@andreogle
Copy link
Member Author

I'm not sure. Would that be a single endpoint that the serverless function calls instead of 4-5 APIs directly? Would that be an endpoint we hosted?

Calling these APIs directly is nice because one or more could be down and we would still get an answer. If there was a single endpoint, that could be better down and we would have problems. Calling them directly also seems better for transparency.

@bbenligiray
Copy link
Member

bbenligiray commented Jun 16, 2020

No I mean if CoinAPI was hosting this node, the node would be calling the CoinAPI API along with these gas price APIs. I'm asking if we would have two classes of calls (the API call and gas price calls) or would they all be treated the same as much as possible?

@andreogle
Copy link
Member Author

I'm not really sure yet. I would need to see what the other class of call would look like. I don't really see a problem with having different ways of calling APIs at the moment. They will all share a common interface (in core/clients/http.ts) that would standardize at that level.

I updated the way it calls each API to remove a lot of the duplication. You simply need to declare each API at the top with a URL and onSuccess function that normalizes the response. I like this much more

@andreogle
Copy link
Member Author

These are also only the gas prices for mainnet. We'll need to think about what to do for testnets like ropsten

@bbenligiray
Copy link
Member

I'm not really sure yet. I would need to see what the other class of call would look like. I don't really see a problem with having different ways of calling APIs at the moment. They will all share a common interface (in core/clients/http.ts) that would standardize at that level.

Okay. I agree that it's early, I was mostly thinking out loud.

I updated the way it calls each API to remove a lot of the duplication. You simply need to declare each API at the top with a URL and onSuccess function that normalizes the response. I like this much more

Yeah that's pretty elegant.

These are also only the gas prices for mainnet. We'll need to think about what to do for testnets like ropsten

I like Chainlink's approach of not giving a fuck and using mainnet gas prices because

  • There are no Ropsten gas price APIs
  • Mainnet gas price tends to be higher than Ropsten and wasting gas is not an issue
  • Even if Ropsten gas price goes higher than mainnet gas price, not fulfilling requests on Ropsten is not an issue
  • It's good testing grounds for keeping up with mainnet gas prices

If it comes to that, we can always use our on-chain feed to override gas prices, both on mainnet and Ropsten.

@bbenligiray
Copy link
Member

bbenligiray commented Jun 16, 2020

It also needs a fallback gas price for when none of the sources return anything useful. I think that should be 40 gwei nowadays.

There's also a max gas price of 1000 gwei in the Chainlink version. It used to be 50 gwei (?), but then the gas price actually exceeded that and that wrecked all nodes. 1000 gwei is better than nothing because a gas price API can return a very high price and cause the node wallet to be drained instantly. On the other hand, we don't want to set something that we may want to change later. In that sense, 1000 gwei doesn't sound that bad.

Again, the Chainlink version adds 1 gwei to the gas price but that's pretty useless at the current prices. The extrapolation I added is an adaptive solution, but it requires the gas price history, so we can't use it here. So we can forget about it for now.

In the future, I'm hoping that we're going to be able to send these kind of configuration variables to the node over the chain. You make a transaction on ChainAPI, that emits a configuration event (variables are encoded in CBOR), the node reads the most recent configuration event at the start of each loop to set these.

@andreogle
Copy link
Member Author

Thanks. With the current setup, it will get mainnet gas prices from the 4 APIs and the data feed from ropsten and then take the maximum across the successful responses. I'll just leave it at that until it becomes a problem.

I quite like the idea of the node being able to configure itself based on onchain events. We can definitely work towards that eventually.

  1. Max gas price
    I definitely think we need a maximum gas price. I'll go with 1000 gwei for now.

  2. Fallback gas price
    Something would have to go catastrophically wrong for us to get 0 usable responses back. In any case, it doesn't hurt to have a fallback price. I'll go with 40 gwei for now.

  3. API configuration
    I also think we need a way to configure which APIs to use. Say anyblock adds back authentication at some point in the future. The logs would get spammed every minute with an error until we manage to get an update out that would remove it. It would be better to have this configurable somehow although I'm not sure what would be the ideal way to do this.

@bbenligiray
Copy link
Member

I'm okay with all of those.

I think gas price API configuration is quite doable, though it's probably not needed for now. We can configure that through events too, but updating that by redeploying the node (just as you would update integration specs) sounds more sensible to me.

@andreogle
Copy link
Member Author

Alright cool. I'll just make a note about API configuration so we don't forget

@andreogle
Copy link
Member Author

I think this is ready to go but waiting for #5 to be merged first

package.json Outdated Show resolved Hide resolved
return process.env.ETHEREUM_NETWORK || 'ropsten';
}

export function getProvider() {
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

@andreogle andreogle Jun 20, 2020

Choose a reason for hiding this comment

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

I want to avoid having any singleton global variables in the node. It looks like they have weird behaviour:

https://medium.com/tensult/aws-lambda-function-issues-with-global-variables-eb5785d4b876

I've been thinking about how best to manage state. I've been thinking of possibly using a simple state object that would store things we don't want to keep initializing repeatedly or passing around individually.

Something like this that would get created on each function call. Each property then gets set once and the state gets passed around to functions lower down:

const state = {
  provider: null,
  gasPrice: null,
};

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good

Copy link
Member

Choose a reason for hiding this comment

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

I think they are also using that weird behavior as a shitty way to persist state between function invocations
https://engineering.flosports.tv/persistent-data-in-lambda-it-works-ca0c1b25879e

1. Change all gas prices to be denominated in Wei instead of Gwei

2. Add a global state object intended to reduce calls to the Ethereum
node and re-use objects

3. Add utils for Wei/Gwei conversions
// Do this upfront to reduce potential extra calls later on
const [err, network] = await go(provider.getNetwork());
if (err) {
// TODO: not sure how exactly to handle errors here
Copy link
Member

Choose a reason for hiding this comment

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

I was thinking that we could use two providers simultaneously (both to detect requests and make transactions to fulfill them), but Airnode depends on the Ethereum node's version of truth and if we try to use two Ethereum nodes with different versions of truth, we may end up with weird problems. So in a single sleep-wake cycle, the node should decide to use a provider (e.g. the one with the higher blockNumber) and stick with it. Then, it can retry provider calls until the serverless function times out (in 60 seconds total), as we can't really skip any of the provider calls.

Copy link
Member

Choose a reason for hiding this comment

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

But yes, I think all provider calls need a wrapper that handles what provider to use

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I think that sounds like a good idea. I was wondering about how we could control the function time out, but I see serverless provides that for us. Yeah I'll need to implement the ability to retry provider calls in a separate PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants