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 chainId support to TokenRatesController #476

Merged
merged 13 commits into from
Jul 5, 2021

Conversation

wachunei
Copy link
Member

No description provided.

Comment on lines +173 to +180
set chainId(_chainId: string) {
!this.disabled && safelyExecute(() => this.updateExchangeRates());
}

get chainId() {
throw new Error('Property only used for setting');
}

Copy link
Member Author

Choose a reason for hiding this comment

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

When chainId changes, run update if not disabled.

@@ -170,21 +214,43 @@ export class TokenRatesController extends BaseController<
* @returns Promise resolving when this operation completes
*/
async updateExchangeRates() {
if (this.tokenList.length === 0) {
if (this.tokenList.length === 0 || this.disabled) {
Copy link
Member Author

Choose a reason for hiding this comment

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

If this was called directly, nothing was stopping it from being run. What is the intended behavior originally: should disable just disable tokenList/chainId changes?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think the disabled property was used in this controller 🤔

Like many aspects of the BaseController, some controllers are written to use it and some ignore it, or break when you try to use it. The config is similar - it's built to be changeable at runtime, but many controllers are written assuming that never happens, and they break if you try it.

It doesn't hurt to add a check for disabled here but I'm not sure it helps either, unless I'm missing something. This awkwardness with the API is one of the things BaseControllerV2 is meant to address.

Comment on lines 223 to 234
let chainSlug;

if (!chainSlugIdentifier) {
try {
chainSlug = await this.getChainSlugIdentifier(chainId);
if (!chainSlug) {
this.update({ chainSlugIdentifier: null });
}
} catch {
this.update({ chainSlugIdentifier: undefined });
}
}
Copy link
Member Author

@wachunei wachunei May 25, 2021

Choose a reason for hiding this comment

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

Here's the main question.

Since now there is an extra chainId parameter, we need to hit two endpoints

  1. Get the chain slug given the chain id
  2. Use the chain slug to recover the prices, like it was done before

Should we "remember" the slug? There are three cases:

  1. The request fails
  2. The request is successful but the requested chain id is not listed on the api
  3. The request is successful and we have a slug we can use

If we don't have the slug stored, this will always make two request when fetching prices.
If we store the slug, we must be able to define when to invalidate it in case the API changes, for example: it supports a new chain which we stored as unsupported

Copy link
Contributor

@adonesky1 adonesky1 May 28, 2021

Choose a reason for hiding this comment

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

Maybe we store the slug when we are able to get a valid response and then wrap the fetchExchangeRate in a try/catch in case the slug becomes invalid and attempt to refetch it if that's the case?

Copy link
Member Author

Choose a reason for hiding this comment

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

How would we manage the case the slug does not exist and later it does? Example: We would store it as null, so we don't even attempt to fetch the rates, but if the slug is supported in the future, how would we invalidate that null?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I'm not understanding but if the chainSlugIdentifier is null we try will try to fetch it again as the code stands? Its not ideal because we are not able to cut out that extra network request by storing it as unsupported but I can't think of a way around that right now - unless they only update the api when they change the endpoint (v3 -> v4)?

Copy link
Member Author

Choose a reason for hiding this comment

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

What we are doing on swaps-controller to hold values for a certain amount of time is to store pairs (value, timestamp) and compare timestamp, which represents the last time value was fetched, to a time threshold.

This way if value is null but Date.now() - timestamp is less than a threshold, we consider the network is not supported, otherwise we attempt to fetch the slug and update the pair (value, timestamp = Date.now()).

The same works when value is a-network-slug that might become unsupported, but we check only after we are over the time threshold.

Does it make sense?

Copy link
Contributor

@adonesky1 adonesky1 May 28, 2021

Choose a reason for hiding this comment

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

Yes that makes sense. So we're weighing the trade-offs of optimizing for fewer network requests and, if we choose to go that route, possibly not fetching exchange rates when they are available until the unsupported flag expires according to whatever time threshold we set. I guess I don't have a strong intuition of which way to go with this. What do you think @Gudahtt?

@wachunei
Copy link
Member Author

Another issue is that updateExchangeRates will run twice: when chainId changes and when the token list changes.

This could be avoided if assets had the chain_id as property, which would also work when listing assets from different chains in the same view.

Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

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

I left some initial comments here and on Notion. If we decide to pursue caching the API response, some of these comments might end up being obsolete though.

},
};

function getUpdatedSupportedChains(
Copy link
Member

Choose a reason for hiding this comment

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

This function seems.... odd. It just combines the two parameters? Seems to have limited utility at first glance.

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, it actually merges a nested property of state because BaseController.update is shallow merge, would override other chainIds values.

@@ -109,9 +153,16 @@ export class TokenRatesController extends BaseController<
disabled: true,
interval: 180000,
nativeCurrency: 'eth',
chainId: '1',
Copy link
Member

Choose a reason for hiding this comment

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

We should not have a hard-coded default network - we should find out for sure what the initial network is. This could be disastrous, if this controller thinks the user is on a different network until he first switch.

Copy link
Member

Choose a reason for hiding this comment

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

Kinda just as bad to do this with the native currency as well to be honest, though that's a pre-existing issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

This was done in order to provide backwards compatibility, since the controller was implicitly always hard coded to chainId: 1 (because ethereum was the hardcoded endpoint).

Will look into how we should handle this properly then.

Copy link
Member

@Gudahtt Gudahtt Jun 9, 2021

Choose a reason for hiding this comment

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

I've handled this elsewhere by passing in the current network state as a constructor parameter, or passing in a "getNetworkState" function which can be used to check the initial state.

Copy link
Member Author

Choose a reason for hiding this comment

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

Cool, will look it up!

@@ -137,6 +192,22 @@ export class TokenRatesController extends BaseController<
}, this.config.interval);
}

