From 45c2621b6090066af7c43011a4d9c60c95ba975c Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Tue, 31 Dec 2024 03:46:09 -0400 Subject: [PATCH] feat: add web rtc initial config --- .../mobile/app/onboarding/camera-scan.tsx | 47 ++++- .../server/src/queries/twitter/twitter.ts | 34 +--- packages/server/src/utils/index.ts | 1 + packages/server/src/utils/redis.ts | 29 +++ packages/trpc/package.json | 1 + packages/trpc/src/api.ts | 67 ++++++- packages/trpc/src/index.ts | 1 + packages/trpc/src/web-rtc.ts | 148 +++++++++++++++ packages/web/.env | 3 + packages/web/package.json | 1 + packages/web/pages/mobile-testing.tsx | 131 +++++++++++++ packages/web/server/api/edge-router.ts | 2 + packages/web/server/api/trpc.ts | 5 +- yarn.lock | 179 +++--------------- 14 files changed, 457 insertions(+), 192 deletions(-) create mode 100644 packages/server/src/utils/redis.ts create mode 100644 packages/trpc/src/web-rtc.ts create mode 100644 packages/web/pages/mobile-testing.tsx diff --git a/packages/mobile/app/onboarding/camera-scan.tsx b/packages/mobile/app/onboarding/camera-scan.tsx index e8d728a117..6ee1e97342 100644 --- a/packages/mobile/app/onboarding/camera-scan.tsx +++ b/packages/mobile/app/onboarding/camera-scan.tsx @@ -14,7 +14,7 @@ const CAMERA_ASPECT_RATIO = 4 / 3; const SCAN_ICON_WIDTH_RATIO = 0.7; export default function Welcome() { - const { top } = useSafeAreaInsets(); + const { top, bottom } = useSafeAreaInsets(); const [autoFocus, setAutoFocus] = useState("off"); const resetCameraAutoFocus = () => { @@ -35,12 +35,21 @@ export default function Welcome() { Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO; return ( - + @@ -73,6 +82,7 @@ export default function Welcome() { width: scannerSize, height: scannerSize, position: "relative", + marginBottom: 48, }} /> @@ -83,13 +93,14 @@ export default function Welcome() { onBarcodeScanned={({ data }) => { console.log(data); }} + autofocus={autoFocus} /> - Scan your QR Code - - With your wallet connected, navigate{"\n"} - to 'Profile' > 'Link mobile device' on{"\n"} - Osmosis web app. - + + Scan your QR Code + + With your wallet connected, navigate{"\n"} + to 'Profile' > 'Link mobile device' on{"\n"} + Osmosis web app. + + ); } diff --git a/packages/server/src/queries/twitter/twitter.ts b/packages/server/src/queries/twitter/twitter.ts index e6b701bfe8..42688e4cfc 100644 --- a/packages/server/src/queries/twitter/twitter.ts +++ b/packages/server/src/queries/twitter/twitter.ts @@ -1,13 +1,9 @@ import { apiClient } from "@osmosis-labs/utils"; import { Redis } from "@upstash/redis"; -import { Cache, CacheEntry, cachified } from "cachified"; +import { Cache, cachified } from "cachified"; -import { - KV_STORE_REST_API_TOKEN, - KV_STORE_REST_API_URL, - TWITTER_API_ACCESS_TOKEN, - TWITTER_API_URL, -} from "../../env"; +import { TWITTER_API_ACCESS_TOKEN, TWITTER_API_URL } from "../../env"; +import { getRedisClient, redisKvStoreAdapter } from "../../utils"; interface RawUser { id: string; @@ -48,23 +44,6 @@ export interface RichTweet { previewImage: string | null; } -const DEFAULT_TTL = 1000 * 60 * 60 * 24 * 7; - -const kvStoreAdapter = (store: Redis): Cache => ({ - set: (key: string, value: CacheEntry) => { - value.metadata.ttl; - return store.set(key, value, { - px: value.metadata.ttl ?? DEFAULT_TTL, - }); - }, - get: (key: string) => { - return store.get(key); - }, - delete: (key) => { - return store.del(key); - }, -}); - export class Twitter { /** * Expire time in milliseconds. @@ -85,11 +64,8 @@ export class Twitter { */ constructor(cacheExpireTime: number = DEFAULT_TTL) { this.cacheExpireTime = cacheExpireTime; - this.kvStore = new Redis({ - url: KV_STORE_REST_API_URL!, - token: KV_STORE_REST_API_TOKEN!, - }); - this.cache = kvStoreAdapter(this.kvStore); + this.kvStore = getRedisClient(); + this.cache = redisKvStoreAdapter(this.kvStore); } /** diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 6a595b948f..c51d870d1f 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1,6 +1,7 @@ export * from "./cache"; export * from "./error"; export * from "./pagination"; +export * from "./redis"; export * from "./search"; export * from "./sort"; export * from "./superjson"; diff --git a/packages/server/src/utils/redis.ts b/packages/server/src/utils/redis.ts new file mode 100644 index 0000000000..f66fe8eecb --- /dev/null +++ b/packages/server/src/utils/redis.ts @@ -0,0 +1,29 @@ +import { Redis } from "@upstash/redis"; +import { Cache, CacheEntry } from "cachified"; + +import { KV_STORE_REST_API_URL } from "../env"; +import { KV_STORE_REST_API_TOKEN } from "../env"; + +const DEFAULT_TTL = 1000 * 60 * 60 * 24 * 7; + +export function getRedisClient() { + return new Redis({ + url: KV_STORE_REST_API_URL!, + token: KV_STORE_REST_API_TOKEN!, + }); +} + +export const redisKvStoreAdapter = (store: Redis): Cache => ({ + set: (key: string, value: CacheEntry) => { + value.metadata.ttl; + return store.set(key, value, { + px: value.metadata.ttl ?? DEFAULT_TTL, + }); + }, + get: (key: string) => { + return store.get(key); + }, + delete: (key) => { + return store.del(key); + }, +}); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index a23ec569f2..92137dff72 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -39,6 +39,7 @@ "@osmosis-labs/utils": "^1.0.0", "@trpc/client": "^10.45.1", "@trpc/server": "^10.45.1", + "@upstash/ratelimit": "^2.0.5", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/trpc/src/api.ts b/packages/trpc/src/api.ts index 7618de60a9..320eb30bc0 100644 --- a/packages/trpc/src/api.ts +++ b/packages/trpc/src/api.ts @@ -1,5 +1,10 @@ import { SpanStatusCode, trace } from "@opentelemetry/api"; -import { superjson } from "@osmosis-labs/server"; +import { + getRedisClient, + KV_STORE_REST_API_TOKEN, + KV_STORE_REST_API_URL, + superjson, +} from "@osmosis-labs/server"; import { AssetList, Chain } from "@osmosis-labs/types"; import { timeout } from "@osmosis-labs/utils"; import { @@ -10,8 +15,9 @@ import { TRPCClientError, TRPCLink, } from "@trpc/client"; -import { type AnyRouter, initTRPC } from "@trpc/server"; +import { type AnyRouter, initTRPC, TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; +import { Ratelimit } from "@upstash/ratelimit"; import { ZodError } from "zod"; /** @@ -21,6 +27,7 @@ type CreateContextOptions = { assetLists: AssetList[]; chainList: Chain[]; opentelemetryServiceName: string | undefined; + req?: Request; }; /** @@ -130,6 +137,62 @@ export const publicProcedure = t.procedure return result; }); +let rateLimiter: Ratelimit | undefined; + +const getRateLimiter = () => { + if (!rateLimiter && KV_STORE_REST_API_URL && KV_STORE_REST_API_TOKEN) { + rateLimiter = new Ratelimit({ + redis: getRedisClient(), + limiter: Ratelimit.slidingWindow(10, "10 s"), + analytics: true, + }); + } + return rateLimiter; +}; + +/** + * Our middleware that uses the request's IP address + * as the unique identifier for rate limiting. + */ +export const rateLimitedProcedure = publicProcedure.use( + async ({ ctx, next }) => { + // Extract IP from ctx. + // "x-forwarded-for" often contains a comma-separated list + // of IPs if there are multiple proxies. + // For example, "x-forwarded-for: clientIP, proxyIP, ...". + // You might adjust logic or parse for a real client IP. + const ip = ctx.req?.headers.get("x-forwarded-for"); + + const rateLimiter = getRateLimiter(); + + if (!ip) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "IP address not found", + }); + } + + if (!rateLimiter) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Rate limiter not configured", + }); + } + + // Rate limit on IP address + const { success } = await rateLimiter.limit(ip); + + if (!success) { + throw new TRPCError({ + code: "TOO_MANY_REQUESTS", + message: "Too many requests", + }); + } + + return next(); + } +); + export const createCallerFactory = t.createCallerFactory; /** diff --git a/packages/trpc/src/index.ts b/packages/trpc/src/index.ts index cf574164e4..54a110b8f9 100644 --- a/packages/trpc/src/index.ts +++ b/packages/trpc/src/index.ts @@ -14,3 +14,4 @@ export * from "./portfolio"; export * from "./staking"; export * from "./swap"; export * from "./transactions"; +export * from "./web-rtc"; diff --git a/packages/trpc/src/web-rtc.ts b/packages/trpc/src/web-rtc.ts new file mode 100644 index 0000000000..a5c8d07b31 --- /dev/null +++ b/packages/trpc/src/web-rtc.ts @@ -0,0 +1,148 @@ +import { getRedisClient } from "@osmosis-labs/server"; +import { z } from "zod"; + +import { createTRPCRouter, rateLimitedProcedure } from "./api"; + +const SessionDataSchema = z.object({ + offerSDP: z.string().optional(), + answerSDP: z.string().optional(), + candidates: z.array(z.string()).optional(), + createdAt: z.number(), +}); + +type SessionData = z.infer; + +const TTL_SECONDS = 5 * 60; // 5 minutes + +function getSessionKey(sessionToken: string) { + return `session:${sessionToken}`; +} + +export const webRTCRouter = createTRPCRouter({ + // 1. Desktop: create offer + createOffer: rateLimitedProcedure + .input( + z.object({ + sessionToken: z.string(), + offerSDP: z.string(), + }) + ) + .mutation(async ({ input }) => { + const sessionKey = getSessionKey(input.sessionToken); + const sessionData = SessionDataSchema.parse({ + offerSDP: input.offerSDP, + candidates: [], + createdAt: Date.now(), + }); + + await getRedisClient().set(sessionKey, JSON.stringify(sessionData), { + ex: TTL_SECONDS, + }); + return { success: true }; + }), + + // 2. Mobile: fetch offer + fetchOffer: rateLimitedProcedure + .input( + z.object({ + sessionToken: z.string(), + }) + ) + .query(async ({ input }) => { + const sessionKey = getSessionKey(input.sessionToken); + const data = await getRedisClient().get(sessionKey); + if (!data) return { offerSDP: null }; + + const validatedData = SessionDataSchema.parse(data); + return { offerSDP: validatedData.offerSDP ?? null }; + }), + + // 3. Mobile: post answer + postAnswer: rateLimitedProcedure + .input( + z.object({ + sessionToken: z.string(), + answerSDP: z.string(), + }) + ) + .mutation(async ({ input }) => { + const sessionKey = getSessionKey(input.sessionToken); + const existingData = await getRedisClient().get(sessionKey); + if (!existingData) { + return { success: false, error: "Session not found" }; + } + + const validatedExistingData = SessionDataSchema.parse(existingData); + const updatedData = SessionDataSchema.parse({ + ...validatedExistingData, + answerSDP: input.answerSDP, + }); + + await getRedisClient().set(sessionKey, JSON.stringify(updatedData), { + ex: TTL_SECONDS, + }); + return { success: true }; + }), + + // 4. Desktop: fetch answer + fetchAnswer: rateLimitedProcedure + .input( + z.object({ + sessionToken: z.string(), + }) + ) + .query(async ({ input }) => { + const sessionKey = getSessionKey(input.sessionToken); + const data = await getRedisClient().get(sessionKey); + if (!data) return { answerSDP: null }; + + const validatedData = SessionDataSchema.parse(data); + return { answerSDP: validatedData.answerSDP ?? null }; + }), + + // 5. Either side: post ICE candidate + postCandidate: rateLimitedProcedure + .input( + z.object({ + sessionToken: z.string(), + candidate: z.string(), + }) + ) + .mutation(async ({ input }) => { + const sessionKey = getSessionKey(input.sessionToken); + const existingData = await getRedisClient().get(sessionKey); + if (!existingData) { + return { success: false, error: "Session not found" }; + } + + const validatedExistingData = SessionDataSchema.parse(existingData); + const updatedData = SessionDataSchema.parse({ + ...validatedExistingData, + candidates: [ + ...(validatedExistingData.candidates ?? []), + input.candidate, + ], + }); + + await getRedisClient().set(sessionKey, JSON.stringify(updatedData), { + ex: TTL_SECONDS, + }); + return { success: true }; + }), + + // 6. Either side: fetch ICE candidates + fetchCandidates: rateLimitedProcedure + .input( + z.object({ + sessionToken: z.string(), + }) + ) + .query(async ({ input }) => { + const sessionKey = getSessionKey(input.sessionToken); + const data = await getRedisClient().get(sessionKey); + if (!data) return { candidates: [] }; + + const validatedData = SessionDataSchema.parse(data); + return { candidates: validatedData.candidates ?? [] }; + }), +}); diff --git a/packages/web/.env b/packages/web/.env index 71442d636a..44d82c4466 100644 --- a/packages/web/.env +++ b/packages/web/.env @@ -4,6 +4,9 @@ # NOTE: turbo build already invalidates build cache with these values, no need to add to turbo.json. See: https://turbo.build/repo/docs/reference/configuration#env +# Vercel KV +# KV_STORE_REST_API_URL= +# KV_STORE_REST_API_TOKEN= GITHUB_URL=https://raw.githubusercontent.com/osmosis-labs/ CMS_REPOSITORY_PATH=assetlists/main/osmosis-1/generated/asset_detail diff --git a/packages/web/package.json b/packages/web/package.json index 36fc92c40a..cf164755f6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -127,6 +127,7 @@ "sha.js": "^2.4.11", "sharp": "^0.30.4", "utility-types": "^3.10.0", + "uuid": "^11.0.3", "viem": "^2.21.19", "wagmi": "^2.12.17", "zod": "^3.22.4", diff --git a/packages/web/pages/mobile-testing.tsx b/packages/web/pages/mobile-testing.tsx new file mode 100644 index 0000000000..d831dbd023 --- /dev/null +++ b/packages/web/pages/mobile-testing.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; + +import { QRCode } from "~/components/qrcode"; +import { api } from "~/utils/trpc"; + +/** + * STUN server configuration for WebRTC peer connections. + * Enables NAT traversal and peer discovery through Google's public STUN server. + * This allows peers behind different networks/firewalls to establish direct connections. + * The server runs on Google's infrastructure at stun.l.google.com:19302 + */ +const STUN_SERVER = { urls: "stun:stun.l.google.com:19302" }; + +export default function DesktopPage() { + const [sessionToken, setSessionToken] = useState(""); + const [qrValue, setQrValue] = useState(""); + const [pc, setPc] = useState(null); + const [isReady, setIsReady] = useState(false); + + const createOfferMutation = api.edge.webRTC.createOffer.useMutation(); + const postCandidateMutation = api.edge.webRTC.postCandidate.useMutation(); + + // Poll for the answer + const fetchAnswerQuery = api.edge.webRTC.fetchAnswer.useQuery( + { sessionToken }, + { + enabled: !!sessionToken, + refetchInterval: 3000, + } + ); + + // Poll for candidates + const fetchCandidatesQuery = api.edge.webRTC.fetchCandidates.useQuery( + { sessionToken }, + { + enabled: !!sessionToken, + refetchInterval: 3000, + } + ); + + useEffect(() => { + // 1. Generate session token + const token = uuidv4(); + setSessionToken(token); + + // 2. Create RTCPeerConnection + const peer = new RTCPeerConnection({ iceServers: [STUN_SERVER] }); + + // Create data channel if you want + const dc = peer.createDataChannel("keyTransferChannel"); + dc.onopen = () => { + console.log("[Desktop] Data channel open, can send data now."); + }; + dc.onmessage = (e) => { + console.log("[Desktop] Received from phone:", e.data); + }; + + // 3. ICE candidate handling + peer.onicecandidate = async (event) => { + if (event.candidate && sessionToken) { + // Send candidate to server + await postCandidateMutation.mutateAsync({ + sessionToken, + candidate: JSON.stringify(event.candidate), + }); + } + }; + + // 4. Create local offer + (async () => { + const offer = await peer.createOffer(); + await peer.setLocalDescription(offer); + // Store on the server + await createOfferMutation.mutateAsync({ + sessionToken: token, + offerSDP: offer.sdp ?? "", + }); + // 5. Create QR code with sessionToken + server URL + const payload = { + sessionToken: token, + serverUrl: "https://your-deployed-domain.com/trpc", + // or wherever your tRPC server is + }; + setQrValue(JSON.stringify(payload)); + setIsReady(true); + })(); + + setPc(peer); + + // Should only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // 6. If we have an answer from fetchAnswer, set remote desc + useEffect(() => { + const answerSDP = fetchAnswerQuery.data?.answerSDP; + if (answerSDP && pc) { + console.log("[Desktop] Setting remote description with answerSDP..."); + pc.setRemoteDescription({ type: "answer", sdp: answerSDP }); + } + }, [fetchAnswerQuery.data, pc]); + + // 7. If we have ICE candidates from fetchCandidates, add them + useEffect(() => { + const candidateList = fetchCandidatesQuery.data?.candidates ?? []; + if (candidateList && pc) { + candidateList.forEach(async (cStr) => { + try { + const candidate = new RTCIceCandidate(JSON.parse(cStr)); + await pc.addIceCandidate(candidate); + } catch (e) { + console.error("Failed to add ICE candidate:", e); + } + }); + } + }, [fetchCandidatesQuery.data, pc]); + + return ( +
+

Desktop WebRTC Transfer

+ {!isReady &&

Generating offer...

} + {isReady && ( +
+

Scan this QR code with your mobile:

+ +
+ )} +
+ ); +} diff --git a/packages/web/server/api/edge-router.ts b/packages/web/server/api/edge-router.ts index d064c68daa..873d0f8eb4 100644 --- a/packages/web/server/api/edge-router.ts +++ b/packages/web/server/api/edge-router.ts @@ -7,6 +7,7 @@ import { poolsRouter, stakingRouter, transactionsRouter, + webRTCRouter, } from "@osmosis-labs/trpc"; /** Contains tRPC functions running on Vercel's edge network. */ @@ -18,4 +19,5 @@ export const edgeRouter = createTRPCRouter({ transactions: transactionsRouter, orderbooks: orderbookRouter, chains: chainsRouter, + webRTC: webRTCRouter, }); diff --git a/packages/web/server/api/trpc.ts b/packages/web/server/api/trpc.ts index 5b175ede1a..a66eb7a343 100644 --- a/packages/web/server/api/trpc.ts +++ b/packages/web/server/api/trpc.ts @@ -7,7 +7,7 @@ import { ChainList } from "~/config/generated/chain-list"; import { getOpentelemetryServiceName } from "~/utils/service-name"; /** tRPC context for Next.js endpoints. */ -export const createNextTrpcContext = (_opts: CreateNextContextOptions) => { +export const createNextTrpcContext = (opts: CreateNextContextOptions) => { return createInnerTRPCContext({ assetLists: AssetLists, chainList: ChainList, @@ -16,10 +16,11 @@ export const createNextTrpcContext = (_opts: CreateNextContextOptions) => { }; /** tRPC context for Next.js endpoints running on Vercel's edge runtime. */ -export const createEdgeTrpcContext = (_opts: FetchCreateContextFnOptions) => { +export const createEdgeTrpcContext = (opts: FetchCreateContextFnOptions) => { return createInnerTRPCContext({ assetLists: AssetLists, chainList: ChainList, opentelemetryServiceName: getOpentelemetryServiceName(), + req: opts.req, }); }; diff --git a/yarn.lock b/yarn.lock index bcb9142df5..927310e578 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5483,29 +5483,7 @@ buffer "^6.0.3" delay "^4.4.0" -"@keplr-wallet/common@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.12.tgz#55030d985b729eac582c0d7203190e25ea2cb3ec" - integrity sha512-AxpwmXdqs083lMvA8j0/V30oTGyobsefNaCou+lP4rCyDdYuXSEux+x2+1AGL9xB3yZfN+4jvEEKJdMwHYEHcQ== - dependencies: - "@keplr-wallet/crypto" "0.12.12" - "@keplr-wallet/types" "0.12.12" - buffer "^6.0.3" - delay "^4.4.0" - mobx "^6.1.7" - -"@keplr-wallet/common@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.28.tgz#1d5d985070aced31a34a6426c9ac4b775081acca" - integrity sha512-ESQorPZw8PRiUXhsrxED+E1FEWkAdc6Kwi3Az7ce204gMBQDI2j0XJtTd4uCUp+C24Em9fk0samdHzdoB4caIg== - dependencies: - "@keplr-wallet/crypto" "0.12.28" - "@keplr-wallet/types" "0.12.28" - buffer "^6.0.3" - delay "^4.4.0" - mobx "^6.1.7" - -"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos@0.12.12", "@keplr-wallet/cosmos@0.12.28": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-/A/wHyYo5gQIW5YkAQYZadEv/12EcAuDclO0KboIb9ti4XFJW6S4VY8LnA16R7DZyBx1cnQknyDm101fUrJfJQ== @@ -5522,40 +5500,6 @@ long "^4.0.0" protobufjs "^6.11.2" -"@keplr-wallet/cosmos@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.12.tgz#72c0505d2327bbf2f5cb51502acaf399b88b4ae3" - integrity sha512-9TLsefUIAuDqqf1WHBt9Bk29rPlkezmLM8P1eEsXGUaHBfuqUrO+RwL3eLA3HGcgNvdy9s8e0p/4CMInH/LLLQ== - dependencies: - "@ethersproject/address" "^5.6.0" - "@keplr-wallet/common" "0.12.12" - "@keplr-wallet/crypto" "0.12.12" - "@keplr-wallet/proto-types" "0.12.12" - "@keplr-wallet/simple-fetch" "0.12.12" - "@keplr-wallet/types" "0.12.12" - "@keplr-wallet/unit" "0.12.12" - bech32 "^1.1.4" - buffer "^6.0.3" - long "^4.0.0" - protobufjs "^6.11.2" - -"@keplr-wallet/cosmos@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.28.tgz#d56e73468256e7276a66bb41f145449dbf11efa1" - integrity sha512-IuqmSBgKgIeWBA0XGQKKs28IXFeFMCrfadCbtiZccNc7qnNr5Y/Cyyk01BPC8Dd1ZyEyAByoICgrxvtGN0GGvA== - dependencies: - "@ethersproject/address" "^5.6.0" - "@keplr-wallet/common" "0.12.28" - "@keplr-wallet/crypto" "0.12.28" - "@keplr-wallet/proto-types" "0.12.28" - "@keplr-wallet/simple-fetch" "0.12.28" - "@keplr-wallet/types" "0.12.28" - "@keplr-wallet/unit" "0.12.28" - bech32 "^1.1.4" - buffer "^6.0.3" - long "^4.0.0" - protobufjs "^6.11.2" - "@keplr-wallet/crypto@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -5628,7 +5572,7 @@ resolved "https://registry.npmjs.org/@keplr-wallet/popup/-/popup-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-Q/teyV6vdmpH3SySGd1xrNc/mVGK/tCP5vFEG2I3Y4FDCSV1yD7vcVgUy+tN19Z8EM3goR57V2QlarSOidtdjQ== -"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/proto-types@0.12.12": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-fLUJEtDadYJIMBzhMSZpEDTvXqk8wW68TwnUCRAcAooEQEtXPwY5gfo3hcekQEiCYtIu8XqzJ9fg01rp2Z4d3w== @@ -5636,22 +5580,6 @@ long "^4.0.0" protobufjs "^6.11.2" -"@keplr-wallet/proto-types@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.12.tgz#24e0530af7604a90f33a397a82fe500865c76154" - integrity sha512-iAqqNlJpxu/8j+SwOXEH2ymM4W0anfxn+eNeWuqz2c/0JxGTWeLURioxQmCtewtllfHdDHHcoQ7/S+NmXiaEgQ== - dependencies: - long "^4.0.0" - protobufjs "^6.11.2" - -"@keplr-wallet/proto-types@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.28.tgz#2fb2c37749ce7db974f01d07387e966c9b99027d" - integrity sha512-ukti/eCTltPUP64jxtk5TjtwJogyfKPqlBIT3KGUCGzBLIPeYMsffL5w5aoHsMjINzOITjYqzXyEF8LTIK/fmw== - dependencies: - long "^4.0.0" - protobufjs "^6.11.2" - "@keplr-wallet/provider-extension@^0.12.95": version "0.12.107" resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-extension/-/provider-extension-0.12.107.tgz#98a0fb42cb0c54d4e681e60e6b1145429a6e3e23" @@ -5727,32 +5655,12 @@ deepmerge "^4.2.2" long "^4.0.0" -"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/router@0.12.12", "@keplr-wallet/router@0.12.96": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/router/-/router-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-bt9weexlbhlh8KsOvbDrvHJ8jtUXrXgB2LX+hEAwjclHQt7PMUhx9a5z0Obd19/ive5G/1M7/ccdPIWxRBpKQw== -"@keplr-wallet/router@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.12.tgz#92a2c006aec6945ed313575af6b0801f8e84e315" - integrity sha512-Aa1TiVRIEPaqs1t27nCNs5Kz6Ty4CLarVdfqcRWlFQL6zFq33GT46s6K9U4Lz2swVCwdmerSXaq308K/GJHTlw== - -"@keplr-wallet/router@0.12.96": - version "0.12.96" - resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.96.tgz#6a20ed2c90ba3ed4f3fc43ed7513f72d7055482d" - integrity sha512-O8izj032ZKQIoTus96BFqem+w6NpYHU3j6NEnSaQBh6Zncj9fgjoOVs0CKK+jsuLYUsOHx2t86BxMSKESsR0Ug== - -"@keplr-wallet/simple-fetch@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.12.tgz#aacc5c3f22b7ab2804b39e864725294a32f858fd" - integrity sha512-lCOsaI8upMpbusfwJqEK8VIEX77+QE8+8MJVRqoCYwjOTqKGdUH7D1ieZWh+pzvzOnVgedM3lxqdmCvdgU91qw== - -"@keplr-wallet/simple-fetch@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.28.tgz#44225df5b329c823076280df1ec9930a21b1373e" - integrity sha512-T2CiKS2B5n0ZA7CWw0CA6qIAH0XYI1siE50MP+i+V0ZniCGBeL+BMcDw64vFJUcEH+1L5X4sDAzV37fQxGwllA== - -"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/types@0.12.107", "@keplr-wallet/types@0.12.12", "@keplr-wallet/types@0.12.96", "@keplr-wallet/types@^0.12.95": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-3KUjDMUCscYkvKnC+JsJh9+X0NHlsvBgAghP/uy2p5OGtiULqPBAjWiO+hnBbhis3ZEkzGcCROnnBOoccKd3CQ== @@ -5763,41 +5671,6 @@ long "^4.0.0" secretjs "^0.17.0" -"@keplr-wallet/types@0.12.107": - version "0.12.107" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.107.tgz#8d6726d86e17a79131b4b6f4f114052d6384aa58" - integrity sha512-jBpjJO+nNL8cgsJLjZYoq84n+7nXHDdztTgRMVnnomFb+Vy0FVIEI8VUl89ImmHDUImDd0562ywsvA496/0yCA== - dependencies: - long "^4.0.0" - -"@keplr-wallet/types@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.12.tgz#f4bd9e710d5e53504f6b53330abb45bedd9c20ae" - integrity sha512-fo6b8j9EXnJukGvZorifJWEm1BPIrvaTLuu5PqaU5k1ANDasm/FL1NaUuaTBVvhRjINtvVXqYpW/rVUinA9MBA== - dependencies: - long "^4.0.0" - -"@keplr-wallet/types@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.28.tgz#eac3c2c9d4560856c5c403a87e67925992a04fbf" - integrity sha512-EcM9d46hYDm3AO4lf4GUbTSLRySONtTmhKb7p88q56OQOgJN3MMjRacEo2p9jX9gpPe7gRIjMUalhAfUiFpZoQ== - dependencies: - long "^4.0.0" - -"@keplr-wallet/types@0.12.96": - version "0.12.96" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.96.tgz#a7735051b1f7cbcdf9b8c29010b1c3c45d195c19" - integrity sha512-tr4tPjMrJCsfRXXhhmqnpb9DqH9auJp3uuj8SvDB3pQTTaYJNxkdonLv1tYmXZZ6J9oWtk9WVEDTVgBQN/wisw== - dependencies: - long "^4.0.0" - -"@keplr-wallet/types@^0.12.95": - version "0.12.169" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.169.tgz#1c3d0095fec660f6c28e8790b056f1888a6dab6c" - integrity sha512-+m/LnQ6sQKqClyaAeWFKdxmGqgbfqlhOv4Nt10domwsf0c5731W8IEQqkaQJXzol9/iyj6Zh50hM1Irar/TY1w== - dependencies: - long "^4.0.0" - "@keplr-wallet/unit@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -5807,24 +5680,6 @@ big-integer "^1.6.48" utility-types "^3.10.0" -"@keplr-wallet/unit@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.12.tgz#2d7f2e38df4e09c8123dcc0784ffc4b5f4166217" - integrity sha512-fayJcfXWKUnbDZiRJHyuA9GMVS9DymjRlCzlpAJ0+xV0c4Kun/f+9FajL9OQAdPPhnJ7A3KevMI4VHZsd9Yw+A== - dependencies: - "@keplr-wallet/types" "0.12.12" - big-integer "^1.6.48" - utility-types "^3.10.0" - -"@keplr-wallet/unit@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.28.tgz#907c7fa0b49a729cda207fca14fc0a38871cc6c4" - integrity sha512-kpXigHDBJGOmhtPkv9hqsQid9zkFo7OQPeKgO2n8GUlOINIXW6kWG5LXYTi/Yg9Uiw1CQF69gFMuZCJ8IzVHlA== - dependencies: - "@keplr-wallet/types" "0.12.28" - big-integer "^1.6.48" - utility-types "^3.10.0" - "@keplr-wallet/wc-client@^0.12.95": version "0.12.96" resolved "https://registry.yarnpkg.com/@keplr-wallet/wc-client/-/wc-client-0.12.96.tgz#a56995172dcdc73d32b24d5a704a954062befc2a" @@ -9422,6 +9277,27 @@ "@typescript-eslint/types" "7.4.0" eslint-visitor-keys "^3.4.1" +"@upstash/core-analytics@^0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@upstash/core-analytics/-/core-analytics-0.0.10.tgz#e686a313ec2279d5a8d53e6c215085f1c0f5ab4b" + integrity sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ== + dependencies: + "@upstash/redis" "^1.28.3" + +"@upstash/ratelimit@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@upstash/ratelimit/-/ratelimit-2.0.5.tgz#0e8e693b79bdcf5d8643c38bebe8b76e7b79b54a" + integrity sha512-1FRv0cs3ZlBjCNOCpCmKYmt9BYGIJf0J0R3pucOPE88R21rL7jNjXG+I+rN/BVOvYJhI9niRAS/JaSNjiSICxA== + dependencies: + "@upstash/core-analytics" "^0.0.10" + +"@upstash/redis@^1.28.3": + version "1.34.3" + resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.34.3.tgz#df0338f4983bba5141878e851be4fced494b44a0" + integrity sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ== + dependencies: + crypto-js "^4.2.0" + "@upstash/redis@^1.31.5": version "1.31.5" resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.31.5.tgz#8d5fe439a2a28638b3a354a23680ecf7f7eb4f54" @@ -25771,6 +25647,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"