Skip to content

LW-12681 Coingecko token price API #1867

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

Merged
merged 1 commit into from
May 20, 2025
Merged

LW-12681 Coingecko token price API #1867

merged 1 commit into from
May 20, 2025

Conversation

iccicci
Copy link
Contributor

@iccicci iccicci commented May 13, 2025

Checklist

  • JIRA - LW-12681
  • Proper tests implemented
  • Screenshots added.

Proposed solution

Explain how does this PR solves the problem stated in JIRA ticket.
You can also enumerate different alternatives considered while approaching this task.

Testing

Describe here, how the new implementation can be tested.
Provide link or briefly describe User Acceptance Criteria/Tests that need to be met

Screenshots

Attach screenshots here if implementation involves some UI changes

@lace-bot
Copy link
Collaborator

lace-bot commented May 13, 2025

Allure Report

allure-report-publisher generated test report!

processReports: ✅ test report for 3174dc33

passed failed skipped flaky total result
Total 33 0 4 0 37

@iccicci iccicci force-pushed the feat/LW-12681-token-price branch 11 times, most recently from fcb3a0b to 8e077d9 Compare May 16, 2025 06:45
@iccicci iccicci marked this pull request as ready for review May 16, 2025 06:47
@iccicci iccicci requested a review from a team as a code owner May 16, 2025 06:47
@iccicci iccicci force-pushed the feat/LW-12681-token-price branch from 8e077d9 to f52263f Compare May 16, 2025 07:14
Copy link
Contributor

@szymonmaslowski szymonmaslowski left a comment

Choose a reason for hiding this comment

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

Great Job!

