Skip to content

Commit

Permalink
Refactor prices caching (#3526)
Browse files Browse the repository at this point in the history
Relocates cached asset prices to a new redux slice. This reduces
performance hits after price fetching by avoiding nested array diffs,
which Immer struggles to manage. Consequently, it prevents unnecessary
updates being serialized/deserialized.

## To Test
This clears cached data for both account balances and assets, after
switching from `main` to this branch:
- [x] Check that prices and balances are loading and displaying normally
- [x] Check Swaps (Quotes)
- [x] Check Send page (Estimates)

Latest build:
[extension-builds-3526](https://github.com/tahowallet/extension/suites/14603122227/artifacts/827664213)
(as of Thu, 27 Jul 2023 06:33:36 GMT).
  • Loading branch information
kkosiorowska authored Jul 27, 2023
2 parents 4d97a2e + 234d1c3 commit df02ccb
Show file tree
Hide file tree
Showing 31 changed files with 496 additions and 377 deletions.
2 changes: 1 addition & 1 deletion background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ import {
} from "./redux-slices/accounts"
import {
assetsLoaded,
newPricePoints,
refreshAsset,
removeAssetData,
} from "./redux-slices/assets"
Expand Down Expand Up @@ -199,6 +198,7 @@ import {
import { getPricePoint, getTokenPrices } from "./lib/prices"
import { makeFlashbotsProviderCreator } from "./services/chain/serial-fallback-provider"
import { DismissableItem } from "./services/preferences"
import { newPricePoints } from "./redux-slices/prices"

// This sanitizer runs on store and action data before serializing for remote
// redux devtools. The goal is to end up with an object that is directly
Expand Down
8 changes: 7 additions & 1 deletion background/redux-slices/0x-swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
PriceDetails,
SwapQuoteRequest,
} from "./utils/0x-swap-utils"
import { PricesState } from "./prices"

// This is how 0x represents native token addresses
const ZEROEX_NATIVE_TOKEN_CONTRACT_ADDRESS =
Expand Down Expand Up @@ -315,7 +316,10 @@ export const fetchSwapPrice = createBackgroundAsyncThunk(
> => {
const signer = getProvider().getSigner()
const tradeAddress = await signer.getAddress()
const { assets } = getState() as { assets: AssetsState }
const { assets, prices } = getState() as {
assets: AssetsState
prices: PricesState
}

const requestUrl = build0xUrlFromSwapRequest("/price", quoteRequest, {
takerAddress: tradeAddress,
Expand Down Expand Up @@ -369,13 +373,15 @@ export const fetchSwapPrice = createBackgroundAsyncThunk(
Number(quote.buyTokenToEthRate),
quoteRequest.assets.buyAsset,
assets,
prices,
quote.buyAmount,
quoteRequest.network
),
sellCurrencyAmount: await checkCurrencyAmount(
Number(quote.sellTokenToEthRate),
quoteRequest.assets.sellAsset,
assets,
prices,
quote.sellAmount,
quoteRequest.network
),
Expand Down
10 changes: 5 additions & 5 deletions background/redux-slices/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
AssetMainCurrencyAmount,
AssetDecimalAmount,
isBaseAssetForNetwork,
AssetID,
getAssetID,
isSameAsset,
isTrustedAsset,
getFullAssetID,
FullAssetID,
} from "./utils/asset-utils"
import { DomainName, HexString, URI } from "../types"
import { normalizeEVMAddress, sameEVMAddress } from "../lib/utils"
Expand Down Expand Up @@ -61,7 +61,7 @@ export type AccountData = {
address: HexString
network: Network
balances: {
[assetID: AssetID]: AccountBalance
[assetID: FullAssetID]: AccountBalance
}
ens: {
name?: DomainName
Expand Down Expand Up @@ -316,7 +316,7 @@ const accountSlice = createSlice({
network,
assetAmount: { asset },
} = updatedAccountBalance
const assetID = getAssetID(asset)
const assetID = getFullAssetID(asset)

const normalizedAddress = normalizeEVMAddress(address)
const existingAccountData =
Expand Down Expand Up @@ -451,7 +451,7 @@ const accountSlice = createSlice({
if (account !== "loading") {
Object.values(account.balances).forEach(({ assetAmount }) => {
if (isSameAsset(assetAmount.asset, asset)) {
delete account.balances[getAssetID(assetAmount.asset)]
delete account.balances[getFullAssetID(assetAmount.asset)]
}
})
}
Expand Down
150 changes: 11 additions & 139 deletions background/redux-slices/assets.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,28 @@
import { createSelector, createSlice } from "@reduxjs/toolkit"
import { createSlice } from "@reduxjs/toolkit"
import { ethers } from "ethers"
import type { RootState } from "."
import { AddressOnNetwork } from "../accounts"
import {
AnyAsset,
AnyAssetAmount,
AnyAssetMetadata,
flipPricePoint,
isFungibleAsset,
isSmartContractFungibleAsset,
PricePoint,
SmartContractFungibleAsset,
} from "../assets"
import { AddressOnNetwork } from "../accounts"
import { findClosestAssetIndex } from "../lib/asset-similarity"
import { createBackgroundAsyncThunk } from "./utils"
import { isBaseAssetForNetwork, isSameAsset } from "./utils/asset-utils"
import { getProvider } from "./utils/contract-utils"
import { EVMNetwork, sameNetwork } from "../networks"
import { ERC20_INTERFACE } from "../lib/erc20"
import logger from "../lib/logger"
import { FIAT_CURRENCIES_SYMBOL } from "../constants"
import { convertFixedPoint } from "../lib/fixed-point"
import { removeAssetReferences, updateAssetReferences } from "./accounts"
import { EVMNetwork, sameNetwork } from "../networks"
import { NormalizedEVMAddress } from "../types"
import type { RootState } from "."

export type AssetWithRecentPrices<T extends AnyAsset = AnyAsset> = T & {
recentPrices: {
[assetSymbol: string]: PricePoint
}
}
import { removeAssetReferences, updateAssetReferences } from "./accounts"
import { createBackgroundAsyncThunk } from "./utils"
import { isBaseAssetForNetwork, isSameAsset } from "./utils/asset-utils"
import { getProvider } from "./utils/contract-utils"

export type SingleAssetState = AssetWithRecentPrices
export type SingleAssetState = AnyAsset

export type AssetsState = SingleAssetState[]

export const initialState = [] as AssetsState
export const initialState: AssetsState = []

const assetsSlice = createSlice({
name: "assets",
Expand All @@ -60,7 +48,6 @@ const assetsSlice = createSlice({
mappedAssets[newAsset.symbol] = [
{
...newAsset,
recentPrices: {},
},
]
} else {
Expand All @@ -77,7 +64,6 @@ const assetsSlice = createSlice({
if (duplicateIndexes.length === 0) {
mappedAssets[newAsset.symbol].push({
...newAsset,
recentPrices: {},
})
} else {
// TODO if there are duplicates... when should we replace assets?
Expand All @@ -94,25 +80,6 @@ const assetsSlice = createSlice({

return Object.values(mappedAssets).flat()
},
newPricePoints: (
immerState,
{ payload: pricePoints }: { payload: PricePoint[] }
) => {
pricePoints.forEach((pricePoint) => {
const fiatCurrency = pricePoint.pair.find((asset) =>
FIAT_CURRENCIES_SYMBOL.includes(asset.symbol)
)
const [pricedAsset] = pricePoint.pair.filter(
(asset) => asset !== fiatCurrency
)
if (fiatCurrency && pricedAsset) {
const index = findClosestAssetIndex(pricedAsset, immerState)
if (typeof index !== "undefined") {
immerState[index].recentPrices[fiatCurrency.symbol] = pricePoint
}
}
})
},
removeAsset: (
immerState,
{ payload: removedAsset }: { payload: AnyAsset }
Expand All @@ -122,19 +89,10 @@ const assetsSlice = createSlice({
},
})

export const { assetsLoaded, newPricePoints, removeAsset } = assetsSlice.actions
export const { assetsLoaded, removeAsset } = assetsSlice.actions

export default assetsSlice.reducer

const selectAssetsState = (state: AssetsState) => state
const selectAsset = (_: AssetsState, asset: AnyAsset) => asset

const selectPairedAssetSymbol = (
_: AssetsState,
_2: AnyAsset,
pairedAssetSymbol: string
) => pairedAssetSymbol

export const updateAssetMetadata = createBackgroundAsyncThunk(
"assets/updateAssetMetadata",
async (
Expand Down Expand Up @@ -271,92 +229,6 @@ export const transferAsset = createBackgroundAsyncThunk(
}
)

/**
* Selects a particular asset price point given the asset symbol and the paired
* asset symbol used to price it.
*
* For example, calling `selectAssetPricePoint(state.assets, ETH, "USD")`
* will return the ETH-USD price point, if it exists. Note that this selector
* guarantees that the returned price point will have the pair in the specified
* order, so even if the store price point has amounts in the order [USD, ETH],
* the selector will return them in the order [ETH, USD].
*/
export const selectAssetPricePoint = createSelector(
[selectAssetsState, selectAsset, selectPairedAssetSymbol],
(assets, assetToFind, pairedAssetSymbol) => {
const hasRecentPriceData = (asset: SingleAssetState): boolean =>
pairedAssetSymbol in asset.recentPrices &&
asset.recentPrices[pairedAssetSymbol].pair.some(
({ symbol }) => symbol === assetToFind.symbol
)

let pricedAsset: SingleAssetState | undefined

/* If we're looking for a smart contract, try to find an exact price point */
if (isSmartContractFungibleAsset(assetToFind)) {
pricedAsset = assets.find(
(asset): asset is AssetWithRecentPrices<SmartContractFungibleAsset> =>
isSmartContractFungibleAsset(asset) &&
asset.contractAddress === assetToFind.contractAddress &&
asset.homeNetwork.chainID === assetToFind.homeNetwork.chainID &&
hasRecentPriceData(asset)
)

/* Don't do anything else if this is an unverified asset and there's no exact match */
if (
(assetToFind.metadata?.tokenLists?.length ?? 0) < 1 &&
!isBaseAssetForNetwork(assetToFind, assetToFind.homeNetwork)
) {
return undefined
}
}

/* Otherwise, find a best-effort match by looking for assets with the same symbol */
if (!pricedAsset) {
pricedAsset = assets.find(
(asset) =>
asset.symbol === assetToFind.symbol && hasRecentPriceData(asset)
)
}

if (pricedAsset) {
let pricePoint = pricedAsset.recentPrices[pairedAssetSymbol]

// Flip it if the price point looks like USD-ETH
if (pricePoint.pair[0].symbol !== assetToFind.symbol) {
pricePoint = flipPricePoint(pricePoint)
}

const assetDecimals = isFungibleAsset(assetToFind)
? assetToFind.decimals
: 0
const pricePointAssetDecimals = isFungibleAsset(pricePoint.pair[0])
? pricePoint.pair[0].decimals
: 0

if (assetDecimals !== pricePointAssetDecimals) {
const { amounts } = pricePoint
pricePoint = {
...pricePoint,
amounts: [
convertFixedPoint(
amounts[0],
pricePointAssetDecimals,
assetDecimals
),
amounts[1],
],
}
}

return pricePoint
}

// If no matching priced asset was found, return undefined.
return undefined
}
)

export const importCustomToken = createBackgroundAsyncThunk(
"assets/importCustomToken",
async (
Expand Down
6 changes: 3 additions & 3 deletions background/redux-slices/earn-utils/getDoggoPrice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { BigNumber } from "ethers"
import { AssetsState, selectAssetPricePoint } from "../assets"
import { getContract } from "../utils/contract-utils"
import UNISWAP_V2_PAIR from "../../lib/uniswapPair"
import { ETHEREUM } from "../../constants"
import { PricesState, selectAssetPricePoint } from "../prices"

export const DOGGOETH_PAIR = "0x93a08986ec9a74CB9E001702F30202f3749ceDC4"

const getDoggoPrice = async (
assets: AssetsState,
prices: PricesState,
mainCurrencySymbol: string
): Promise<bigint> => {
// Fetching price of DOGGO from DOGGO/ETH UniswapV2Pair
Expand All @@ -19,7 +19,7 @@ const getDoggoPrice = async (
const reserves = await doggoUniswapPairContract.getReserves()
const { reserve0, reserve1 } = reserves
const asset0PricePoint = selectAssetPricePoint(
assets,
prices,
ETHEREUM.baseAsset,
mainCurrencySymbol
)
Expand Down
6 changes: 3 additions & 3 deletions background/redux-slices/earn-utils/getLPTokenValue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { BigNumber } from "ethers"
import { AssetsState, selectAssetPricePoint } from "../assets"
import { HexString } from "../../types"
import { getContract } from "../utils/contract-utils"
import { ERC20_ABI } from "../../lib/erc20"
import { PricesState, selectAssetPricePoint } from "../prices"

const getLPTokenValue = async (
mainCurrencySymbol: string,
assets: AssetsState,
prices: PricesState,
token: HexString,
reserve: BigNumber,
LPDecimals: number,
Expand All @@ -16,7 +16,7 @@ const getLPTokenValue = async (
const token0Symbol = await token0Contract.symbol()

const assetPricePoint = selectAssetPricePoint(
assets,
prices,
token0Symbol,
mainCurrencySymbol
)
Expand Down
10 changes: 5 additions & 5 deletions background/redux-slices/earn-utils/getPoolAPR.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { BigNumber } from "ethers"
import { AnyAsset } from "../../assets"
import { HexString } from "../../types"
import { AssetsState } from "../assets"
import { getContract, getCurrentTimestamp } from "../utils/contract-utils"
import VAULT_ABI from "../../lib/vault"
import { DOGGO } from "../../constants"
import getDoggoPrice from "./getDoggoPrice"
import getTokenPrice from "./getTokenPrice"
import { sameEVMAddress } from "../../lib/utils"
import { fetchWithTimeout } from "../../utils/fetching"
import { PricesState } from "../prices"

async function getYearnVaultAPY(yearnVaultAddress: HexString) {
const yearnVaultsAPIData = await (
Expand Down Expand Up @@ -58,14 +58,14 @@ const DOGGO_HIGH_PRICE_ESTIMATE = 250000000n // $750M valuation

const getPoolAPR = async ({
asset,
assets,
prices,
vaultAddress,
}: {
asset: AnyAsset & {
decimals: number
contractAddress: HexString
}
assets: AssetsState
prices: PricesState
vaultAddress: HexString
}): Promise<{
totalAPR?: string
Expand All @@ -80,7 +80,7 @@ const getPoolAPR = async ({
// How many tokens have been staked into the hunting ground
const tokensStaked = await huntingGroundContract.totalSupply()
// What is the value of a single stake token
const { singleTokenPrice } = await getTokenPrice(asset, assets)
const { singleTokenPrice } = await getTokenPrice(asset, prices)
// Fetch underlying yearn vault APR
const yearnVaultAddress = await huntingGroundContract.vault()
const yearnVaultAPYPercent = await getYearnVaultAPY(yearnVaultAddress)
Expand Down Expand Up @@ -115,7 +115,7 @@ const getPoolAPR = async ({
? secondsInAYear.div(remainingPeriodSeconds)
: BigNumber.from(0)
// What is the value of single reward token in USD bigint with 10 decimals
const rewardTokenPrice = await getDoggoPrice(assets, mainCurrencySymbol)
const rewardTokenPrice = await getDoggoPrice(prices, mainCurrencySymbol)
// The doggo price is not available before DAO vote, we will return approximate values
// The values are in USD with 10 decimals, e.g. 1_000_000_000n = $0.1

Expand Down
Loading

0 comments on commit df02ccb

Please sign in to comment.