-
-
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
Changes from 3 commits
84ac2fd
b720274
efddcdf
8ad4be4
f3524f6
4830c33
1774012
0db47bd
204ff43
bca002a
e4bc0d1
1aa1bfb
120f654
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ import { toChecksumAddress } from 'ethereumjs-util'; | |
import BaseController, { BaseConfig, BaseState } from '../BaseController'; | ||
import { safelyExecute, handleFetch } from '../util'; | ||
|
||
import type { NetworkState } from '../network/NetworkController'; | ||
import type { AssetsState } from './AssetsController'; | ||
import type { CurrencyRateState } from './CurrencyRateController'; | ||
|
||
|
@@ -46,9 +47,14 @@ export interface Token { | |
export interface TokenRatesConfig extends BaseConfig { | ||
interval: number; | ||
nativeCurrency: string; | ||
chainId: string; | ||
tokens: Token[]; | ||
} | ||
|
||
interface ContractExchangeRates { | ||
[address: string]: number | undefined; | ||
} | ||
|
||
/** | ||
* @type TokenRatesState | ||
* | ||
|
@@ -57,7 +63,8 @@ export interface TokenRatesConfig extends BaseConfig { | |
* @property contractExchangeRates - Hash of token contract addresses to exchange rates | ||
*/ | ||
export interface TokenRatesState extends BaseState { | ||
contractExchangeRates: { [address: string]: number }; | ||
contractExchangeRates: ContractExchangeRates; | ||
chainSlugIdentifier: string | null | undefined; | ||
} | ||
|
||
/** | ||
|
@@ -72,8 +79,21 @@ export class TokenRatesController extends BaseController< | |
|
||
private tokenList: Token[] = []; | ||
|
||
private getPricingURL(query: string) { | ||
return `https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`; | ||
private getPricingURL(chainSlugIdentifier: string, query: string) { | ||
return `https://api.coingecko.com/api/v3/simple/token_price/${chainSlugIdentifier}?${query}`; | ||
} | ||
|
||
private async getChainSlugIdentifier( | ||
chainId: string, | ||
): Promise<string | undefined> { | ||
const platforms: [ | ||
{ id: string; chain_identifier: number | null }, | ||
] = await handleFetch('https://api.coingecko.com/api/v3/asset_platforms'); | ||
const chain = platforms.find( | ||
({ chain_identifier }) => | ||
chain_identifier !== null && String(chain_identifier) === chainId, | ||
); | ||
return chain?.id; | ||
} | ||
|
||
/** | ||
|
@@ -94,13 +114,17 @@ export class TokenRatesController extends BaseController< | |
{ | ||
onAssetsStateChange, | ||
onCurrencyRateStateChange, | ||
onNetworkStateChange, | ||
}: { | ||
onAssetsStateChange: ( | ||
listener: (assetState: AssetsState) => void, | ||
) => void; | ||
onCurrencyRateStateChange: ( | ||
listener: (currencyRateState: CurrencyRateState) => void, | ||
) => void; | ||
onNetworkStateChange: ( | ||
listener: (networkState: NetworkState) => void, | ||
) => void; | ||
}, | ||
config?: Partial<TokenRatesConfig>, | ||
state?: Partial<TokenRatesState>, | ||
|
@@ -110,9 +134,13 @@ export class TokenRatesController extends BaseController< | |
disabled: true, | ||
interval: 180000, | ||
nativeCurrency: 'eth', | ||
chainId: '1', | ||
tokens: [], | ||
}; | ||
this.defaultState = { contractExchangeRates: {} }; | ||
this.defaultState = { | ||
contractExchangeRates: {}, | ||
chainSlugIdentifier: undefined, | ||
}; | ||
this.initialize(); | ||
this.configure({ disabled: false }, false, false); | ||
onAssetsStateChange((assetsState) => { | ||
|
@@ -121,6 +149,10 @@ export class TokenRatesController extends BaseController< | |
onCurrencyRateStateChange((currencyRateState) => { | ||
this.configure({ nativeCurrency: currencyRateState.nativeCurrency }); | ||
}); | ||
onNetworkStateChange(({ provider }) => { | ||
const { chainId } = provider; | ||
this.configure({ chainId }); | ||
}); | ||
this.poll(); | ||
} | ||
|
||
|
@@ -138,6 +170,14 @@ export class TokenRatesController extends BaseController< | |
}, this.config.interval); | ||
} | ||
|
||
set chainId(_chainId: string) { | ||
!this.disabled && safelyExecute(() => this.updateExchangeRates()); | ||
} | ||
|
||
get chainId() { | ||
throw new Error('Property only used for setting'); | ||
} | ||
|
||
Comment on lines
+220
to
+227
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When |
||
/** | ||
* Sets a new token list to track prices | ||
* | ||
|
@@ -157,11 +197,15 @@ export class TokenRatesController extends BaseController< | |
/** | ||
* Fetches a pairs of token address and native currency | ||
* | ||
* @param chainSlugIdentifier - Chain string identifier | ||
* @param query - Query according to tokens in tokenList and native currency | ||
* @returns - Promise resolving to exchange rates for given pairs | ||
*/ | ||
async fetchExchangeRate(query: string): Promise<CoinGeckoResponse> { | ||
return handleFetch(this.getPricingURL(query)); | ||
async fetchExchangeRate( | ||
chainSlugIdentifier: string, | ||
query: string, | ||
): Promise<CoinGeckoResponse> { | ||
return handleFetch(this.getPricingURL(chainSlugIdentifier, query)); | ||
} | ||
|
||
/** | ||
|
@@ -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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the 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 |
||
return; | ||
} | ||
const newContractExchangeRates: { [address: string]: number } = {}; | ||
const { nativeCurrency } = this.config; | ||
const pairs = this.tokenList.map((token) => token.address).join(','); | ||
const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency.toLowerCase()}`; | ||
const prices = await this.fetchExchangeRate(query); | ||
this.tokenList.forEach((token) => { | ||
const address = toChecksumAddress(token.address); | ||
const price = prices[token.address.toLowerCase()]; | ||
newContractExchangeRates[address] = price | ||
? price[nativeCurrency.toLowerCase()] | ||
: 0; | ||
}); | ||
const { nativeCurrency, chainId } = this.config; | ||
const { chainSlugIdentifier } = this.state; | ||
|
||
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 commentThe 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
Should we "remember" the slug? There are three cases:
If we don't have the slug stored, this will always make two request when fetching prices. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. What we are doing on This way if The same works when Does it make sense? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
|
||
const newContractExchangeRates: ContractExchangeRates = {}; | ||
if (!chainSlug) { | ||
this.tokenList.forEach((token) => { | ||
const address = toChecksumAddress(token.address); | ||
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 commentThe 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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency.toLowerCase()}`; | ||
const prices = await this.fetchExchangeRate(chainSlug, query); | ||
this.tokenList.forEach((token) => { | ||
const address = toChecksumAddress(token.address); | ||
const price = prices[token.address.toLowerCase()]; | ||
newContractExchangeRates[address] = price | ||
? price[nativeCurrency.toLowerCase()] | ||
: 0; | ||
}); | ||
} | ||
this.update({ contractExchangeRates: newContractExchangeRates }); | ||
} | ||
} | ||
|
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
(becauseethereum
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!