Skip to content

Commit

Permalink
Start migrating price history from coingecko to own service
Browse files Browse the repository at this point in the history
  • Loading branch information
mvaivre committed Jan 3, 2024
1 parent bb10b50 commit 8106ab4
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 75 deletions.
1 change: 0 additions & 1 deletion apps/desktop-wallet/src/components/HistoricWorthChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
selectHaveHistoricBalancesLoaded,
selectIsStateUninitialized
} from '@/storage/addresses/addressesSelectors'
import { useGetHistoricalPriceQuery } from '@/storage/prices/pricesHistorySlice'
import { ChartLength, DataPoint, LatestAmountPerAddress } from '@/types/chart'
import { Currency } from '@/types/settings'
import { CHART_DATE_FORMAT } from '@/utils/constants'
Expand Down
32 changes: 32 additions & 0 deletions apps/desktop-wallet/src/storage/prices/pricesActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.

import { TOKENS_QUERY_LIMIT } from '@alephium/shared'
import { createAsyncThunk } from '@reduxjs/toolkit'
import dayjs from 'dayjs'
import { chunk } from 'lodash'

import client from '@/api/client'
import { HistoricalPrice } from '@/storage/prices/pricesHistorySlice'
import { CHART_DATE_FORMAT } from '@/utils/constants'

export const syncTokenPrices = createAsyncThunk(
'assets/syncTokenPrices',
Expand All @@ -37,3 +40,32 @@ export const syncTokenPrices = createAsyncThunk(
return tokenPrices.flat()
}
)

export const syncTokenPricesHistory = createAsyncThunk(
'assets/syncTokenPricesHistory',
async ({ tokenSymbol, currency }: { tokenSymbol: string; currency: string }) => {
const rawHistory = await client.explorer.market.getMarketPricesIdCharts(tokenSymbol, {
currency: currency.toLowerCase()
})

console.log(rawHistory)

const today = dayjs().format(CHART_DATE_FORMAT)

return {
id: tokenSymbol,
history: rawHistory.reduce((acc, v) => {
const itemDate = dayjs(v._1).format(CHART_DATE_FORMAT)
const isDuplicatedItem = !!acc.find(({ date }) => dayjs(date).format(CHART_DATE_FORMAT) === itemDate)

if (!isDuplicatedItem && itemDate !== today)
acc.push({
date: itemDate,
price: v._2
})

return acc
}, [] as HistoricalPrice[])
}
}
)
4 changes: 4 additions & 0 deletions apps/desktop-wallet/src/storage/prices/pricesAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.
import { Price } from '@alephium/web3/dist/src/api/api-explorer'
import { createEntityAdapter } from '@reduxjs/toolkit'

import { PriceHistoryEntity } from '@/storage/prices/pricesHistorySlice'

export const tokenPricesAdapter = createEntityAdapter<Price>({
sortComparer: (a, b) => a.name.localeCompare(b.name)
})

export const tokenPricesHistoryAdapter = createEntityAdapter<PriceHistoryEntity>()
105 changes: 34 additions & 71 deletions apps/desktop-wallet/src/storage/prices/pricesHistorySlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,87 +16,50 @@ You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import { Asset } from '@alephium/shared'
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import dayjs from 'dayjs'
import { uniq } from 'lodash'
import { Tuple2LongDouble } from '@alephium/web3/dist/src/api/api-explorer'
import { createSlice, EntityState } from '@reduxjs/toolkit'

import { Currency } from '@/types/settings'
import { CHART_DATE_FORMAT } from '@/utils/constants'
import { syncTokenPricesHistory } from '@/storage/prices/pricesActions'
import { tokenPricesHistoryAdapter } from '@/storage/prices/pricesAdapter'

export interface HistoricalPrice {
date: string // CHART_DATE_FORMAT
price: number
}

