-
-
Notifications
You must be signed in to change notification settings - Fork 203
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
Conversation
set chainId(_chainId: string) { | ||
!this.disabled && safelyExecute(() => this.updateExchangeRates()); | ||
} | ||
|
||
get chainId() { | ||
throw new Error('Property only used for setting'); | ||
} | ||
|
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/assets/TokenRatesController.ts
Outdated
let chainSlug; | ||
|
||
if (!chainSlugIdentifier) { | ||
try { | ||
chainSlug = await this.getChainSlugIdentifier(chainId); | ||
if (!chainSlug) { | ||
this.update({ chainSlugIdentifier: null }); | ||
} | ||
} catch { | ||
this.update({ chainSlugIdentifier: undefined }); | ||
} | ||
} |
There was a problem hiding this comment.
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
- Get the chain slug given the chain id
- Use the chain slug to recover the prices, like it was done before
Should we "remember" the slug? There are three cases:
- The request fails
- The request is successful but the requested chain id is not listed on the api
- 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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
Another issue is that 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. |
There was a problem hiding this 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.
src/assets/TokenRatesController.ts
Outdated
}, | ||
}; | ||
|
||
function getUpdatedSupportedChains( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/assets/TokenRatesController.ts
Outdated
@@ -109,9 +153,16 @@ export class TokenRatesController extends BaseController< | |||
disabled: true, | |||
interval: 180000, | |||
nativeCurrency: 'eth', | |||
chainId: '1', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
src/assets/TokenRatesController.ts
Outdated
@@ -137,6 +192,22 @@ export class TokenRatesController extends BaseController< | |||
}, this.config.interval); | |||
} | |||
|
|||
set chainId(chainId: string) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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({...})
There was a problem hiding this comment.
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.
src/assets/TokenRatesController.ts
Outdated
|
||
const chainCache = supportedChains[chainId]; | ||
// supportedChain has not been created, we skip this execution | ||
if (!chainCache) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
isnull
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍
newContractExchangeRates[address] = undefined; | ||
}); | ||
} else { | ||
const pairs = this.tokenList.map((token) => token.address).join(','); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
@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. |
Ok I'm going to give it a go (on the extension) if there are no objections. |
That would be great! Thank you @adonesky1 |
@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? |
It is planned, but only in our imaginations at this point. No concrete plan yet. |
@adonesky1 I tested in on the app and it is working, opened a draft PR of the integration here MetaMask/metamask-mobile#2863 |
There was a problem hiding this 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
src/assets/TokenRatesController.ts
Outdated
supportedChains: SupportedChainsCache; | ||
} | ||
|
||
const COINGECKO_API = { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed on 1aa1bfb
(#476)
@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. |
@adonesky1 No worries. This is also in a weird state because of develop moving on to
Thoughts? |
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 There are some basic instructions for how to prepare a backport release in the README. Basically you'd create a |
Thanks @Gudahtt, I'll merge this and ask the mobile team about having this included in v11 or backporting. |
Just a reminder this would be a major version bump since |
No description provided.