() => ({
getTokenPrice: (assetId: Wallet.Cardano.AssetId): TokenPrice | undefined => {
const tokenPrice = tokenPrices?.tokens?.get(assetId);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const tokenPrice = tokenPrices?.tokens?.get(assetId);
const tokenPrice = tokenPrices.tokens.get(assetId);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c removes the second question mark; removing the first one as well makes the extension to not load.

const tokenPrice = tokenPrices?.tokens?.get(assetId);
// Actually track the price only for token in Cardano mainnet, otherwise just do nothing
const trackPrice = () =>
store && store.currentChain && store.currentChain.networkId === Wallet.Cardano.NetworkId.Mainnet
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
store && store.currentChain && store.currentChain.networkId === Wallet.Cardano.NetworkId.Mainnet
store.currentChain.networkId === Wallet.Cardano.NetworkId.Mainnet

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressing this comment I did a refactoring which changes the layout of the same check.
Anyway (as far as I understood) we still need to check currentChain to be defined as it could be not in case the wallet is in bitcoin mode.

// Actually track the price only for token in Cardano mainnet, otherwise just do nothing
const trackPrice = () =>
store && store.currentChain && store.currentChain.networkId === Wallet.Cardano.NetworkId.Mainnet
? trackCardanoTokenPrice(assetId).catch(console.error)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use a logger instead of the console in all the occurences.
import { logger } from '@lace/common';

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c should solve this

Comment on lines 59 to 60
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};
Copy link
Contributor

Choose a reason for hiding this comment

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

A nitpick. Feel free to ignore.

Suggested change
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};
: () => void 0;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressing this comment I did a little refactoring which makes this comment no longer required.

import { config } from '@src/config';

/** Shortcut for the token prices subject type. */
type TokenPriceSubject = Subject<{ tokens: TokenPrices } & Status>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
type TokenPriceSubject = Subject<{ tokens: TokenPrices } & Status>;
type TokenPriceSubject = BehaviorSubject<{ tokens: TokenPrices } & Status>;

This way you don't need the local priceData variable because you can access the last emited value: priceSubject$.value;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just an interface: the Observable used in the implementation is (ATM) actually a BehaviorSubject.
In case in the future we need to change the actual implementation, using the ancestor class Subject reduces the chances we need to change this interface type as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I was not clear enough. I meant:

// have a local BehaviorSubject
const price$ = new BehaviorSubject<{ tokens: TokenPrices } & Status>();
...
const updatePriceData = (assetId: string, data?: PriceData) => {
  const now = Date.now();
  const nextPriceData = {
    ...price$.value,  // access current value from the subject
    [assetId]: data ? [now, data] : [now],
  };
  emitPrices(nextPriceData);
...
const initCardanoTokenPrices = () => {
  storage.local
    .get(CACHE_KEY)
    .then((data) => {
      const initialPriceData = data[CACHE_KEY] || {};
      emitPrices(initialPriceData);
      fetchPrices();
  return price$;  // expose local subject
};

Then it could be assigned to coinPrices.tokenSubject$ in utilityService.ts

const coinPrices: CoinPrices = {
  tokenPrices$: initCardanoTokenPrices(),
};

But again, this is just a suggestion. I am fine with either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice suggestion! Thank you.

5b33fc6 should solve this.

updatePriceData(assetId, cachedData);

try {
const url = `https://coingecko.live-mainnet.eks.lw.iog.io/api/v3/onchain/networks/cardano/tokens/${assetId}/pools`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider providing this URL via env variable

Suggested change
const url = `https://coingecko.live-mainnet.eks.lw.iog.io/api/v3/onchain/networks/cardano/tokens/${assetId}/pools`;
const url = `${process.env.TOKEN_PRICES_URL}/${assetId}/pools`;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c should solve this

setTimeout(fetchPrices, TOKEN_PRICE_CHECK_INTERVAL);
};

export const initCardanoTokenPrices = (tokenPrices$: TokenPriceSubject) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider returning the local priceSubject$ instead of passing it from outside. This way the coinPrices.tokenPrices$ will get the proper types.

Copy link
Contributor Author

@iccicci iccicci May 19, 2025

Choose a reason for hiding this comment

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

There is any locally defined priceSubject$, the only one defined is tokenSubject$ in utilityService.ts and it is defined as property of const coinPrices. The local variable priceSubject$ is just a reference to coinPrices.tokenSubject$ from utilityService.ts.
The reasons why I chosen this approach is to isolate the token price stuff in a dedicated file rather than implementing everything inside utilityService.ts which is already more than 300 lines.
We can evaluate importing the content of cardanoTokenPrices.ts into utilityService.ts, that would significantly simplify this point and some other; probably the type discussed in this comment will be completely removed as well.

Comment on lines 56 to 66
const trackPrice = () =>
store && store.currentChain && store.currentChain.networkId === Wallet.Cardano.NetworkId.Mainnet
? trackCardanoTokenPrice(assetId).catch(console.error)
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};

// If the price for this token was never fetched, wee need to track it
if (!tokenPrice) {
trackPrice();

return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a suggestion. I think it would be more readable written as below. Currently you return an empty function or a promise - not sure why empty function instead of e.g. undefined (this empty/noop function is not getting called later).

edit: Part of this check will be even simpler networkId === Wallet.Cardano.NetworkId.Mainnet when my previous comment is applied

Suggested change
const trackPrice = () =>
store && store.currentChain && store.currentChain.networkId === Wallet.Cardano.NetworkId.Mainnet
? trackCardanoTokenPrice(assetId).catch(console.error)
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};
// If the price for this token was never fetched, wee need to track it
if (!tokenPrice) {
trackPrice();
return undefined;
}
// If the price for this token was never fetched, wee need to track it
// but only for token in Cardano mainnet
if (!tokenPrice && store?.currentChain?.networkId === Wallet.Cardano.NetworkId.Mainnet) {
trackCardanoTokenPrice(assetId).catch(console.error)
return undefined;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function trackPrice is called twice, next call is a few lines later.
I chosen this approach to reduce the code repetition as much as possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

I know you call trackPrice, but not the function it returns here in line 60, so why do you need to return a function instead of e.g. undefined? Also this will be greatly simplified with #1867 (comment)

Copy link
Contributor Author

@iccicci iccicci May 19, 2025

Choose a reason for hiding this comment

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

Mhh that is not correct.
trackPrice is a function which has trackCardanoTokenPrice(assetId).catch(console.error) as body if the wallet is in Cardano mainnet or an empty body in other networks: i.e. the function returned in line 60 is called (when the wallet is in preprod, preview or any bitcoin network)

Edit: please ignore this message, it is completely wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for insisting @przemyslaw-wlodek !
My previous message is probably a bias due to some previous implementations I did...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c should solve this

const tokenPrices = useObservable(coinPrices.tokenPrices$);
const adaPrices = useObservable(coinPrices.adaPrices$);
const bitcoinPrices = useObservable(coinPrices.bitcoinPrices$);
const store = useWalletStore();
Copy link
Contributor

Choose a reason for hiding this comment

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

Please consider using a selector. You're passing whole store as a dependency for useMemo which will constantly recompute.

Suggested change
const store = useWalletStore();
const networkId = useWalletStore(state => state.currentChain?.networkId);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c should solve this

price: isAdaCurrency ? 1 : adaPrices?.prices?.[fiatCurrency.code.toLowerCase() as ADAPricesKeys],
priceVariationPercentage24h:
adaPrices?.prices?.[`${fiatCurrency.code.toLowerCase()}_24h_change` as ADAPricesKeys] || 0
}),
[adaPrices?.prices, fiatCurrency.code, isAdaCurrency]
[adaPrices?.prices, fiatCurrency.code, isAdaCurrency, tokenPrices?.tokens, trackCardanoTokenPrice, store]
Copy link
Contributor

Choose a reason for hiding this comment

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

As described above, please pass just networkId to dependencies (instead of store

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c should solve this

Comment on lines 59 to 62
const [lastFetchTime, cachedData] = priceData[assetId] || [];

// If recently fetched, do nothing
if (typeof lastFetchTime === 'number' && lastFetchTime > Date.now() - TOKEN_PRICE_CHECK_INTERVAL) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a suggestion:

Suggested change
const [lastFetchTime, cachedData] = priceData[assetId] || [];
// If recently fetched, do nothing
if (typeof lastFetchTime === 'number' && lastFetchTime > Date.now() - TOKEN_PRICE_CHECK_INTERVAL) return;
const [lastFetchTime, cachedData] = priceData[assetId] || [0];
// If recently fetched, do nothing
if (lastFetchTime > Date.now() - TOKEN_PRICE_CHECK_INTERVAL) return;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

e101d0c should solve this

updatePriceData(assetId, cachedData);

try {
const url = `https://coingecko.live-mainnet.eks.lw.iog.io/api/v3/onchain/networks/cardano/tokens/${assetId}/pools`;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest moving core part of the URL to some constant + a function that accepts assetId and returns full URL.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressing this comment I did a little refactoring which could make this comment no longer required.

Copy link
Contributor

@przemyslaw-wlodek przemyslaw-wlodek left a comment

Choose a reason for hiding this comment

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

GJ! 🚀

Copy link
Contributor

@szymonmaslowski szymonmaslowski left a comment

Choose a reason for hiding this comment

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

GJ!

@iccicci iccicci force-pushed the feat/LW-12681-token-price branch from 5b33fc6 to 3174dc3 Compare May 20, 2025 09:34
Copy link

@iccicci iccicci merged commit 3174dc3 into main May 20, 2025
31 checks passed
@iccicci iccicci deleted the feat/LW-12681-token-price branch May 20, 2025 09:57
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