diff --git a/.env.sandbox.docker-compose-dev b/.env.sandbox.docker-compose-dev index 2fac03e61..0b87791a1 100644 --- a/.env.sandbox.docker-compose-dev +++ b/.env.sandbox.docker-compose-dev @@ -5,13 +5,14 @@ Network=sandbox ActiveChain=akashSandbox # Deploy Web -API_BASE_URL: http://api:3080 +API_BASE_URL: http://api:3000 +BASE_API_MAINNET_URL: http://api:3000 PROVIDER_PROXY_URL: http://provider-proxy:3040 # Stats Web -API_MAINNET_BASE_URL: http://api:3080 -API_TESTNET_BASE_URL: http://api:3080 -API_SANDBOX_BASE_URL: http://api:3080 +API_MAINNET_BASE_URL: http://api:3000 +API_TESTNET_BASE_URL: http://api:3000 +API_SANDBOX_BASE_URL: http://api:3000 # DB POSTGRES_USER: postgres diff --git a/apps/api/package.json b/apps/api/package.json index d406fbd84..f500fbeae 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -50,7 +50,7 @@ "@opentelemetry/sdk-node": "^0.52.1", "@sentry/node": "^7.55.2", "@supercharge/promise-pool": "^3.2.0", - "axios": "^0.27.2", + "axios": "^1.7.2", "commander": "^12.1.0", "cosmjs-types": "^0.9.0", "date-fns": "^2.29.2", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index c31cc4ab1..c16e8f13a 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -13,6 +13,8 @@ import { container } from "tsyringe"; import { HonoErrorHandlerService } from "@src/core/services/hono-error-handler/hono-error-handler.service"; import { HttpLoggerService } from "@src/core/services/http-logger/http-logger.service"; import { LoggerService } from "@src/core/services/logger/logger.service"; +import { RequestStorageInterceptor } from "@src/core/services/request-storage/request-storage.interceptor"; +import { CurrentUserInterceptor } from "@src/user/services/current-user/current-user.interceptor"; import packageJson from "../package.json"; import { chainDb, syncUserSchema, userDb } from "./db/dbConnection"; import { apiRouter } from "./routers/apiRouter"; @@ -62,6 +64,8 @@ const scheduler = new Scheduler({ }); appHono.use(container.resolve(HttpLoggerService).intercept()); +appHono.use(container.resolve(RequestStorageInterceptor).intercept()); +appHono.use(container.resolve(CurrentUserInterceptor).intercept()); appHono.use( "*", sentry({ diff --git a/apps/api/src/billing/controllers/wallet/wallet.controller.ts b/apps/api/src/billing/controllers/wallet/wallet.controller.ts index 4573ef2d3..99f994347 100644 --- a/apps/api/src/billing/controllers/wallet/wallet.controller.ts +++ b/apps/api/src/billing/controllers/wallet/wallet.controller.ts @@ -11,6 +11,7 @@ import { ManagedUserWalletService, WalletInitializerService } from "@src/billing import { TxSignerService } from "@src/billing/services/tx-signer/tx-signer.service"; import { WithTransaction } from "@src/core/services"; +// TODO: authorize endpoints below @singleton() export class WalletController { constructor( diff --git a/apps/api/src/core/providers/request.provider.ts b/apps/api/src/core/providers/request.provider.ts new file mode 100644 index 000000000..64e3a8b68 --- /dev/null +++ b/apps/api/src/core/providers/request.provider.ts @@ -0,0 +1,13 @@ +import { container, inject } from "tsyringe"; + +import { RequestStorageInterceptor } from "@src/core/services/request-storage/request-storage.interceptor"; + +const REQUEST = "REQUEST"; + +container.register(REQUEST, { + useFactory: c => { + return c.resolve(RequestStorageInterceptor).context; + } +}); + +export const Request = () => inject(REQUEST); diff --git a/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts b/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts index 86806b0f6..e1b0680b4 100644 --- a/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts +++ b/apps/api/src/core/services/postgres-logger/postgres-logger.service.ts @@ -4,13 +4,21 @@ import { format } from "sql-formatter"; import { LoggerService } from "@src/core/services/logger/logger.service"; export class PostgresLoggerService implements LogWriter { - private readonly logger = new LoggerService({ context: "POSTGRES" }); + private readonly logger: LoggerService; + + private readonly isDrizzle: boolean; + + constructor(options?: { orm: "drizzle" | "sequelize" }) { + const orm = options?.orm || "drizzle"; + this.logger = new LoggerService({ context: "POSTGRES", orm }); + this.isDrizzle = orm === "drizzle"; + } write(message: string) { - let formatted = message.replace(/^Query: /, ""); + let formatted = message.replace(this.isDrizzle ? /^Query: / : /^Executing \(default\):/, ""); if (this.logger.isPretty) { - formatted = format(message, { language: "postgresql" }); + formatted = format(formatted, { language: "postgresql" }); } this.logger.debug(formatted); diff --git a/apps/api/src/core/services/request-storage/request-storage.interceptor.ts b/apps/api/src/core/services/request-storage/request-storage.interceptor.ts new file mode 100644 index 000000000..f812f97d4 --- /dev/null +++ b/apps/api/src/core/services/request-storage/request-storage.interceptor.ts @@ -0,0 +1,35 @@ +import { Context, Next } from "hono"; +import { AsyncLocalStorage } from "node:async_hooks"; +import { singleton } from "tsyringe"; +import { v4 as uuid } from "uuid"; + +import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; + +@singleton() +export class RequestStorageInterceptor implements HonoInterceptor { + private readonly CONTEXT_KEY = "CONTEXT"; + + private readonly storage = new AsyncLocalStorage>(); + + get context() { + return this.storage.getStore()?.get(this.CONTEXT_KEY); + } + + intercept() { + return async (c: Context, next: Next) => { + const requestId = c.req.header("X-Request-Id") || uuid(); + c.set("requestId", requestId); + + await this.runWithContext(c, next); + }; + } + + private async runWithContext(context: Context, cb: () => Promise) { + return await new Promise((resolve, reject) => { + this.storage.run(new Map(), () => { + this.storage.getStore().set(this.CONTEXT_KEY, context); + cb().then(resolve).catch(reject); + }); + }); + } +} diff --git a/apps/api/src/db/dbConnection.ts b/apps/api/src/db/dbConnection.ts index 92a7f3b2a..147972eab 100644 --- a/apps/api/src/db/dbConnection.ts +++ b/apps/api/src/db/dbConnection.ts @@ -5,6 +5,7 @@ import pg from "pg"; import { Transaction as DbTransaction } from "sequelize"; import { Sequelize } from "sequelize-typescript"; +import { PostgresLoggerService } from "@src/core/services/postgres-logger/postgres-logger.service"; import { env } from "@src/utils/env"; function isValidNetwork(network: string): network is keyof typeof csMap { @@ -25,10 +26,14 @@ if (!csMap[env.Network]) { throw new Error(`Missing connection string for network: ${env.Network}`); } +const logger = new PostgresLoggerService({ orm: "sequelize" }); +const logging = (msg: string) => logger.write(msg); + pg.defaults.parseInt8 = true; export const chainDb = new Sequelize(csMap[env.Network], { dialectModule: pg, - logging: false, + logging, + logQueryParameters: true, transactionType: DbTransaction.TYPES.IMMEDIATE, define: { timestamps: false, @@ -44,7 +49,8 @@ export const chainDbs: { [key: string]: Sequelize } = Object.keys(chainDefinitio ...obj, [chain]: new Sequelize(chainDefinitions[chain].connectionString, { dialectModule: pg, - logging: false, + logging, + logQueryParameters: true, repositoryMode: true, transactionType: DbTransaction.TYPES.IMMEDIATE, define: { @@ -59,7 +65,8 @@ export const chainDbs: { [key: string]: Sequelize } = Object.keys(chainDefinitio export const userDb = new Sequelize(env.UserDatabaseCS, { dialectModule: pg, - logging: false, + logging, + logQueryParameters: true, transactionType: DbTransaction.TYPES.IMMEDIATE, define: { timestamps: false, diff --git a/apps/api/src/routers/userRouter.ts b/apps/api/src/routers/userRouter.ts index b4c4de3e2..3c043d7cd 100644 --- a/apps/api/src/routers/userRouter.ts +++ b/apps/api/src/routers/userRouter.ts @@ -114,9 +114,17 @@ userRequiredRouter.delete("/removeAddressName/:address", async c => { userRequiredRouter.post("/tokenInfo", async c => { const userId = getCurrentUserId(c); + const anonymousUserId = c.req.header("x-anonymous-user-id"); const { wantedUsername, email, emailVerified, subscribedToNewsletter } = await c.req.json(); - const settings = await getSettingsOrInit(userId, wantedUsername, email, !!emailVerified, subscribedToNewsletter); + const settings = await getSettingsOrInit({ + anonymousUserId, + userId: userId, + wantedUsername, + email: email, + emailVerified: !!emailVerified, + subscribedToNewsletter: subscribedToNewsletter + }); return c.json(settings); }); diff --git a/apps/api/src/services/db/userDataService.ts b/apps/api/src/services/db/userDataService.ts index acf56d359..1b46faa13 100644 --- a/apps/api/src/services/db/userDataService.ts +++ b/apps/api/src/services/db/userDataService.ts @@ -1,7 +1,10 @@ import { UserAddressName, UserSetting } from "@akashnetwork/database/dbSchemas/user"; +import pick from "lodash/pick"; import { Transaction } from "sequelize"; -import { getUserPlan } from "../external/stripeService"; +import { LoggerService } from "@src/core"; + +const logger = new LoggerService({ context: "UserDataService" }); function randomIntFromInterval(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); @@ -13,9 +16,7 @@ export async function checkUsernameAvailable(username: string, dbTransaction?: T } async function generateUsername(wantedUsername: string, dbTransaction?: Transaction): Promise { - const sanitized = wantedUsername.replace(/[^a-zA-Z0-9_-]/gi, ""); - - let baseUsername = sanitized; + let baseUsername = wantedUsername.replace(/[^a-zA-Z0-9_-]/gi, ""); if (baseUsername.length < 3) { baseUsername = "anonymous"; @@ -61,41 +62,82 @@ export async function updateSettings( await settings.save(); } -export async function getSettingsOrInit(userId: string, wantedUsername: string, email: string, emailVerified: boolean, subscribedToNewsletter: boolean) { - const [userSettings, created] = await UserSetting.findCreateFind({ - where: { userId: userId }, - defaults: { +type UserInput = { + anonymousUserId?: string; + userId: string; + wantedUsername: string; + email: string; + emailVerified: boolean; + subscribedToNewsletter: boolean; +}; + +export async function getSettingsOrInit({ anonymousUserId, userId, wantedUsername, email, emailVerified, subscribedToNewsletter }: UserInput) { + let userSettings: UserSetting; + let isAnonymous = false; + + if (anonymousUserId) { + try { + const updateResult = await UserSetting.update( + { + userId, + username: await generateUsername(wantedUsername), + email: email, + emailVerified: emailVerified, + stripeCustomerId: null, + subscribedToNewsletter: subscribedToNewsletter + }, + { where: { id: anonymousUserId, userId: null }, returning: ["*"] } + ); + + userSettings = updateResult[1][0]; + isAnonymous = !!userSettings; + + if (isAnonymous) { + logger.info({ event: "ANONYMOUS_USER_REGISTERED", id: anonymousUserId, userId }); + } + } catch (error) { + if (error.name !== "SequelizeUniqueConstraintError") { + throw error; + } + + logger.info({ event: "ANONYMOUS_USER_ALREADY_REGISTERED", id: anonymousUserId, userId }); + } + } + + if (!isAnonymous) { + userSettings = await UserSetting.findOne({ where: { userId: userId } }); + logger.debug({ event: "USER_RETRIEVED", id: anonymousUserId, userId }); + } + + if (!userSettings) { + userSettings = await UserSetting.create({ userId: userId, username: await generateUsername(wantedUsername), email: email, emailVerified: emailVerified, stripeCustomerId: null, subscribedToNewsletter: subscribedToNewsletter - } - }); + }); + logger.info({ event: "USER_REGISTERED", userId }); + } - if (created) { - console.log(`Created settings for user ${userId}`); - } else if (userSettings.email !== email || userSettings.emailVerified !== emailVerified) { + if (userSettings.email !== email || userSettings.emailVerified !== emailVerified) { userSettings.email = email; userSettings.emailVerified = emailVerified; await userSettings.save(); } - const planCode = await getUserPlan(userSettings.stripeCustomerId); - - return { - username: userSettings.username, - email: userSettings.email, - emailVerified: userSettings.emailVerified, - stripeCustomerId: userSettings.stripeCustomerId, - bio: userSettings.bio, - subscribedToNewsletter: userSettings.subscribedToNewsletter, - youtubeUsername: userSettings.youtubeUsername, - twitterUsername: userSettings.twitterUsername, - githubUsername: userSettings.githubUsername, - planCode: planCode - }; + return pick(userSettings, [ + "username", + "email", + "emailVerified", + "stripeCustomerId", + "bio", + "subscribedToNewsletter", + "youtubeUsername", + "twitterUsername", + "githubUsername" + ]); } export async function getAddressNames(userId: string) { diff --git a/apps/api/src/user/providers/current-user.provider.ts b/apps/api/src/user/providers/current-user.provider.ts new file mode 100644 index 000000000..c593cdd31 --- /dev/null +++ b/apps/api/src/user/providers/current-user.provider.ts @@ -0,0 +1,16 @@ +import { container, inject } from "tsyringe"; + +import { RequestStorageInterceptor } from "@src/core/services/request-storage/request-storage.interceptor"; +import { CURRENT_USER } from "@src/user/services/current-user/current-user.interceptor"; + +container.register(CURRENT_USER, { + useFactory: c => { + return c.resolve(RequestStorageInterceptor).context.get(CURRENT_USER); + } +}); + +export const CurrentUser = () => inject(CURRENT_USER); +export type CurrentUser = { + userId: string; + isAnonymous: boolean; +}; diff --git a/apps/api/src/user/services/current-user/current-user.interceptor.ts b/apps/api/src/user/services/current-user/current-user.interceptor.ts new file mode 100644 index 000000000..f2eed5515 --- /dev/null +++ b/apps/api/src/user/services/current-user/current-user.interceptor.ts @@ -0,0 +1,19 @@ +import { Context, Next } from "hono"; +import { singleton } from "tsyringe"; + +import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; +import { getCurrentUserId } from "@src/middlewares/userMiddleware"; + +export const CURRENT_USER = "CURRENT_USER"; + +@singleton() +export class CurrentUserInterceptor implements HonoInterceptor { + intercept() { + return async (c: Context, next: Next) => { + const userId = getCurrentUserId(c); + c.set(CURRENT_USER, { userId, isAnonymous: !userId }); + + return await next(); + }; + } +} diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 47c67fe50..1247358f5 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -17,6 +17,7 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.10.0", + "@akashnetwork/http-sdk": "*", "@akashnetwork/ui": "*", "@auth0/nextjs-auth0": "^3.5.0", "@chain-registry/types": "^0.41.3", @@ -51,7 +52,7 @@ "@tanstack/react-table": "^8.13.2", "@textea/json-viewer": "^3.0.0", "auth0": "^4.3.1", - "axios": "^0.27.2", + "axios": "^1.7.2", "chain-registry": "^1.20.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", diff --git a/apps/deploy-web/src/components/user/UserProviders.tsx b/apps/deploy-web/src/components/user/UserProviders.tsx index 7698c8a1e..45edf4a37 100644 --- a/apps/deploy-web/src/components/user/UserProviders.tsx +++ b/apps/deploy-web/src/components/user/UserProviders.tsx @@ -3,11 +3,12 @@ import { UserProvider } from "@auth0/nextjs-auth0/client"; import { UserInitLoader } from "@src/components/user/UserInitLoader"; import { envConfig } from "@src/config/env.config"; import { AnonymousUserProvider } from "@src/context/AnonymousUserProvider/AnonymousUserProvider"; +import { authHttpService } from "@src/services/auth/auth-http.service"; import { FCWithChildren } from "@src/types/component"; export const UserProviders: FCWithChildren = ({ children }) => envConfig.NEXT_PUBLIC_BILLING_ENABLED ? ( - + {children} diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index f5223154f..c10356825 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -1,5 +1,6 @@ "use client"; import React, { useEffect, useMemo, useRef, useState } from "react"; +import type { TxOutput } from "@akashnetwork/http-sdk"; import { Snackbar } from "@akashnetwork/ui/components"; import { EncodeObject } from "@cosmjs/proto-signing"; import { SigningStargateClient } from "@cosmjs/stargate"; @@ -18,7 +19,7 @@ import { useUsdcDenom } from "@src/hooks/useDenom"; import { useManagedWallet } from "@src/hooks/useManagedWallet"; import { getSelectedNetwork, useSelectedNetwork } from "@src/hooks/useSelectedNetwork"; import { useWhen } from "@src/hooks/useWhen"; -import { txHttpService, TxOutput } from "@src/services/tx-http/tx-http.service"; +import { txHttpService } from "@src/services/http/http.service"; import { AnalyticsEvents } from "@src/utils/analytics"; import { STATS_APP_URL, uAktDenom } from "@src/utils/constants"; import { customRegistry } from "@src/utils/customRegistry"; diff --git a/apps/deploy-web/src/hooks/useCustomUser.ts b/apps/deploy-web/src/hooks/useCustomUser.ts index d7653b8c0..302fdea97 100644 --- a/apps/deploy-web/src/hooks/useCustomUser.ts +++ b/apps/deploy-web/src/hooks/useCustomUser.ts @@ -12,7 +12,6 @@ type UseCustomUser = { export const useCustomUser = (): UseCustomUser => { const { user, isLoading, error, checkSession } = useUser(); - const completeUser = user ? { ...user, plan: plans.find(x => x.code === user.planCode) } : user; return { diff --git a/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts b/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts index 6fd55c965..9179ffb73 100644 --- a/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts +++ b/apps/deploy-web/src/hooks/useStoredAnonymousUser.ts @@ -1,24 +1,27 @@ import { useMemo } from "react"; -import { useLocalStorage } from "usehooks-ts"; import { envConfig } from "@src/config/env.config"; import { useCustomUser } from "@src/hooks/useCustomUser"; import { useWhen } from "@src/hooks/useWhen"; import { useAnonymousUserQuery, UserOutput } from "@src/queries/useAnonymousUserQuery"; +import { ANONYMOUS_USER_KEY } from "@src/utils/constants"; type UseApiUserResult = { user?: UserOutput; isLoading: boolean; }; +const storedAnonymousUserStr = typeof window !== "undefined" && localStorage.getItem(ANONYMOUS_USER_KEY); +const storedAnonymousUser: UserOutput | undefined = storedAnonymousUserStr ? JSON.parse(storedAnonymousUserStr) : undefined; + export const useStoredAnonymousUser = (): UseApiUserResult => { const { user: registeredUser, isLoading: isLoadingRegisteredUser } = useCustomUser(); - const [storedAnonymousUser, storeAnonymousUser] = useLocalStorage("user", undefined); const { user, isLoading } = useAnonymousUserQuery(storedAnonymousUser?.id, { enabled: envConfig.NEXT_PUBLIC_BILLING_ENABLED && !registeredUser && !isLoadingRegisteredUser }); - useWhen(user, () => storeAnonymousUser(user)); + useWhen(user, () => localStorage.setItem("anonymous-user", JSON.stringify(user))); + useWhen(storedAnonymousUser && registeredUser, () => localStorage.removeItem(ANONYMOUS_USER_KEY)); return useMemo( () => ({ diff --git a/apps/deploy-web/src/pages/api/auth/[...auth0].ts b/apps/deploy-web/src/pages/api/auth/[...auth0].ts index d553b9c18..cde995f80 100644 --- a/apps/deploy-web/src/pages/api/auth/[...auth0].ts +++ b/apps/deploy-web/src/pages/api/auth/[...auth0].ts @@ -1,18 +1,19 @@ // pages/api/auth/[...auth0].js import { handleAuth, handleLogin, handleProfile } from "@auth0/nextjs-auth0"; -import axios from "axios"; +import axios, { AxiosHeaders } from "axios"; +import type { NextApiRequest, NextApiResponse } from "next"; import { BASE_API_MAINNET_URL } from "@src/utils/constants"; export default handleAuth({ - async login(req, res) { + async login(req: NextApiRequest, res: NextApiResponse) { const returnUrl = decodeURIComponent((req.query.from as string) ?? "/"); await handleLogin(req, res, { returnTo: returnUrl }); }, - async profile(req, res) { + async profile(req: NextApiRequest, res: NextApiResponse) { console.log("server /profile", req.url); try { await handleProfile(req, res, { @@ -21,6 +22,15 @@ export default handleAuth({ try { // TODO: Fix for console const user_metadata = session.user["https://console.akash.network/user_metadata"]; + const headers = new AxiosHeaders({ + Authorization: `Bearer ${session.accessToken}` + }); + + const anonymousId = req.headers["x-anonymous-user-id"]; + + if (anonymousId) { + headers.set("x-anonymous-user-id", anonymousId); + } const userSettings = await axios.post( `${BASE_API_MAINNET_URL}/user/tokenInfo`, @@ -31,9 +41,7 @@ export default handleAuth({ subscribedToNewsletter: user_metadata?.subscribedToNewsletter === "true" }, { - headers: { - Authorization: `Bearer ${session.accessToken}` - } + headers: headers.toJSON() } ); diff --git a/apps/deploy-web/src/pages/api/auth/signup.ts b/apps/deploy-web/src/pages/api/auth/signup.ts index c49f455cc..846fec196 100644 --- a/apps/deploy-web/src/pages/api/auth/signup.ts +++ b/apps/deploy-web/src/pages/api/auth/signup.ts @@ -1,6 +1,7 @@ import { handleLogin } from "@auth0/nextjs-auth0"; +import type { NextApiRequest, NextApiResponse } from "next"; -export default async function signup(req, res) { +export default async function signup(req: NextApiRequest, res: NextApiResponse) { try { await handleLogin(req, res, { authorizationParams: { diff --git a/apps/deploy-web/src/queries/useAnonymousUserQuery.ts b/apps/deploy-web/src/queries/useAnonymousUserQuery.ts index d5c3620ec..3e7145cdc 100644 --- a/apps/deploy-web/src/queries/useAnonymousUserQuery.ts +++ b/apps/deploy-web/src/queries/useAnonymousUserQuery.ts @@ -1,7 +1,7 @@ import { useState } from "react"; import { useWhen } from "@src/hooks/useWhen"; -import { userHttpService } from "@src/services/user-http/user-http.service"; +import { userHttpService } from "@src/services/http/http.service"; export interface UserOutput { id: string; diff --git a/apps/deploy-web/src/queries/useManagedWalletQuery.ts b/apps/deploy-web/src/queries/useManagedWalletQuery.ts index 8425be579..98ddb56c3 100644 --- a/apps/deploy-web/src/queries/useManagedWalletQuery.ts +++ b/apps/deploy-web/src/queries/useManagedWalletQuery.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; -import { managedWalletHttpService } from "@src/services/managed-wallet-http/managed-wallet-http.service"; +import { managedWalletHttpService } from "@src/services/http/http.service"; const MANAGED_WALLET = "MANAGED_WALLET"; diff --git a/apps/deploy-web/src/services/api-http/api-http.service.ts b/apps/deploy-web/src/services/api-http/api-http.service.ts deleted file mode 100644 index d3bc95bfe..000000000 --- a/apps/deploy-web/src/services/api-http/api-http.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import axios, { Axios, AxiosRequestConfig, AxiosResponse } from "axios"; - -import { BASE_API_URL } from "@src/utils/constants"; - -export interface ApiOutput { - data: T; -} - -export class ApiHttpService extends Axios { - constructor() { - const { headers, ...defaults } = axios.defaults; - super({ - ...defaults, - baseURL: BASE_API_URL - }); - } - - post>, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise { - return super.post(url, data, config); - } - - get>, D = any>(url: string, config?: AxiosRequestConfig): Promise { - return super.get(url, config); - } - - protected extractData(response: ApiOutput>): AxiosResponse["data"] { - return response.data.data; - } -} diff --git a/apps/deploy-web/src/services/auth/auth-http.service.ts b/apps/deploy-web/src/services/auth/auth-http.service.ts new file mode 100644 index 000000000..394b1b070 --- /dev/null +++ b/apps/deploy-web/src/services/auth/auth-http.service.ts @@ -0,0 +1,30 @@ +import { HttpService } from "@akashnetwork/http-sdk"; +import type { UserProfile } from "@auth0/nextjs-auth0/client"; +import { AxiosHeaders } from "axios"; + +import { ANONYMOUS_USER_KEY } from "@src/utils/constants"; + +export class AuthHttpService extends HttpService { + constructor() { + super(); + this.getProfile = this.getProfile.bind(this); + } + + async getProfile(url: string) { + try { + const user = localStorage.getItem(ANONYMOUS_USER_KEY); + const anonymousUserId = user ? JSON.parse(user).id : undefined; + const headers = new AxiosHeaders(); + if (anonymousUserId) { + headers.set("X-User-Id", anonymousUserId); + } + + return this.extractData(await this.get(url, { headers: headers.toJSON() })); + } catch (error) { + console.warn("DEBUG error", error); + throw error; + } + } +} + +export const authHttpService = new AuthHttpService(); diff --git a/apps/deploy-web/src/services/http/http.service.ts b/apps/deploy-web/src/services/http/http.service.ts new file mode 100644 index 000000000..71ff102fa --- /dev/null +++ b/apps/deploy-web/src/services/http/http.service.ts @@ -0,0 +1,10 @@ +import { ManagedWalletHttpService, TxHttpService, UserHttpService } from "@akashnetwork/http-sdk"; + +import { BASE_API_URL } from "@src/utils/constants"; +import { customRegistry } from "@src/utils/customRegistry"; + +const apiConfig = { baseURL: BASE_API_URL }; + +export const userHttpService = new UserHttpService(apiConfig); +export const txHttpService = new TxHttpService(customRegistry, apiConfig); +export const managedWalletHttpService = new ManagedWalletHttpService(apiConfig); diff --git a/apps/deploy-web/src/services/tx-http/tx-http.service.ts b/apps/deploy-web/src/services/tx-http/tx-http.service.ts deleted file mode 100644 index 2816197b8..000000000 --- a/apps/deploy-web/src/services/tx-http/tx-http.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { EncodeObject } from "@cosmjs/proto-signing/build/registry"; -import { DeliverTxResponse } from "@cosmjs/stargate"; - -import { ApiHttpService } from "@src/services/api-http/api-http.service"; -import { customRegistry } from "@src/utils/customRegistry"; - -export interface TxInput { - userId: string; - messages: EncodeObject[]; -} - -export type TxOutput = Pick; - -export class TxHttpService extends ApiHttpService { - async signAndBroadcastTx(input: TxInput) { - const messages = input.messages.map(m => ({ ...m, value: Buffer.from(customRegistry.encode(m)).toString("base64") })); - - return this.extractData( - await this.post("v1/tx", { - data: { - userId: input.userId, - messages: messages - } - }) - ); - } -} - -export const txHttpService = new TxHttpService(); diff --git a/apps/deploy-web/src/utils/constants.ts b/apps/deploy-web/src/utils/constants.ts index 2b1953c6b..31da68b3f 100644 --- a/apps/deploy-web/src/utils/constants.ts +++ b/apps/deploy-web/src/utils/constants.ts @@ -179,3 +179,5 @@ export const monacoOptions = { }; export const txFeeBuffer = 10000; // 10000 uAKT + +export const ANONYMOUS_USER_KEY = "anonymous-user"; diff --git a/apps/indexer/package.json b/apps/indexer/package.json index c416df26a..1aa3b8f05 100644 --- a/apps/indexer/package.json +++ b/apps/indexer/package.json @@ -31,7 +31,7 @@ "@cosmjs/stargate": "^0.32.4", "@sentry/node": "^7.52.0", "async": "^3.2.4", - "axios": "^0.27.2", + "axios": "^1.7.2", "cosmjs-types": "^0.9.0", "date-fns": "^2.29.2", "date-fns-tz": "^1.3.6", diff --git a/apps/provider-proxy/package.json b/apps/provider-proxy/package.json index 925c8cab7..726bc3386 100644 --- a/apps/provider-proxy/package.json +++ b/apps/provider-proxy/package.json @@ -13,7 +13,7 @@ "dev": "npm run start" }, "dependencies": { - "axios": "^1.3.0", + "axios": "^1.7.2", "cors": "^2.8.5", "express": "^4.18.2", "node-fetch": "^2.6.9", diff --git a/apps/stats-web/package.json b/apps/stats-web/package.json index 284ce0529..9d1bf009a 100644 --- a/apps/stats-web/package.json +++ b/apps/stats-web/package.json @@ -18,7 +18,7 @@ "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-table": "^8.11.2", "@textea/json-viewer": "^3.2.3", - "axios": "^1.6.1", + "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "geist": "^1.3.0", diff --git a/package-lock.json b/package-lock.json index 028a76655..1cebb8585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "@opentelemetry/sdk-node": "^0.52.1", "@sentry/node": "^7.55.2", "@supercharge/promise-pool": "^3.2.0", - "axios": "^0.27.2", + "axios": "^1.7.2", "commander": "^12.1.0", "cosmjs-types": "^0.9.0", "date-fns": "^2.29.2", @@ -159,6 +159,16 @@ "xstream": "^11.14.0" } }, + "apps/api/node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "apps/api/node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -261,6 +271,7 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.10.0", + "@akashnetwork/http-sdk": "*", "@akashnetwork/ui": "*", "@auth0/nextjs-auth0": "^3.5.0", "@chain-registry/types": "^0.41.3", @@ -295,7 +306,7 @@ "@tanstack/react-table": "^8.13.2", "@textea/json-viewer": "^3.0.0", "auth0": "^4.3.1", - "axios": "^0.27.2", + "axios": "^1.7.2", "chain-registry": "^1.20.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -429,6 +440,16 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "dev": true }, + "apps/deploy-web/node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "apps/indexer": { "name": "console-indexer", "version": "1.9.1", @@ -443,7 +464,7 @@ "@cosmjs/stargate": "^0.32.4", "@sentry/node": "^7.52.0", "async": "^3.2.4", - "axios": "^0.27.2", + "axios": "^1.7.2", "cosmjs-types": "^0.9.0", "date-fns": "^2.29.2", "date-fns-tz": "^1.3.6", @@ -543,6 +564,16 @@ "xstream": "^11.14.0" } }, + "apps/indexer/node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "apps/indexer/node_modules/reflect-metadata": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", @@ -664,7 +695,7 @@ "version": "1.0.7", "license": "Apache-2.0", "dependencies": { - "axios": "^1.3.0", + "axios": "^1.7.2", "cors": "^2.8.5", "express": "^4.18.2", "node-fetch": "^2.6.9", @@ -714,7 +745,7 @@ "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-table": "^8.11.2", "@textea/json-viewer": "^3.2.3", - "axios": "^1.6.1", + "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "geist": "^1.3.0", @@ -4021,7 +4052,6 @@ "version": "0.32.4", "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.32.4.tgz", "integrity": "sha512-QdyQDbezvdRI4xxSlyM1rSVBO2st5sqtbEIl3IX03uJ7YiZIQHyv6vaHVf1V4mapusCqguiHJzm4N4gsFdLBbQ==", - "license": "Apache-2.0", "dependencies": { "@cosmjs/amino": "^0.32.4", "@cosmjs/crypto": "^0.32.4", @@ -19409,9 +19439,9 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "node_modules/@types/lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/long": { @@ -45642,7 +45672,51 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stargate": "^0.32.4", + "@types/lodash": "^4.17.7" + } + }, + "packages/http-sdk/node_modules/@cosmjs/encoding": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.32.4.tgz", + "integrity": "sha512-tjvaEy6ZGxJchiizzTn7HVRiyTg1i4CObRRaTRPknm5EalE13SV+TCHq38gIDfyUeden4fCuaBVEdBR5+ti7Hw==", + "dev": true, + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "packages/http-sdk/node_modules/@cosmjs/math": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.32.4.tgz", + "integrity": "sha512-++dqq2TJkoB8zsPVYCvrt88oJWsy1vMOuSOKcdlnXuOA/ASheTJuYy4+oZlTQ3Fr8eALDLGGPhJI02W2HyAQaw==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "packages/http-sdk/node_modules/@cosmjs/stargate": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.32.4.tgz", + "integrity": "sha512-usj08LxBSsPRq9sbpCeVdyLx2guEcOHfJS9mHGCLCXpdAPEIEQEtWLDpEUc0LEhWOx6+k/ChXTc5NpFkdrtGUQ==", + "dev": true, + "dependencies": { + "@confio/ics23": "^0.6.8", + "@cosmjs/amino": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stream": "^0.32.4", + "@cosmjs/tendermint-rpc": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "cosmjs-types": "^0.9.0", + "xstream": "^11.14.0" } }, "packages/http-sdk/node_modules/axios": { diff --git a/packages/http-sdk/package.json b/packages/http-sdk/package.json index 0200ad12f..d619256e5 100644 --- a/packages/http-sdk/package.json +++ b/packages/http-sdk/package.json @@ -11,6 +11,12 @@ "lint": "eslint ." }, "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stargate": "^0.32.4", + "@types/lodash": "^4.17.7" } } diff --git a/packages/http-sdk/src/api-http/api-http.service.ts b/packages/http-sdk/src/api-http/api-http.service.ts new file mode 100644 index 000000000..5aca315af --- /dev/null +++ b/packages/http-sdk/src/api-http/api-http.service.ts @@ -0,0 +1,24 @@ +import { HttpService } from "@akashnetwork/http-sdk"; +import { AxiosRequestConfig, AxiosResponse } from "axios"; + +export interface ApiOutput { + data: T; +} + +export class ApiHttpService extends HttpService { + constructor(config?: AxiosRequestConfig) { + super(config); + } + + post>, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise { + return super.post(url, data, config); + } + + get>, D = any>(url: string, config?: AxiosRequestConfig): Promise { + return super.get(url, config); + } + + protected extractApiData(response: ApiOutput>): AxiosResponse["data"] { + return this.extractData(response.data); + } +} diff --git a/packages/http-sdk/src/index.ts b/packages/http-sdk/src/index.ts index da83a678e..7b2d7afa6 100644 --- a/packages/http-sdk/src/index.ts +++ b/packages/http-sdk/src/index.ts @@ -1 +1,6 @@ +export * from "./http/http.service"; export * from "./allowance/allowance-http.service"; +export * from "./api-http/api-http.service"; +export * from "./tx-http/tx-http.service"; +export * from "./managed-wallet-http/managed-wallet-http.service"; +export * from "./user-http/user-http.service"; diff --git a/apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts b/packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts similarity index 59% rename from apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts rename to packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts index 813d0beba..16d45ba3f 100644 --- a/apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts +++ b/packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts @@ -1,4 +1,4 @@ -import { ApiHttpService } from "@src/services/api-http/api-http.service"; +import { ApiHttpService } from "../api-http/api-http.service"; export interface ApiWalletOutput { id: string; @@ -9,11 +9,11 @@ export interface ApiWalletOutput { export class ManagedWalletHttpService extends ApiHttpService { async createWallet(userId: string) { - return this.addWalletEssentials(this.extractData(await this.post("v1/wallets", { data: { userId } }))); + return this.addWalletEssentials(this.extractApiData(await this.post("v1/wallets", { data: { userId } }))); } async getWallet(userId: string) { - const [wallet] = this.extractData(await this.get("v1/wallets", { params: { userId } })); + const [wallet] = this.extractApiData(await this.get("v1/wallets", { params: { userId } })); return wallet && this.addWalletEssentials(wallet); } @@ -26,5 +26,3 @@ export class ManagedWalletHttpService extends ApiHttpService { }; } } - -export const managedWalletHttpService = new ManagedWalletHttpService(); diff --git a/packages/http-sdk/src/tx-http/tx-http.service.ts b/packages/http-sdk/src/tx-http/tx-http.service.ts new file mode 100644 index 000000000..4b54ecfad --- /dev/null +++ b/packages/http-sdk/src/tx-http/tx-http.service.ts @@ -0,0 +1,33 @@ +import { ApiHttpService } from "@akashnetwork/http-sdk"; +import type { Registry } from "@cosmjs/proto-signing"; +import type { EncodeObject } from "@cosmjs/proto-signing/build/registry"; +import type { DeliverTxResponse } from "@cosmjs/stargate"; +import { AxiosRequestConfig } from "axios"; + +export interface TxInput { + userId: string; + messages: EncodeObject[]; +} + +export type TxOutput = Pick; + +export class TxHttpService extends ApiHttpService { + constructor( + private readonly registry: Registry, + config?: AxiosRequestConfig + ) { + super(config); + } + async signAndBroadcastTx(input: TxInput) { + const messages = input.messages.map(m => ({ ...m, value: Buffer.from(this.registry.encode(m)).toString("base64") })); + + return this.extractApiData( + await this.post("v1/tx", { + data: { + userId: input.userId, + messages: messages + } + }) + ); + } +} diff --git a/apps/deploy-web/src/services/user-http/user-http.service.ts b/packages/http-sdk/src/user-http/user-http.service.ts similarity index 72% rename from apps/deploy-web/src/services/user-http/user-http.service.ts rename to packages/http-sdk/src/user-http/user-http.service.ts index ec48cc948..ccd6c7ab3 100644 --- a/apps/deploy-web/src/services/user-http/user-http.service.ts +++ b/packages/http-sdk/src/user-http/user-http.service.ts @@ -1,7 +1,7 @@ +import { ApiHttpService } from "@akashnetwork/http-sdk"; +import { AxiosRequestConfig } from "axios"; import memoize from "lodash/memoize"; -import { ApiHttpService } from "@src/services/api-http/api-http.service"; - export interface UserOutput { id: string; userId?: string; @@ -17,8 +17,8 @@ export interface UserOutput { } export class UserHttpService extends ApiHttpService { - constructor() { - super(); + constructor(config?: AxiosRequestConfig) { + super(config); this.getOrCreateAnonymousUser = memoize(this.getOrCreateAnonymousUser.bind(this)); } @@ -27,12 +27,12 @@ export class UserHttpService extends ApiHttpService { } private async createAnonymousUser() { - return this.extractData(await this.post("/v1/anonymous-users")); + return this.extractApiData(await this.post("/v1/anonymous-users")); } private async getAnonymousUser(id: string) { try { - return this.extractData(await this.get(`/v1/anonymous-users/${id}`)); + return this.extractApiData(await this.get(`/v1/anonymous-users/${id}`)); } catch (error) { if (error.response?.status === 404) { return this.createAnonymousUser(); @@ -42,5 +42,3 @@ export class UserHttpService extends ApiHttpService { } } } - -export const userHttpService = new UserHttpService(); diff --git a/packages/http-sdk/tsconfig.build.json b/packages/http-sdk/tsconfig.build.json new file mode 100644 index 000000000..306ea8d76 --- /dev/null +++ b/packages/http-sdk/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "noImplicitAny": true, + "paths": { + "@src/*": ["./src/*"], + "@test/*": ["./test/*"] + } + }, + "extends": "@akashnetwork/dev-config/tsconfig.base-node.json" +} diff --git a/packages/http-sdk/tsconfig.json b/packages/http-sdk/tsconfig.json new file mode 100644 index 000000000..579446f28 --- /dev/null +++ b/packages/http-sdk/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["node_modules", "dist", "**/*spec.ts"], + "extends": "./tsconfig.build.json", + "include": ["src/**/*"] +}