Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions src/services/bet.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Account} from "../domain/account";
import axios from "axios";
import AsyncLock from "async-lock";
import {toDecimal} from "./utils";
import {CacheRepository} from "./cache.repository";

export class BetDTO {
constructor(
Expand All @@ -22,11 +23,8 @@ export class BetDTO {
}

export class BetClient {

private static CACHE_MS = 1000;
private lastFetch = new Map<string, number>;
private caches = new Map<string, BetDTO[]>();
private lock = new AsyncLock();
private cacheRepository = new CacheRepository<BetDTO[]>(1000);

constructor(private readonly host: string) {
}
Expand All @@ -36,8 +34,9 @@ export class BetClient {
const now = Date.now();

// use cache
if (now < (this.lastFetch.get(game.id) ?? 0) + BetClient.CACHE_MS) {
return this.caches.get(game.id)!;
const cached: BetDTO[] | null = this.cacheRepository.get(game.id);
if (cached !== null) {
return cached
}

// fetch
Expand All @@ -47,16 +46,15 @@ export class BetClient {
return[]
});

this.lastFetch.set(game.id, now);
this.caches.set(game.id, bets);
this.cacheRepository.set(game.id, bets);
return bets;
});
}

private async fetch(game: Game, account: Account): Promise<BetDTO[]> {
const res = await axios.get(`${this.host}/allround/${game.contract.id}/${account.address}`)
.then(res => res.status == 200 ? res.data : []);

return res.map((bet: any) => new BetDTO(
bet.side,
bet.sideMultipleChoice,
Expand Down
69 changes: 31 additions & 38 deletions src/services/blockchain.client.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Game, GameType } from "../domain/game";
import { Round, RoundPrice, RoundStatus } from "../domain/round";
import {Game, GameType} from "../domain/game";
import {Round, RoundPrice, RoundStatus} from "../domain/round";
import {
PredictChoice,
PredictPrice,
PredictPriceTypes,
Round as RoundPriceContract,
RoundChoice,
} from "../artifacts/ts";
import { addressFromContractId, subContractId, web3 } from "@alephium/web3";
import { CoinGeckoClient } from "./coinGeckoClient";
import {addressFromContractId, subContractId, web3} from "@alephium/web3";
import {CoinGeckoClient} from "./coinGeckoClient";
import AsyncLock from "async-lock";
import { contractExists, toDecimal } from "./utils";
import { PredictMultipleChoice } from "../artifacts/ts/PredictMultipleChoice";
import { RoundMultipleChoice } from "../artifacts/ts/RoundMultipleChoice";
import {contractExists, toDecimal} from "./utils";
import {PredictMultipleChoice} from "../artifacts/ts/PredictMultipleChoice";
import {RoundMultipleChoice} from "../artifacts/ts/RoundMultipleChoice";
import {CacheRepository} from "./cache.repository";

export function getRoundContractId(
predictAlphContractId: string,
Expand All @@ -27,12 +28,9 @@ function getEpochPath(epoch: bigint) {
}

export class BlockchainClient {
private static CACHE_EXP_ROUND_MS = 2000;
private static CACHE_EXP_CURRENT_EPOCH_MS = 2000;
private lastFetchCurrentRound: number = 0;
private lastFetchCurrentEpoch: number = 0;
private cachedRounds: Map<string, Round> = new Map();
private cachedCurrentEpoch: Map<string, bigint> = new Map();
private cacheRound = new CacheRepository<Round>(2000);
private cacheEpoch = new CacheRepository<bigint>(2000);
private cacheContractExist = new CacheRepository<boolean>(2000);
private lock = new AsyncLock();

constructor(
Expand All @@ -50,16 +48,11 @@ export class BlockchainClient {
}

async getCurrentRound(game: Game): Promise<Round> {
const cached = this.cachedCurrentEpoch.get(game.id);
const now = Date.now();
const cached: bigint | null = this.cacheEpoch.get(game.id);
let epoch: bigint = BigInt("0");

// Used cached current epoch from main contract state
if (
cached !== undefined &&
now - this.lastFetchCurrentEpoch <
BlockchainClient.CACHE_EXP_CURRENT_EPOCH_MS
) {
if (cached !== null ) {
epoch = cached;
return this.getRound(epoch, game);
}
Expand All @@ -86,38 +79,38 @@ export class BlockchainClient {
epoch = gameState.fields.epoch-BigInt(1)
}

this.cachedCurrentEpoch.set(game.id, epoch);
this.lastFetchCurrentEpoch = now;
this.cacheEpoch.set(game.id, epoch);
return this.getRound(epoch, game);
}

private async contractExists(address: string): Promise<boolean> {
const cached: boolean | null = this.cacheContractExist.get(address);
if (cached !== null) {
return cached;
}

const exist = await contractExists(address);
this.cacheContractExist.set(address, exist);
return exist;
}

private static key(epoch: bigint, game: Game): string {
return `${epoch}${game.id}`;
}

async getRound(epoch: bigint, game: Game): Promise<Round> {
return this.lock.acquire("GETROUND", async () => {
const cache = this.cachedRounds.get(BlockchainClient.key(epoch, game));
const cached: Round | null = this.cacheRound.get(BlockchainClient.key(epoch, game));
const now = Date.now();

// if round finished and cached return it
if (cache !== undefined && cache.status !== RoundStatus.RUNNING) {
return cache;
}

// if running round but cache not expire return it
if (
cache !== undefined &&
now - this.lastFetchCurrentRound < BlockchainClient.CACHE_EXP_ROUND_MS
) {
return cache;
// if round cached return it
if (cached !== null) {
return cached;
}

const round = await this.fetchRound(epoch, game);
if (round.status === RoundStatus.RUNNING) {
this.lastFetchCurrentRound = now;
}
this.cachedRounds.set(BlockchainClient.key(epoch, game), round);
const notExpire = round.status !== RoundStatus.RUNNING;
this.cacheRound.set(BlockchainClient.key(epoch, game), round, notExpire);
return round;
});
}
Expand Down
29 changes: 29 additions & 0 deletions src/services/cache.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {BetDTO} from "./bet.client";

export class CacheRepository<T> {
private static FAR_AWAY = 9999999999999;

private caches = new Map<string, {expireAt: number, data: T}>();

constructor(private expirationMs: number) {}

get(id: string): T | null {
if (!this.caches.has(id)) {
return null;
}

const { expireAt, data } = this.caches.get(id)!;
if (Date.now() > expireAt) {
return null;
}

return data;
}

set(id: string, data: T, notExpire = false){
const expireAt = notExpire
? CacheRepository.FAR_AWAY
: Date.now() + this.expirationMs;
this.caches.set(id, {expireAt, data});
}
}
34 changes: 19 additions & 15 deletions src/services/coinGeckoClient.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import axios from "axios";
import {CacheRepository} from "./cache.repository";

export class CoinGeckoClient {
private static readonly ONE_MINUTE = 60*1000;
private lastValues: Map<string, number> = new Map();
private lastDates: Map<string, number> = new Map();
private cacheRepository = new CacheRepository<number>(60 * 1000);

constructor() {
}

async getPriceAlph(symbol: string): Promise<number> {
if ((Date.now() - (this.lastDates.get(symbol) ?? 0)) > CoinGeckoClient.ONE_MINUTE) {
try {
const res = await axios.get(
`https://api.coingecko.com/api/v3/simple/price?ids=${symbol}&vs_currencies=usd`,
{validateStatus: (status) => true},
);
if (res.status === 200) {
this.lastValues.set(symbol, res.data[symbol].usd);
}
} catch (e) {} finally {
this.lastDates.set(symbol, Date.now());
const cached: number | null = this.cacheRepository.get(symbol);
if (cached !== null) {
return cached;
}

try {
const res = await axios.get(
`https://api.coingecko.com/api/v3/simple/price?ids=${symbol}&vs_currencies=usd`,
{validateStatus: (status) => true},
);
if (res.status === 200) {
this.cacheRepository.set(symbol, res.data[symbol].usd);
} else {
this.cacheRepository.set(symbol, 0);
}
} catch (e) {
this.cacheRepository.set(symbol, 0);
}

return this.lastValues.get(symbol) ?? 0;
return this.cacheRepository.get(symbol) ?? 0;
}
}