From 7e030d620a80095169feaef2d3d35befd865c9e7 Mon Sep 17 00:00:00 2001 From: Reppelin Tom Date: Thu, 1 Aug 2024 08:00:20 +0200 Subject: [PATCH 1/2] feat(location): adding implem --- .gitignore | 3 +- package.json | 1 + src/component/OracleDecoder.tsx | 15 +++++- src/component/OracleTestor.tsx | 16 +++++- src/services/rate/userClick.ts | 95 +++++++++++++++++++++++++++++++++ yarn.lock | 12 +++++ 6 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 src/services/rate/userClick.ts diff --git a/.gitignore b/.gitignore index 532eddc..aeec48b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -.env \ No newline at end of file +.env +/read \ No newline at end of file diff --git a/package.json b/package.json index 036d1dd..83e945b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^13.5.0", "@types/file-saver": "^2.0.7", "@types/react-modal": "^3.16.3", + "@upstash/redis": "^1.34.0", "crypto-browserify": "^3.12.0", "ethers": "^6.13.1", "ethers-multicall-provider": "^6.4.0", diff --git a/src/component/OracleDecoder.tsx b/src/component/OracleDecoder.tsx index 5f7e15f..de880f9 100644 --- a/src/component/OracleDecoder.tsx +++ b/src/component/OracleDecoder.tsx @@ -12,6 +12,7 @@ import CheckItemFeeds from "./common/CheckItemFeeds"; import useRouteMatch from "../hooks/testor/useRouteMatch"; import CheckItemPrice from "./common/CheckItemPrice"; import useOraclePriceCheck from "../hooks/testor/useOraclePriceCheck"; +import { initializeUser, recordQuery } from "../services/rate/userClick"; const ethLogo = "https://cdn.morpho.org/assets/chains/eth.svg"; const baseLogo = "https://cdn.morpho.org/assets/chains/base.png"; @@ -34,6 +35,7 @@ const OracleDecoder = () => { const [countdown, setCountdown] = useState(5); const [submitStarted, setSubmitStarted] = useState(false); const [triggerCheck, setTriggerCheck] = useState(false); + const [userId, setUserId] = useState(null); const [selectedNetwork, setSelectedNetwork] = useState<{ value: number; @@ -147,7 +149,18 @@ const OracleDecoder = () => { const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); resetState(); - + try { + const userClick = + (event.target as HTMLFormElement).ownerDocument.defaultView?.location + .hostname ?? "unknown"; + if (!userId) { + const newUserId = await initializeUser(userClick); + setUserId(newUserId); + } + await recordQuery(userId || "", userClick); + } catch (error) { + console.log("Error updating click count"); + } setIsSubmitting(true); setSubmitStarted(true); diff --git a/src/component/OracleTestor.tsx b/src/component/OracleTestor.tsx index 6d605d8..d8f299f 100644 --- a/src/component/OracleTestor.tsx +++ b/src/component/OracleTestor.tsx @@ -16,6 +16,7 @@ import CheckItemFeeds from "./common/CheckItemFeeds"; import { Asset } from "../hooks/types"; import useOracleDeploymentCheck from "../hooks/testor/useOracleDeploymentCheck"; import CheckItemDeployment from "./common/CheckItemDeployment"; +import { initializeUser, recordQuery } from "../services/rate/userClick"; const ethLogo = "https://cdn.morpho.org/assets/chains/eth.svg"; const baseLogo = "https://cdn.morpho.org/assets/chains/base.png"; @@ -33,7 +34,7 @@ const OracleTestor = () => { const [submitStarted, setSubmitStarted] = useState(false); const [formSubmitted, setFormSubmitted] = useState(false); const [showPayload, setShowPayload] = useState(false); - + const [userId, setUserId] = useState(null); const [selectedNetwork, setSelectedNetwork] = useState<{ value: number; label: JSX.Element; @@ -167,7 +168,18 @@ const OracleTestor = () => { event.preventDefault(); resetState(); - + try { + const userClick = + (event.target as HTMLFormElement).ownerDocument.defaultView?.location + .hostname ?? "unknown"; + if (!userId) { + const newUserId = await initializeUser(userClick); + setUserId(newUserId); + } + await recordQuery(userId || "", userClick); + } catch (error) { + console.log("Error updating click count"); + } setIsSubmitting(true); setSubmitStarted(true); setFormSubmitted(true); diff --git a/src/services/rate/userClick.ts b/src/services/rate/userClick.ts new file mode 100644 index 0000000..604a864 --- /dev/null +++ b/src/services/rate/userClick.ts @@ -0,0 +1,95 @@ +class RedisManager { + private static instance: RedisManager; + private readonly UPSTASH_REDIS_REST_URL: string; + private readonly UPSTASH_REDIS_REST_TOKEN: string; + + private constructor() { + this.UPSTASH_REDIS_REST_URL = + process.env.REACT_APP_UPSTASH_REDIS_REST_URL || ""; + this.UPSTASH_REDIS_REST_TOKEN = + process.env.REACT_APP_UPSTASH_REDIS_REST_TOKEN || ""; + + if (!this.UPSTASH_REDIS_REST_URL || !this.UPSTASH_REDIS_REST_TOKEN) { + throw new Error("Redis environment variables are not set"); + } + } + + public static getInstance(): RedisManager { + if (!RedisManager.instance) { + RedisManager.instance = new RedisManager(); + } + return RedisManager.instance; + } + + private getAuthHeaders() { + return { + Authorization: `Bearer ${this.UPSTASH_REDIS_REST_TOKEN}`, + }; + } + + public async incrementRedisValue(key: string): Promise { + try { + const response = await fetch( + `${this.UPSTASH_REDIS_REST_URL}/incr/${key}`, + { + headers: this.getAuthHeaders(), + } + ); + if (!response.ok) return null; + const data = await response.json(); + return data.result; + } catch { + return null; + } + } + + public async setRedisValue(key: string, value: string): Promise { + try { + await fetch(`${this.UPSTASH_REDIS_REST_URL}/set/${key}/${value}`, { + headers: this.getAuthHeaders(), + }); + } catch { + // Silently fail + } + } + + public async initializeUser(locationAddress: string): Promise { + try { + const totalUsers = await this.incrementRedisValue("totalUsers"); + if (totalUsers === null) return null; + const userId = `user:${totalUsers}`; + await this.setRedisValue(`${userId}:location`, locationAddress); + await this.setRedisValue(`${userId}:queries`, "0"); + return userId; + } catch { + return null; + } + } + + public async recordQuery( + userId: string, + locationAddress: string + ): Promise { + try { + await this.incrementRedisValue(`${userId}:queries`); + await this.incrementRedisValue("totalQueries"); + await this.setRedisValue(`${userId}:location`, locationAddress); + } catch { + // Silently fail + } + } +} + +// Export functions that use the RedisManager instance +export const initializeUser = async ( + locationAddress: string +): Promise => { + return RedisManager.getInstance().initializeUser(locationAddress); +}; + +export const recordQuery = async ( + userId: string, + locationAddress: string +): Promise => { + return RedisManager.getInstance().recordQuery(userId, locationAddress); +}; diff --git a/yarn.lock b/yarn.lock index 5970f8b..b8ab1c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3597,6 +3597,13 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@upstash/redis@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.34.0.tgz#f32cd53ebeeafbba7eca10f8597a573d5a2fed0d" + integrity sha512-TrXNoJLkysIl8SBc4u9bNnyoFYoILpCcFJcLyWCccb/QSUmaVKdvY0m5diZqc3btExsapcMbaw/s/wh9Sf1pJw== + dependencies: + crypto-js "^4.2.0" + "@vercel/nft@^0.27.0", "@vercel/nft@^0.27.1": version "0.27.2" resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-0.27.2.tgz#b5f7881a1c33b813fdc83e7112082411d2eb524b" @@ -5665,6 +5672,11 @@ crypto-browserify@^3.12.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" From 6cc0e9f4c86e187e391963a4002baaa843fa3a9d Mon Sep 17 00:00:00 2001 From: Reppelin Tom Date: Thu, 1 Aug 2024 09:51:38 +0200 Subject: [PATCH 2/2] feat(tracker): adding the tracker --- src/component/OracleDecoder.tsx | 8 +++--- src/component/OracleTestor.tsx | 8 +++--- src/services/rate/userClick.ts | 45 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/component/OracleDecoder.tsx b/src/component/OracleDecoder.tsx index de880f9..35f26cb 100644 --- a/src/component/OracleDecoder.tsx +++ b/src/component/OracleDecoder.tsx @@ -150,14 +150,12 @@ const OracleDecoder = () => { event.preventDefault(); resetState(); try { - const userClick = - (event.target as HTMLFormElement).ownerDocument.defaultView?.location - .hostname ?? "unknown"; + const locationAddress = window.location.hostname; if (!userId) { - const newUserId = await initializeUser(userClick); + const newUserId = await initializeUser(locationAddress); setUserId(newUserId); } - await recordQuery(userId || "", userClick); + await recordQuery(userId || "", locationAddress); } catch (error) { console.log("Error updating click count"); } diff --git a/src/component/OracleTestor.tsx b/src/component/OracleTestor.tsx index d8f299f..7b161e2 100644 --- a/src/component/OracleTestor.tsx +++ b/src/component/OracleTestor.tsx @@ -169,14 +169,12 @@ const OracleTestor = () => { resetState(); try { - const userClick = - (event.target as HTMLFormElement).ownerDocument.defaultView?.location - .hostname ?? "unknown"; + const locationAddress = window.location.hostname; if (!userId) { - const newUserId = await initializeUser(userClick); + const newUserId = await initializeUser(locationAddress); setUserId(newUserId); } - await recordQuery(userId || "", userClick); + await recordQuery(userId || "", locationAddress); } catch (error) { console.log("Error updating click count"); } diff --git a/src/services/rate/userClick.ts b/src/services/rate/userClick.ts index 604a864..3e0dcf7 100644 --- a/src/services/rate/userClick.ts +++ b/src/services/rate/userClick.ts @@ -27,6 +27,22 @@ class RedisManager { }; } + public async getRedisValue(key: string): Promise { + try { + const response = await fetch( + `${this.UPSTASH_REDIS_REST_URL}/get/${key}`, + { + headers: this.getAuthHeaders(), + } + ); + if (!response.ok) return null; + const data = await response.json(); + return data.result; + } catch { + return null; + } + } + public async incrementRedisValue(key: string): Promise { try { const response = await fetch( @@ -53,13 +69,36 @@ class RedisManager { } } + public async getUserIdByLocation( + locationAddress: string + ): Promise { + const totalUsers = await this.getRedisValue("totalUsers"); + if (totalUsers === null) return null; + + for (let i = 1; i <= parseInt(totalUsers); i++) { + const userId = `user:${i}`; + const storedLocation = await this.getRedisValue(`${userId}:location`); + if (storedLocation === locationAddress) { + return userId; + } + } + return null; + } + public async initializeUser(locationAddress: string): Promise { try { + // Check if user already exists + const existingUserId = await this.getUserIdByLocation(locationAddress); + if (existingUserId) { + return existingUserId; + } + const totalUsers = await this.incrementRedisValue("totalUsers"); if (totalUsers === null) return null; const userId = `user:${totalUsers}`; await this.setRedisValue(`${userId}:location`, locationAddress); await this.setRedisValue(`${userId}:queries`, "0"); + await this.setRedisValue(`${userId}:createdAt`, new Date().toISOString()); return userId; } catch { return null; @@ -71,9 +110,15 @@ class RedisManager { locationAddress: string ): Promise { try { + const date = new Date().toISOString().split("T")[0]; // Get current date in YYYY-MM-DD format await this.incrementRedisValue(`${userId}:queries`); + await this.incrementRedisValue(`${userId}:queries:${date}`); await this.incrementRedisValue("totalQueries"); await this.setRedisValue(`${userId}:location`, locationAddress); + await this.setRedisValue( + `${userId}:lastQueryAt`, + new Date().toISOString() + ); } catch { // Silently fail }