Skip to content

Commit a38e154

Browse files
committed
Fetch currency rates from on-chain price feeds
This adds a few contract addresses for EUR/AUD/JPY rates which should be good for testing. Also added a dropdown on the settings page to test switching between currencies that is yet to be wrapped under a feature flag.
1 parent a3bee0a commit a38e154

File tree

8 files changed

+150
-13
lines changed

8 files changed

+150
-13
lines changed

background/redux-slices/ui.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export type Events = {
8383
updateAnalyticsPreferences: Partial<AnalyticsPreferences>
8484
addCustomNetworkResponse: [string, boolean]
8585
updateAutoLockInterval: number
86+
updateDisplayCurrency: string
8687
toggleShowTestNetworks: boolean
8788
clearNotification: string
8889
}
@@ -383,6 +384,13 @@ export const updateAutoLockInterval = createBackgroundAsyncThunk(
383384
},
384385
)
385386

387+
export const updateDisplayCurrency = createBackgroundAsyncThunk(
388+
"ui/updateDisplayCurrency",
389+
async (newValue: string) => {
390+
emitter.emit("updateDisplayCurrency", newValue)
391+
},
392+
)
393+
386394
export const userActivityEncountered = createBackgroundAsyncThunk(
387395
"ui/userActivityEncountered",
388396
async (addressNetwork: AddressOnNetwork) => {

background/redux-slices/utils/asset-utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ export function convertUSDPricePointToCurrency(
205205
pricePoint: PricePoint,
206206
currency: DisplayCurrency,
207207
) {
208+
if (currency.code === "USD") {
209+
// noop
210+
return pricePoint
211+
}
208212
const { pair, amounts, time } = pricePoint
209213
const idx = pricePoint.pair.findIndex((asset) => asset.symbol === "USD")
210214

@@ -225,7 +229,7 @@ export function convertUSDPricePointToCurrency(
225229
const rate = new ExchangeRate({
226230
// Keeping 10 decimals ensures we don't lose precision during conversion
227231
baseCurrency: { ...currencies[currency.code], decimals: 10n },
228-
quoteCurrency: currencies.USD,
232+
quoteCurrency: { ...currencies.USD, decimals: 10n },
229233
rate: FixedPoint(currency.rate),
230234
})
231235

background/services/indexing/db.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ function numberArrayCompare(arr1: number[], arr2: number[]) {
121121
return 0
122122
}
123123

124+
type FeedRate = {
125+
id: string
126+
value: bigint
127+
decimals: bigint
128+
time: number
129+
}
130+
124131
export class IndexingDatabase extends Dexie {
125132
private prices!: Dexie.Table<PriceMeasurement, number>
126133

@@ -139,6 +146,11 @@ export class IndexingDatabase extends Dexie {
139146
*/
140147
private customAssets!: Dexie.Table<CustomAsset, number>
141148

149+
/**
150+
* Currency exchange rates
151+
*/
152+
private currencyRates!: Dexie.Table<FeedRate, "id">
153+
142154
/*
143155
* Tokens whose balances should be checked periodically. It might make sense
144156
* for this to be tracked against particular accounts in the future.
@@ -256,6 +268,8 @@ export class IndexingDatabase extends Dexie {
256268
delete customAsset.metadata?.discoveryTxHash
257269
}),
258270
)
271+
272+
this.version(7).stores({ currencyRates: "&id" })
259273
}
260274

261275
async savePriceMeasurement(
@@ -429,6 +443,14 @@ export class IndexingDatabase extends Dexie {
429443
tokenList: v as TokenList,
430444
}))
431445
}
446+
447+
async getCurrencyRates(): Promise<FeedRate[]> {
448+
return this.currencyRates.toArray()
449+
}
450+
451+
async saveCurrencyRates(rates: FeedRate[]): Promise<void> {
452+
await this.currencyRates.bulkPut(rates)
453+
}
432454
}
433455

434456
export async function getOrCreateDb(

background/services/indexing/index.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
isSameAsset,
5858
} from "../../redux-slices/utils/asset-utils"
5959
import { wrapIfEnabled } from "../../features"
60+
import fetchRatesFromPriceFeeds from "./price-feeds"
6061

6162
// Transactions seen within this many blocks of the chain tip will schedule a
6263
// token refresh sooner than the standard rate.
@@ -1108,11 +1109,32 @@ export default class IndexingService extends BaseService<Events> {
11081109
private async handleCurrencyRatesAlarm(): Promise<void> {
11091110
logger.info("Syncing currency rates...")
11101111

1111-
const rate = {
1112-
code: "EUR",
1113-
rate: { amount: BigInt("0x06f9d7c8"), decimals: 8n },
1114-
}
1112+
const rates = await fetchRatesFromPriceFeeds(this.chainService)
1113+
1114+
this.db.saveCurrencyRates(Object.values(rates))
1115+
1116+
const currencies = Object.keys(rates).map((id) => {
1117+
const currencyCode = id.split("/")[0]
1118+
1119+
return {
1120+
code: currencyCode,
1121+
rate: { amount: rates[id].value, decimals: rates[id].decimals },
1122+
}
1123+
})
11151124

1116-
this.emitter.emit("updatedCurrencyRates", [rate])
1125+
this.emitter.emit("updatedCurrencyRates", currencies)
1126+
}
1127+
1128+
async getCurrencyRates() {
1129+
const rates = await this.db.getCurrencyRates()
1130+
1131+
return rates.map((rate) => {
1132+
const currencyCode = rate.id.split("/")[0]
1133+
1134+
return {
1135+
code: currencyCode,
1136+
rate: { amount: rate.value, decimals: rate.decimals },
1137+
}
1138+
})
11171139
}
11181140
}

background/services/indexing/price-feeds.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type FeedRate = {
2222
id: string
2323
value: bigint
2424
decimals: bigint
25+
time: number
2526
}
2627

2728
export default async function fetchRatesFromPriceFeeds(
@@ -40,10 +41,14 @@ export default async function fetchRatesFromPriceFeeds(
4041
provider,
4142
)
4243

44+
const decimals = contract.callStatic.decimals()
45+
const roundData = await contract.callStatic.latestRoundData()
46+
4347
return {
4448
id,
45-
value: (await contract.callStatic.latestRoundData()) as BigNumber,
46-
decimals: (await contract.callStatic.decimals()) as number,
49+
value: roundData.answer as BigNumber,
50+
decimals: (await decimals) as number,
51+
time: Date.now(),
4752
}
4853
}),
4954
)
@@ -55,6 +60,7 @@ export default async function fetchRatesFromPriceFeeds(
5560
id: feed.id,
5661
value: feed.value.toBigInt(),
5762
decimals: BigInt(feed.decimals),
63+
time: feed.time,
5864
},
5965
]),
6066
)

background/services/preferences/db.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,27 @@ export class PreferenceDatabase extends Dexie {
480480
}),
481481
)
482482

483+
this.version(24).upgrade((tx) =>
484+
tx
485+
.table("preferences")
486+
.toCollection()
487+
.modify((storedPreferences: Preferences) => {
488+
const USD: DisplayCurrency = {
489+
code: "USD",
490+
rate: {
491+
amount: 1_000_000_0000n, // 1 USD = 1 USD
492+
decimals: 10n,
493+
},
494+
}
495+
496+
const update: Partial<Preferences> = {
497+
currency: USD,
498+
}
499+
500+
Object.assign(storedPreferences, update)
501+
}),
502+
)
503+
483504
// This is the old version for populate
484505
// https://dexie.org/docs/Dexie/Dexie.on.populate-(old-version)
485506
// The this does not behave according the new docs, but works

background/services/redux/index.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -947,16 +947,24 @@ export default class ReduxService extends BaseService<never> {
947947
this.store.dispatch(
948948
setDisplayCurrency({
949949
code: "USD",
950-
rate: { amount: 1000000n, decimals: 6n },
950+
rate: { amount: 1_000_000_000_0n, decimals: 10n },
951951
}),
952952
)
953953
}
954954

955-
this.indexingService.emitter.on("updatedCurrencyRates", async (rates) => {
956-
// const currency = await this.preferenceService.getCurrency()
955+
this.indexingService.emitter.on(
956+
"updatedCurrencyRates",
957+
async (currencies) => {
958+
const currency = await this.preferenceService.getCurrency()
957959

958-
this.store.dispatch(setDisplayCurrency(rates[0]))
959-
})
960+
const update = currencies.find((rate) => rate.code === currency.code)
961+
962+
// Only update if we successfully received an updated rate
963+
if (update) {
964+
await this.store.dispatch(setDisplayCurrency(update))
965+
}
966+
},
967+
)
960968

961969
this.indexingService.emitter.on("refreshAsset", (asset) => {
962970
this.store.dispatch(
@@ -1767,6 +1775,24 @@ export default class ReduxService extends BaseService<never> {
17671775
uiSliceEmitter.on("updateAutoLockInterval", async (newTimerValue) => {
17681776
await this.preferenceService.updateAutoLockInterval(newTimerValue)
17691777
})
1778+
1779+
uiSliceEmitter.on("updateDisplayCurrency", async (currencyCode) => {
1780+
const rates = await this.indexingService.getCurrencyRates()
1781+
1782+
const fallback = {
1783+
code: "USD",
1784+
rate: { amount: 1_000_000_000_0n, decimals: 10n },
1785+
}
1786+
1787+
const currency =
1788+
// FIXME: Currency won't update if there's no exchange rate available.
1789+
// This may confuse the user, as it may seem the dropdown did nothing.
1790+
rates.find((rate) => rate.code === currencyCode) ?? fallback
1791+
1792+
// TODO: dispatch preferences db update
1793+
1794+
this.store.dispatch(setDisplayCurrency(currency))
1795+
})
17701796
}
17711797

17721798
async connectCampaignService() {

ui/pages/Settings.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import {
1717
selectUseFlashbots,
1818
selectAutoLockTimer as selectAutoLockInterval,
1919
updateAutoLockInterval,
20+
updateDisplayCurrency,
2021
} from "@tallyho/tally-background/redux-slices/ui"
2122
import { useHistory } from "react-router-dom"
2223
import { FLASHBOTS_DOCS_URL, MINUTE } from "@tallyho/tally-background/constants"
2324
import {
25+
selectDisplayCurrency,
2426
selectDisplayCurrencySign,
2527
userValueDustThreshold,
2628
} from "@tallyho/tally-background/redux-slices/selectors"
@@ -64,6 +66,13 @@ const AUTO_LOCK_OPTIONS = [
6466
{ label: "60", value: String(60 * MINUTE) },
6567
]
6668

69+
const CURRENCY_OPTIONS = [
70+
{ label: "USD", value: "USD" },
71+
{ label: "EUR", value: "EUR" },
72+
{ label: "AUD", value: "AUD" },
73+
{ label: "JPY", value: "JPY" },
74+
]
75+
6776
const FOOTER_ACTIONS = [
6877
{
6978
icon: "icons/m/discord",
@@ -361,6 +370,24 @@ export default function Settings(): ReactElement {
361370
),
362371
}
363372

373+
const displayCurrency = useBackgroundSelector(selectDisplayCurrency)
374+
375+
const currencySettings = {
376+
title: "Currency",
377+
component: () => (
378+
<SharedSelect
379+
width={194}
380+
options={CURRENCY_OPTIONS}
381+
onChange={(currencyCode) =>
382+
dispatch(updateDisplayCurrency(currencyCode))
383+
}
384+
defaultIndex={CURRENCY_OPTIONS.findIndex(
385+
(option) => option.value === displayCurrency.code,
386+
)}
387+
/>
388+
),
389+
}
390+
364391
const notificationBanner = {
365392
title: t("settings.showBanners"),
366393
component: () => (
@@ -415,6 +442,7 @@ export default function Settings(): ReactElement {
415442
dAppsSettings,
416443
analytics,
417444
...wrapIfEnabled(FeatureFlags.SUPPORT_MULTIPLE_LANGUAGES, languages),
445+
currencySettings,
418446
...wrapIfEnabled(
419447
FeatureFlags.SUPPORT_ACHIEVEMENTS_BANNER,
420448
notificationBanner,

0 commit comments

Comments
 (0)