Mixing On- and Off-Chain Data

Mixing On- and Off-Chain Data

Most people think in dollars. While I know plenty of people trying to accumulate greater nominal amounts of their favorite platform/project tokens, the majority also keep track of its market USD value and size their trades based on this. What does this mean as a developer? -> Show the people want they want to see.

I originally implemented my Raider Vault app in the context of the deposit tokens for each vault. The UI would display the total assets, user holdings, and amounts to deposit in units of the token being deposited. This is useful so that you can see your percentage of the vault, and know how many tokens you have. A partial reason I did this was because it's easier, especially in this case.

New projects = DEXs and no price APIs

If you want to get the price of mainstream crypto assets and load them in your app, you can easily send a call to a public REST API with price data (e.g. CoinGecko, CoinMarketCap, etc.) and use that for display or your internal calculations. There are some inconveniences with this (e.g. rate-limiting and paid plans for production apps), but it's fairly straightforward.

However, for newer projects, like Crypto Raiders, these sources are either not available or less reliable (fewer data sources if it's not listed by CEX). In fact, Crypto Raider tokens (RAIDER and AURUM) are currently only available for trading through SushiSwap on Polygon Mainnet.

Using LP pools and base asset prices

DEX contracts live on-chain. The one we're interested here is the liquidity pool (LP) token contract. They don't provide the USD value of the tokens being swapped, only the relative value of the two assets in the pool.

Luckily, most pools on a DEX are between a specific project token and the network's base token and/or mainstream tokens. This means that we can get the market USD value of the "base" token from one of the external APIs and derive an estimated USD value for the project token using the relative values from the LP token. Hooray!

Side note: If a token is available on multiple DEXs, then the derived value may vary between the two. However, arb bots will likely keep them close enough to use one or the other as an estimate.

A big caveat with using Uniswap V2 LP pools for getting the token price is that it is not secure inside of smart contracts. In that case, an oracle is required. Here, however, we are just getting the data to display information so it's fine.

Implementing USD prices in my Raider Vault app

There isn't much to calling the off-chain price feeds from the various providers. You can use any REST client on in your frontend to do, some popular ones are restful.js and axios. I've used axios for awhile and am not trying to learn too many new things at once.

const getMaticPrice = async () => {
    const response = await axios.get(matic_price_url);
    const maticPrice = parseFloat(response.data['matic-network']['usd'])

    dispatch({token: 'MATIC', field: 'PRICE', newValue: maticPrice}); // Using a Reducer for the market data state given the number of variables

The on-chain contract calls to get data are straightforward too. I'm not going to list all of them, but here is how the USD price for RAIDER is estimated:

const raiderSlpToken = new ethers.Contract(raiderSlpAddress, raiderSlpAbi, provider);
const reserves = await raiderSlpToken.getReserves();
const rSlpMaticReserves = reserves[0];
const rSlpRaiderReserves = reserves[1];
const currentRaiderPrice = rSlpMaticReserves / rSlpRaiderReserves * market.matic.price;

The only challenge with getting this setup correctly is dealing with the async functions. As you can see, both the MATIC price feed and the on-chain data calls have await statements. While the single API response is likely faster than the on-chain calls, it's not guaranteed. A further complexity is that both React useState and useReducer have asynchronous data updates to the state variables. Even if the API returns sooner, the data might not be available to the other function yet. Therefore, there are three things that I've implemented to avoid this problem.

Use try/catch statements around the async calls to avoid the app crashing from missed time. Force the MATIC price feed to get called first by (ab)using the useLayoutEffect and useEffect hooks in a (probably) not intended way. useLayoutEffect runs a little earlier in the React lifecycle than useEffect. useLayoutEffect is intended to provide a way to call functions that need to run before the components are rendered vs. after (which is when useEffect fires). A good overview of the differences is this article by Kent C. Dodds. Have the on-chain data useEffect function depend on the MATIC price value. By doing this, the functions will re-run if the MATIC price changes, e.g. as a result of a delayed response from the API.

useLayoutEffect(() => {
}, []); // Empty list as second argument prevents it from calling when the component updates.

useEffect(() => {
    if(user) {
        Promise.allSettled([getAurumData(), getRaiderData()]);
}, [market.matic.price, user]);

This may not be an optimal solution, but it works. That is actually a common theme I've seen learning Web3 so far. Because a lot of the libraries are still new and being developed, there are workarounds for certain tasks. Additionally, the nature of blockchain data calls and everything being asynchronous starts to compound the complexity quickly.

Here are some screenshots of the updated UI. I added a Market data table to help users find a lot of basic information in once place as well.

Update Vault Page

Market Data Page