interface MarketChartEnpointResult {
market_caps: [number, number][] // date, value
prices: [number, number][]
total_volumes: [number, number][]
export interface PriceHistoryEntity {
id: string
history: HistoricalPrice[]
}

// TODO: EXPORT TO SHARED LIB
export type CoinGeckoID = 'alephium' | 'tether' | 'usdc' | 'dai' | 'ethereum' | 'wrapped-bitcoin'

export const symbolCoinGeckoMapping: { [key: string]: CoinGeckoID } = {
ALPH: 'alephium',
USDT: 'tether',
USDC: 'usdc',
DAI: 'dai',
WETH: 'ethereum',
WBTC: 'wrapped-bitcoin'
interface PricesHistoryState extends EntityState<PriceHistoryEntity> {
loading: boolean
}

export const getTokensApiIds = (tokens: Asset[]) =>
uniq(tokens.flatMap((t) => (t.symbol && symbolCoinGeckoMapping[t.symbol] ? symbolCoinGeckoMapping[t.symbol] : [])))

type HistoricalPriceQueryParams = {
assetIds: CoinGeckoID[]
currency: Currency
days: number
}

export const priceHistoryApi = createApi({
reducerPath: 'priceHistoryApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.coingecko.com/api/v3/' }),
endpoints: (builder) => ({
getHistoricalPrice: builder.query<{ [id in CoinGeckoID]: HistoricalPrice[] }, HistoricalPriceQueryParams>({
queryFn: async ({ assetIds, currency, days }, _queryApi, _extraOptions, fetchWithBQ) => {
const results = (await Promise.all(
assetIds.map((id) =>
fetchWithBQ(`/coins/${id}/market_chart?vs_currency=${currency.toLowerCase()}&days=${days}`)
)
)) as { data: MarketChartEnpointResult; error?: string }[]

const today = dayjs().format(CHART_DATE_FORMAT)

const errors = results.filter((r) => !!r.error)

if (errors.length > 0) return { error: { error: errors.join(', ') } as FetchBaseQueryError }

return {
data: results.reduce(
(acc, { data: { prices } }, i) => ({
...acc,
[assetIds[i]]: prices.reduce((acc, [date, price]) => {
const itemDate = dayjs(date).format(CHART_DATE_FORMAT)
const isDuplicatedItem = !!acc.find(({ date }) => dayjs(date).format(CHART_DATE_FORMAT) === itemDate)

if (!isDuplicatedItem && itemDate !== today)
acc.push({
date: itemDate,
price
})
const initialState: PricesHistoryState = tokenPricesHistoryAdapter.getInitialState({
loading: false
})

return acc
}, [] as HistoricalPrice[])
}),
{} as { [id in CoinGeckoID]: HistoricalPrice[] }
)
const pricesHistorySlice = createSlice({
name: 'tokenPricesHistory',
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(syncTokenPricesHistory.pending, (state) => {
state.loading = true
})
.addCase(syncTokenPricesHistory.fulfilled, (state, action) => {
const tokenPriceHistory = action.payload

if (tokenPriceHistory) {
tokenPricesHistoryAdapter.upsertOne(state, tokenPriceHistory)
}
}
})
})

state.loading = false
})
.addCase(syncTokenPricesHistory.rejected, (state) => {
state.loading = false
})
}
})

export const { useGetHistoricalPriceQuery } = priceHistoryApi
export default pricesHistorySlice
6 changes: 3 additions & 3 deletions apps/desktop-wallet/src/storage/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import assetsInfoSlice from '@/storage/assets/assetsInfoSlice'
import nftsSlice from '@/storage/assets/nftsSlice'
import globalSlice from '@/storage/global/globalSlice'
import snackbarSlice from '@/storage/global/snackbarSlice'
import { priceHistoryApi } from '@/storage/prices/pricesHistorySlice'
import pricesHistorySlice from '@/storage/prices/pricesHistorySlice'
import pricesSlice from '@/storage/prices/pricesSlice'
import networkSlice, { networkListenerMiddleware } from '@/storage/settings/networkSlice'
import settingsSlice, { settingsListenerMiddleware } from '@/storage/settings/settingsSlice'
Expand All @@ -48,8 +48,8 @@ export const store = configureStore({
[assetsInfoSlice.name]: assetsInfoSlice.reducer,
[snackbarSlice.name]: snackbarSlice.reducer,
[pricesSlice.name]: pricesSlice.reducer,
[nftsSlice.name]: nftsSlice.reducer,
[priceHistoryApi.reducerPath]: priceHistoryApi.reducer
[pricesHistorySlice.name]: pricesHistorySlice.reducer,
[nftsSlice.name]: nftsSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
Expand Down

0 comments on commit 8106ab4

Please sign in to comment.