set chainId(chainId: string) {
Copy link
Member

Choose a reason for hiding this comment

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

We've been trying to move away from using setters for this kinda thing, and instead preferring methods. If this was a method (like setChainId) it's clear that setting it has a side-effect, and the caller can look to the method definition to see what it does. It might even have a nice JSDoc comment (which gets shown inline via intellisense on some editors).

But with a setter like this... it's ambiguous to the caller what effect setting this has. It might have a setter, or it might be referenced anywhere in this controller or in other modules, or both.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is there an example of the right way to do this?
I've been using it as a side-effect handler of controller.config({...})

Copy link
Member

Choose a reason for hiding this comment

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

Ah, right, I forgot configure worked that way. Ah well. Maybe this is fine until switching to the new BaseControllerV2 API then. Not a huge deal.


const chainCache = supportedChains[chainId];
// supportedChain has not been created, we skip this execution
if (!chainCache) {
Copy link
Member

Choose a reason for hiding this comment

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

This branch seems like it should be impossible to reach 🤔 Since supportedChains[chainId] is set immediately after switching networks.

Copy link
Member Author

@wachunei wachunei Jun 9, 2021

Choose a reason for hiding this comment

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

The race condition I was thinking of is:
Given this state:

  • chainCache.slug is null
  • tokens is a list of tokens

Since we have now two listeners (on assets change and on network change), could it be the case this method executes when the assets change (because of a network change) instead of before the network change itself?

There is an open question on the technical proposal about this

Copy link
Member

Choose a reason for hiding this comment

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

The thing is, if the assets change before the chain ID does, this would still be referencing the old chain ID. I don't believe it's possible for this to have a new chain ID without the setter for chainId running first.

Copy link
Member Author

Choose a reason for hiding this comment

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

I see it now, this check is unnecessary 👍

@wachunei wachunei marked this pull request as ready for review June 23, 2021 19:46
@wachunei wachunei requested a review from a team as a code owner June 23, 2021 19:46
newContractExchangeRates[address] = undefined;
});
} else {
const pairs = this.tokenList.map((token) => token.address).join(',');
Copy link
Contributor

@adonesky1 adonesky1 Jun 23, 2021

Choose a reason for hiding this comment

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

[nit] @wachunei I know you didn't name this const but it seems slightly misnamed to me? It's a list of tokens that are each one side of a pair with the native currency right?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, this is just a concatenation of token addresses joined by a comma, from something like [{ address: '0x1...', ...restOfTokenObject1 }, { address: '0x2...', ...restOfTokenObject2 }] into "0x1...,0x2...", which is what CoinGecko takes in as query param for the API call.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree is not a great name! But you know, naming stuff is hard hahahaha

Copy link
Member Author

Choose a reason for hiding this comment

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

pairs should be addresses, sorry for necroposting

@adonesky1
Copy link
Contributor

@wachunei have you tested this at all wired up in the extension?

@wachunei
Copy link
Member Author

@wachunei have you tested this at all wired up in the extension?

No, I could test it with the mobile app, have not done it yet tho.

@adonesky1
Copy link
Contributor

No, I could test it with the mobile app, have not done it yet tho.

Ok I'm going to give it a go (on the extension) if there are no objections.

@wachunei
Copy link
Member Author

That would be great! Thank you @adonesky1

@adonesky1
Copy link
Contributor

@Gudahtt, @brad-decker is there any roadmap for migrating the extension towards use of the AssetsController? Or, if not, in order to integrate the TokenRatesController in the extension will we need to modify this controller to expect alternative listeners that correspond with where/how token data is in the extension?

@Gudahtt
Copy link
Member

Gudahtt commented Jun 24, 2021

It is planned, but only in our imaginations at this point. No concrete plan yet.

@wachunei
Copy link
Member Author

wachunei commented Jul 1, 2021

@adonesky1 I tested in on the app and it is working, opened a draft PR of the integration here MetaMask/metamask-mobile#2863

Screen Shot 2021-07-01 at 10 41 13Screen Shot 2021-07-01 at 10 40 55Screen Shot 2021-07-01 at 10 40 27

brad-decker
brad-decker previously approved these changes Jul 2, 2021
Copy link
Contributor

@brad-decker brad-decker left a comment

Choose a reason for hiding this comment

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

Looks good to me!One non blocking Q

supportedChains: SupportedChainsCache;
}

