Skip to content

Commit

Permalink
[web] Cache Klaytn DeFi stats with Redis (cache-first for 1m & fallba…
Browse files Browse the repository at this point in the history
…ck) (#282)
  • Loading branch information
junhoyeo authored Oct 9, 2022
1 parent 47e530f commit 983be94
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 9 deletions.
10 changes: 10 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
8 changes: 8 additions & 0 deletions packages/bento-web/@types/compressed-json.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare module 'compressed-json' {
export module compress {
export function toString<T extends any>(obj: T): string;
}
export module decompress {
export function fromString<T extends any>(obj: string): T;
}
}
1 change: 1 addition & 0 deletions packages/bento-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"caver-js": "^1.9.0",
"clsx": "^1.2.1",
"cobe": "^0.6.2",
"compressed-json": "^1.0.16",
"date-fns": "^2.29.3",
"dedent": "^0.7.0",
"framer-motion": "^7.5.1",
Expand Down
110 changes: 102 additions & 8 deletions packages/bento-web/pages/api/defis/klaytn/[walletAddress].ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { safeAsyncFlatMap, safePromiseAll } from '@bento/common';
import { getTokenBalancesFromCovalent } from '@bento/core';
import { getAddress, isAddress } from '@ethersproject/address';
import CompressedJSON from 'compressed-json';
import { NextApiRequest, NextApiResponse } from 'next';

import { createRedisClient } from '@/utils/Redis';
import { withCORS } from '@/utils/middlewares/withCORS';

import {
Expand All @@ -13,6 +16,7 @@ import {
import { KlayStation } from '@/defi/klaystation';
import { KlaySwap } from '@/defi/klayswap';
import { KokonutSwap } from '@/defi/kokonutswap';
import { DeFiStaking } from '@/defi/types/staking';

interface APIRequest extends NextApiRequest {
query: {
Expand All @@ -37,11 +41,108 @@ const isSameAddress = (a: string, b: string): boolean => {
}
};

const isEthereumAddress = (addr: string): boolean => {
if (!addr.startsWith('0x')) {
return false;
}
try {
const addressWithChecksum = getAddress(addr);
if (isAddress(addressWithChecksum)) {
return true;
}
return false;
} catch {
return false;
}
};

type DeFiStakingCacheDTO = {
t: number;
v: DeFiStaking[];
};
const getCached = async <T extends any>(
__key: string,
__redisClient: ReturnType<typeof createRedisClient>,
) => {
const cachedRawValue = await __redisClient.get(__key);
if (!cachedRawValue) {
return null;
}
return CompressedJSON.decompress.fromString<T>(cachedRawValue);
};

const MINUTES = 1_000 * 60;
const CACHED_TIME = 1 * MINUTES;

const handler = async (req: APIRequest, res: NextApiResponse) => {
const wallets = parseWallets(req.query.walletAddress ?? '');

// TODO: Enumerate for all wallets
const walletAddress = wallets[0];
if (!isEthereumAddress(walletAddress)) {
res.status(400).json({ error: 'Invalid wallet address' });
return;
}

const redisClient = createRedisClient();
await redisClient.connect();

let hasError: boolean = false;
let stakings: DeFiStaking[] = [];
let cachedTime = 0;

const cached = await getCached<DeFiStakingCacheDTO>(
`defis:klaytn:${walletAddress}`,
redisClient,
);
if (cached && cached.t >= Date.now() - CACHED_TIME) {
// Use cached value if not expired
stakings = cached.v;
cachedTime = cached.t;
} else {
try {
stakings = await getDeFiStakingsByWalletAddress(walletAddress);
cachedTime = new Date().getTime();
await redisClient.set(
`defis:klaytn:${walletAddress}`,
CompressedJSON.compress.toString<DeFiStakingCacheDTO>({
v: stakings,
t: cachedTime,
}),
);
} catch (err) {
console.error(err);

if (!cached) {
hasError = true;
} else {
// Use cached value if available
stakings = cached.v;
cachedTime = cached.t;
}
}
}

await redisClient.disconnect();

if (hasError) {
res.status(500).json({ error: 'Internal server error' });
return;
}

res.status(200).json({ stakings, cachedTime });
};

export default withCORS(handler);

const handleError = (error: any) => {
console.error(error);
return [];
};

const getDeFiStakingsByWalletAddress = async (
walletAddress: string,
): Promise<DeFiStaking[]> => {
const [tokenBalances, dynamicLeveragePools] = await Promise.all([
getTokenBalancesFromCovalent({
chainId: klaytnChain.chainId,
Expand Down Expand Up @@ -121,12 +222,5 @@ const handler = async (req: APIRequest, res: NextApiResponse) => {
])
).flat();

res.status(200).json(stakings);
};

export default withCORS(handler);

const handleError = (error: any) => {
console.error(error);
return [];
return stakings;
};
3 changes: 2 additions & 1 deletion packages/bento-web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"paths": {
"@/*": ["*"]
},
"incremental": true
"incremental": true,
"typeRoots": ["@types"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,7 @@ __metadata:
caver-js: ^1.9.0
clsx: ^1.2.1
cobe: ^0.6.2
compressed-json: ^1.0.16
date-fns: ^2.29.3
dedent: ^0.7.0
env-cmd: ^10.1.0
Expand Down Expand Up @@ -5037,6 +5038,13 @@ __metadata:
languageName: node
linkType: hard

"compressed-json@npm:^1.0.16":
version: 1.0.16
resolution: "compressed-json@npm:1.0.16"
checksum: ef3823d27807e504661a1cb1cb4518e6f55be9e483ee0ebb8ed20ab443cd9806e7dc6eb604d265d942cb58abd792092ecb6aa99e3ee4bdc51535ec1c17afe76a
languageName: node
linkType: hard

"concat-map@npm:0.0.1":
version: 0.0.1
resolution: "concat-map@npm:0.0.1"
Expand Down

0 comments on commit 983be94

Please sign in to comment.