Skip to content

Commit

Permalink
Additionally fetch exchange rates from ICPSwap (#7139)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Jan 3, 2025
1 parent 9d920a9 commit ae6a91a
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 26 deletions.
3 changes: 3 additions & 0 deletions frontend/openchat-agent/codegen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ didc bind ./src/services/dexes/sonic/swaps/candid/can.did -t js > ./src/services
didc bind ./src/services/icpcoins/candid/can.did -t ts > ./src/services/icpcoins/candid/types.d.ts
didc bind ./src/services/icpcoins/candid/can.did -t js > ./src/services/icpcoins/candid/idl.js

didc bind ./src/services/icpSwap/candid/can.did -t ts > ./src/services/icpSwap/candid/types.d.ts
didc bind ./src/services/icpSwap/candid/can.did -t js > ./src/services/icpSwap/candid/idl.js

didc bind ./src/services/icpLedgerIndex/candid/can.did -t ts > ./src/services/icpLedgerIndex/candid/types.d.ts
didc bind ./src/services/icpLedgerIndex/candid/can.did -t js > ./src/services/icpLedgerIndex/candid/idl.js

Expand Down
10 changes: 10 additions & 0 deletions frontend/openchat-agent/src/services/icpSwap/candid/can.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type PublicTokenOverview =
record {
address: text;
priceUSD: float64;
symbol: text;
volumeUSD7d: float64;
};
service : {
getAllTokens: () -> (vec PublicTokenOverview) query;
}
7 changes: 7 additions & 0 deletions frontend/openchat-agent/src/services/icpSwap/candid/idl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { IDL } from "@dfinity/candid";
import { _SERVICE } from "./types";
export {
_SERVICE as ICPSwapService,
};

export const idlFactory: IDL.InterfaceFactory;
12 changes: 12 additions & 0 deletions frontend/openchat-agent/src/services/icpSwap/candid/idl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const idlFactory = ({ IDL }) => {
const PublicTokenOverview = IDL.Record({
'volumeUSD7d' : IDL.Float64,
'address' : IDL.Text,
'priceUSD' : IDL.Float64,
'symbol' : IDL.Text,
});
return IDL.Service({
'getAllTokens' : IDL.Func([], [IDL.Vec(PublicTokenOverview)], ['query']),
});
};
export const init = ({ IDL }) => { return []; };
15 changes: 15 additions & 0 deletions frontend/openchat-agent/src/services/icpSwap/candid/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Principal } from '@dfinity/principal';
import type { ActorMethod } from '@dfinity/agent';
import type { IDL } from '@dfinity/candid';

export interface PublicTokenOverview {
'volumeUSD7d' : number,
'address' : string,
'priceUSD' : number,
'symbol' : string,
}
export interface _SERVICE {
'getAllTokens' : ActorMethod<[], Array<PublicTokenOverview>>,
}
export declare const idlFactory: IDL.InterfaceFactory;
export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];
27 changes: 27 additions & 0 deletions frontend/openchat-agent/src/services/icpSwap/icpSwapClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { HttpAgent, Identity } from "@dfinity/agent";
import type { CryptocurrencyDetails, TokenExchangeRates } from "openchat-shared";
import { idlFactory, type ICPSwapService } from "./candid/idl";
import { CandidService } from "../candidService";
import { getAllTokensResponse } from "./mappers";
import type { ExchangeRateClient } from "../openchatAgent";

const ICPSWAP_CANISTER_ID = "ggzvv-5qaaa-aaaag-qck7a-cai";

export class IcpSwapClient extends CandidService implements ExchangeRateClient {
private service: ICPSwapService;

constructor(identity: Identity, agent: HttpAgent) {
super(identity, agent, ICPSWAP_CANISTER_ID);

this.service = this.createServiceClient<ICPSwapService>(idlFactory);
}

exchangeRates(
supportedTokens: CryptocurrencyDetails[],
): Promise<Record<string, TokenExchangeRates>> {
return this.handleQueryResponse(
() => this.service.getAllTokens(),
(resp) => getAllTokensResponse(resp, supportedTokens),
);
}
}
20 changes: 20 additions & 0 deletions frontend/openchat-agent/src/services/icpSwap/mappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type CryptocurrencyDetails, type TokenExchangeRates } from "openchat-shared";
import type { PublicTokenOverview } from "./candid/types";