const COINGECKO_API = {
Copy link
Contributor

Choose a reason for hiding this comment

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

[Q] Is this a common pattern in the controller code base, having the UPPER_SNAKE_CASE (which I read as a constant) have methods on it? Non blocking just curious

Copy link
Member Author

Choose a reason for hiding this comment

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

Great question, actually I don't know. It has landed to this point because it started as a constant plain object but then it mutated to an object with methods for convenience. Happy to adopt convention/pattern before moving on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just CoinGeckoApi or some such for now. I could see it living in its own platforms file when there are more than one platform we use for this. For now its fine here. Its also fine to let it in like this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed on 1aa1bfb (#476)

@adonesky1
Copy link
Contributor

Ok I'm going to give it a go (on the extension) if there are no objections.

@wachunei sorry I never followed up on this. This work is blocked by a need to migrating the extension to use of the AssetController. I began to plan this work and then got pulled in another direction. This obviously shouldn't be a blocker though. I will probably try to pick that work up again soon after this has merged.

@wachunei
Copy link
Member Author

wachunei commented Jul 2, 2021

@adonesky1 No worries.

This is also in a weird state because of develop moving on to v11. Do we maintain minor versions of different major versions in parallel?

  • If I revert e4bc0d1 on this branch, this is 10.x compatible (our current mobile target version) and this could be released as 10.3
  • If I merge as is, this would be 11 compatible and won't work with current mobile code, so this is blocked until we upgrade to 11

Thoughts?

@Gudahtt
Copy link
Member

Gudahtt commented Jul 2, 2021

Generally I'd recommend always targeting the main branch with all bug fixes/features/etc. and separately backporting if necessary. If you're going to be backporting, you'll typically want the change in both places anyway, and it's just easier to land it on main first.

There are some basic instructions for how to prepare a backport release in the README. Basically you'd create a 10.x branch off of v10.1 (the most recent non-deprecated v10 release), create a release branch based there, cherry-pick the change you want to backport, and do the usual release bump and changelog update. Whether you want to bother with backporting is up to you/the mobile team. It's a tradeoff between taking the extra effort to backport v.s. waiting for v11 to land. It's probably better to land v11 and save yourself the effort unless time constraints make that difficult.

@wachunei
Copy link
Member Author

wachunei commented Jul 2, 2021

Thanks @Gudahtt, I'll merge this and ask the mobile team about having this included in v11 or backporting.

@wachunei wachunei merged commit f23f71e into main Jul 5, 2021
@wachunei wachunei deleted the feature/token-rate-chainid branch July 5, 2021 13:54
@wachunei
Copy link
Member Author

wachunei commented Jul 5, 2021

Just a reminder this would be a major version bump since TokenRatesController constructor now expects a onNetworkStateChange listener.

@wachunei wachunei mentioned this pull request Jul 7, 2021
@brad-decker brad-decker mentioned this pull request Jul 7, 2021
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.

4 participants