export function getAllTokensResponse(
candid: Array<PublicTokenOverview>,
supportedTokens: CryptocurrencyDetails[],
): Record<string, TokenExchangeRates> {
const exchangeRates: Record<string, TokenExchangeRates> = {};
const supportedLedgers = new Set<string>(supportedTokens.map((t) => t.ledger));

for (const token of candid) {
if (supportedLedgers.has(token.address)) {
exchangeRates[token.symbol.trim().toLowerCase()] = {
toUSD: token.priceUSD,
};
}
}

return exchangeRates;
}
14 changes: 10 additions & 4 deletions frontend/openchat-agent/src/services/icpcoins/icpCoinsClient.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { HttpAgent, Identity } from "@dfinity/agent";
import type { TokenExchangeRates } from "openchat-shared";
import type { CryptocurrencyDetails, TokenExchangeRates } from "openchat-shared";
import { idlFactory, type ICPCoinsService } from "./candid/idl";
import { CandidService } from "../candidService";
import { getLatestResponse } from "./mappers";
import type { ExchangeRateClient } from "../openchatAgent";

const ICPCOINS_CANISTER_ID = "u45jl-liaaa-aaaam-abppa-cai";

export class IcpCoinsClient extends CandidService {
export class IcpCoinsClient extends CandidService implements ExchangeRateClient {
private service: ICPCoinsService;

constructor(identity: Identity, agent: HttpAgent) {
Expand All @@ -15,7 +16,12 @@ export class IcpCoinsClient extends CandidService {
this.service = this.createServiceClient<ICPCoinsService>(idlFactory);
}

exchangeRates(): Promise<Record<string, TokenExchangeRates>> {
return this.handleQueryResponse(() => this.service.get_latest(), getLatestResponse);
exchangeRates(
supportedTokens: CryptocurrencyDetails[],
): Promise<Record<string, TokenExchangeRates>> {
return this.handleQueryResponse(
() => this.service.get_latest(),
(resp) => getLatestResponse(resp, supportedTokens),
);
}
}
20 changes: 8 additions & 12 deletions frontend/openchat-agent/src/services/icpcoins/mappers.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import type { TokenExchangeRates } from "openchat-shared";
import type { CryptocurrencyDetails, TokenExchangeRates } from "openchat-shared";
import type { LatestTokenRow } from "./candid/types";

export function getLatestResponse(
candid: Array<LatestTokenRow>,
supportedTokens: CryptocurrencyDetails[],
): Record<string, TokenExchangeRates> {
const supportedSymbols = new Set<string>(supportedTokens.map((t) => t.symbol.toLowerCase()));
supportedSymbols.add("btc");
supportedSymbols.add("eth");

const exchangeRates: Record<string, TokenExchangeRates> = {};

for (const row of candid) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_pair, pairText, rate] = row;
const [from, to] = parseSymbolPair(pairText);

if (to === "usd") {
exchangeRates[from] = { ...exchangeRates[from], toUSD: rate };
} else if (to === "icp") {
exchangeRates[from] = { ...exchangeRates[from], toICP: rate };
if (to === "usd" && supportedSymbols.has(from)) {
exchangeRates[from] = { toUSD: rate };
}
}

exchangeRates["icp"] = { ...exchangeRates["icp"], toICP: 1 };

const icpToUsd = exchangeRates["icp"]["toUSD"];
if (icpToUsd !== undefined) {
exchangeRates["ckusdc"] = { toICP: 1 / icpToUsd, toUSD: 1 };
}

return exchangeRates;
}

Expand Down
51 changes: 44 additions & 7 deletions frontend/openchat-agent/src/services/openchatAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { GroupIndexClient } from "./groupIndex/groupIndex.client";
import { MarketMakerClient } from "./marketMaker/marketMaker.client";
import { RegistryClient } from "./registry/registry.client";
import { DexesAgent } from "./dexes";
import { chunk, distinctBy, toRecord } from "../utils/list";
import { chunk, distinctBy, toRecord, toRecord2 } from "../utils/list";
import { measure } from "./common/profiling";
import {
buildBlobUrl,
Expand Down Expand Up @@ -259,6 +259,7 @@ import {
import { AnonUserClient } from "./user/anonUser.client";
import { excludeLatestKnownUpdateIfBeforeFix } from "./common/replicaUpToDateChecker";
import { IcpCoinsClient } from "./icpcoins/icpCoinsClient";
import { IcpSwapClient } from "./icpSwap/icpSwapClient";
import { IcpLedgerIndexClient } from "./icpLedgerIndex/icpLedgerIndex.client";
import { TranslationsClient } from "./translations/translations.client";
import { CkbtcMinterClient } from "./ckbtcMinter/ckbtcMinter";
Expand All @@ -274,6 +275,7 @@ import {
clearCache as clearReferralCache,
getCommunityReferral,
} from "../utils/referralCache";
import { mean } from "../utils/maths";

export class OpenChatAgent extends EventTarget {
private _agent: HttpAgent;
Expand All @@ -291,7 +293,7 @@ export class OpenChatAgent extends EventTarget {
private _ledgerIndexClients: Record<string, LedgerIndexClient>;
private _groupClients: Record<string, GroupClient>;
private _communityClients: Record<string, CommunityClient>;
private _icpcoinsClient: IcpCoinsClient;
private _exchangeRateClients: ExchangeRateClient[];
private _signInWithEmailClient: SignInWithEmailClient;
private _signInWithEthereumClient: SignInWithEthereumClient;
private _signInWithSolanaClient: SignInWithSolanaClient;
Expand Down Expand Up @@ -345,7 +347,10 @@ export class OpenChatAgent extends EventTarget {
config.blobUrlPattern,
);
this._dataClient = new DataClient(identity, this._agent, config);
this._icpcoinsClient = new IcpCoinsClient(identity, this._agent);
this._exchangeRateClients = [
new IcpCoinsClient(identity, this._agent),
new IcpSwapClient(identity, this._agent),
];
this.translationsClient = new TranslationsClient(
identity,
this._agent,
Expand Down Expand Up @@ -3531,10 +3536,36 @@ export class OpenChatAgent extends EventTarget {
return this._registryClient.setTokenEnabled(ledger, enabled);
}

exchangeRates(): Promise<Record<string, TokenExchangeRates>> {
return isMainnet(this.config.icUrl)
? this._icpcoinsClient.exchangeRates()
: Promise.resolve({});
async exchangeRates(): Promise<Record<string, TokenExchangeRates>> {
const supportedTokens = this._registryValue?.tokenDetails;

if (supportedTokens === undefined || !isMainnet(this.config.icUrl)) {
return Promise.resolve({});
}

const exchangeRatesFromAllProviders = await Promise.allSettled(
this._exchangeRateClients.map((c) => c.exchangeRates(supportedTokens)),
);

const grouped: Record<string, TokenExchangeRates[]> = {};
for (const response of exchangeRatesFromAllProviders) {
if (response.status === "fulfilled") {
for (const [token, exchangeRates] of Object.entries(response.value)) {
if (grouped[token] === undefined) {
grouped[token] = [];
}
grouped[token].push(exchangeRates);
}
}
}

return toRecord2(
Object.entries(grouped),
([token, _]) => token,
([_, group]) => ({
toUSD: mean(group.map((e) => e.toUSD)),
}),
);
}

reportedMessages(userId: string | undefined): Promise<string> {
Expand Down Expand Up @@ -4111,3 +4142,9 @@ export class OpenChatAgent extends EventTarget {
});
}
}

export interface ExchangeRateClient {
exchangeRates(
supportedTokens: CryptocurrencyDetails[],
): Promise<Record<string, TokenExchangeRates>>;
}
13 changes: 13 additions & 0 deletions frontend/openchat-agent/src/utils/maths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function mean(arr: (number | undefined)[]): number {
let total = 0;
let count = 0;

for (const value of arr) {
if (value !== undefined) {
total += value;
count++;
}
}

return count > 0 ? total / count : 0;
}
8 changes: 6 additions & 2 deletions frontend/openchat-client/src/stores/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ export const lastCryptoSent = {
export const enhancedCryptoLookup = derived(
[cryptoLookup, cryptoBalance, exchangeRatesLookupStore],
([$lookup, $balance, $exchangeRatesLookup]) => {
const xrICPtoDollar = $exchangeRatesLookup["icp"]?.toUSD;
const xrBTCtoDollar = $exchangeRatesLookup["btc"]?.toUSD;
const xrETHtoDollar = $exchangeRatesLookup["eth"]?.toUSD;

const xrDollarToICP = xrICPtoDollar === undefined ? 0 : 1 / xrICPtoDollar;
const xrDollarToBTC = xrBTCtoDollar === undefined ? 0 : 1 / xrBTCtoDollar;
const xrDollarToETH = xrETHtoDollar === undefined ? 0 : 1 / xrETHtoDollar;

Expand All @@ -60,8 +62,10 @@ export const enhancedCryptoLookup = derived(
const rates = $exchangeRatesLookup[symbolLower];
const xrUSD = rates?.toUSD;
const dollarBalance = xrUSD !== undefined ? xrUSD * balanceWholeUnits : undefined;
const xrICP = rates?.toICP;
const icpBalance = xrICP !== undefined ? xrICP * balanceWholeUnits : undefined;
const icpBalance =
dollarBalance !== undefined && xrDollarToICP !== undefined
? dollarBalance * xrDollarToICP
: undefined;
const btcBalance =
dollarBalance !== undefined && xrDollarToBTC !== undefined
? dollarBalance * xrDollarToBTC
Expand Down
1 change: 0 additions & 1 deletion frontend/openchat-shared/src/domain/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export type AccountTransactions = {
export type AccountTransactionResult = Failure | (Success & AccountTransactions);

export type TokenExchangeRates = {
toICP: number | undefined;
toUSD: number | undefined;
};

Expand Down

0 comments on commit ae6a91a

Please sign in to comment.