diff --git a/.commitlintrc.json b/.commitlintrc.json index dd240a528..239d2f78f 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -15,6 +15,8 @@ "deployment", "dx", "stats" + "certificate", + "dx" ] ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd0948632..2c8d1a99d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,17 @@ feat(wallet): add a new function to compute wallet balance - **Include Unit Tests (When Applicable)**: Verifiable unit tests aid in maintaining code quality and prevent additional bugs from being introduced. - **Linting**: Run `npm run lint:fix` to make sure your code is properly formatted. +### Big Features + +For large features or significant changes: + +1. Create a fork of the main repository. +2. Implement your feature in small, incremental pull requests to your fork. +3. This allows us to gradually review the changes and provide guidance throughout the development process. +4. Once the feature is complete and has gone through the review process on the fork, we can then merge it into the main repository. + +This approach helps manage complex features more effectively and ensures that large changes are thoroughly reviewed before being integrated into the main codebase. + ### Contribution Process Overview If you're ready to contribute, follow our guidelines: diff --git a/README.md b/README.md index 62ffa3639..5759ac5dc 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Quick Start](#quick-start) +- [Apps Configuration](./doc/apps-configuration.md) - [Services](#services) - [Monitoring](#monitoring) - [Example SQL Queries](#example-sql-queries) diff --git a/apps/api/README.md b/apps/api/README.md index 29b490fce..849ea525a 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -13,23 +13,6 @@ You can make sure the api is working by accessing the status endpoint: `http://localhost:3080/status` -## Environment Variables - -This app utilizes `.env*` files to manage environment variables. The list of environment variables can be found in the `env/.env.sample` file. These files are included in version control and should only contain non-sensitive values. Sensitive values are provided by the deployment system. - -### Important Notes: -- **Sensitive Values**: The only env file that's ignored by Git is `env/.env.local`, which is intended for sensitive values used in development. -- **Loading Order**: Environment files are loaded in a specific order, depending on two environment variables: `DEPLOYMENT_ENV` and `NETWORK`. - -### Loading Order: -1. `env/.env.local` - Contains sensitive values for development. -2. `env/.env` - Default values applicable to all environments. -3. `env/.env.${DEPLOYMENT_ENV}` - Values specific to the deployment environment. -4. `env/.env.${NETWORK}` - Values specific to the network. - -### Additional Details: -- **Variable Precedence**: If a variable is already set in the environment, it will not be overridden by values in the `.env*` files. This behavior is critical when adjusting the loading order of these files. - ## Testing Project is configured to use [Jest](https://jestjs.io/) for testing. It is intended to be covered with unit and functional tests where applicable. diff --git a/apps/api/package.json b/apps/api/package.json index 0da305a07..f6e2f54bf 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "console-api", - "version": "2.23.3", + "version": "2.23.4", "description": "Api providing data to the deploy tool", "repository": { "type": "git", @@ -34,6 +34,7 @@ "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.10.0", "@akashnetwork/database": "*", + "@akashnetwork/env-loader": "*", "@akashnetwork/http-sdk": "*", "@casl/ability": "^6.7.1", "@chain-registry/assets": "^1.64.79", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index b1eab8539..bc18e1afb 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -73,8 +73,8 @@ if (BILLING_ENABLED === "true") { const { AuthInterceptor } = require("./auth/services/auth.interceptor"); appHono.use(container.resolve(AuthInterceptor).intercept()); // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createWalletRouter, getWalletListRouter, signAndBroadcastTxRouter, checkoutRouter, stripeWebhook } = require("./billing"); - appHono.route("/", createWalletRouter); + const { startTrialRouter, getWalletListRouter, signAndBroadcastTxRouter, checkoutRouter, stripeWebhook } = require("./billing"); + appHono.route("/", startTrialRouter); appHono.route("/", getWalletListRouter); appHono.route("/", signAndBroadcastTxRouter); appHono.route("/", checkoutRouter); diff --git a/apps/api/src/billing/controllers/wallet/wallet.controller.ts b/apps/api/src/billing/controllers/wallet/wallet.controller.ts index bda758a39..d4b1864b2 100644 --- a/apps/api/src/billing/controllers/wallet/wallet.controller.ts +++ b/apps/api/src/billing/controllers/wallet/wallet.controller.ts @@ -3,7 +3,7 @@ import { Lifecycle, scoped } from "tsyringe"; import { Protected } from "@src/auth/services/auth.service"; import type { WalletListOutputResponse, WalletOutputResponse } from "@src/billing/http-schemas/wallet.schema"; -import type { CreateWalletRequestInput, SignTxRequestInput, SignTxResponseOutput } from "@src/billing/routes"; +import type { SignTxRequestInput, SignTxResponseOutput, StartTrialRequestInput } from "@src/billing/routes"; import { GetWalletQuery } from "@src/billing/routes/get-wallet-list/get-wallet-list.router"; import { WalletInitializerService } from "@src/billing/services"; import { RefillService } from "@src/billing/services/refill/refill.service"; @@ -22,7 +22,7 @@ export class WalletController { @WithTransaction() @Protected([{ action: "create", subject: "UserWallet" }]) - async create({ data: { userId } }: CreateWalletRequestInput): Promise { + async create({ data: { userId } }: StartTrialRequestInput): Promise { return { data: await this.walletInitializer.initializeAndGrantTrialLimits(userId) }; diff --git a/apps/api/src/billing/routes/checkout/checkout.router.ts b/apps/api/src/billing/routes/checkout/checkout.router.ts index f4e15d229..ad5a1d773 100644 --- a/apps/api/src/billing/routes/checkout/checkout.router.ts +++ b/apps/api/src/billing/routes/checkout/checkout.router.ts @@ -8,7 +8,7 @@ const route = createRoute({ method: "get", path: "/v1/checkout", summary: "Creates a stripe checkout session and redirects to checkout", - tags: ["Wallets"], + tags: ["Wallet"], request: {}, responses: { 301: { diff --git a/apps/api/src/billing/routes/get-wallet-list/get-wallet-list.router.ts b/apps/api/src/billing/routes/get-wallet-list/get-wallet-list.router.ts index 6276460b3..bf04c4333 100644 --- a/apps/api/src/billing/routes/get-wallet-list/get-wallet-list.router.ts +++ b/apps/api/src/billing/routes/get-wallet-list/get-wallet-list.router.ts @@ -16,7 +16,7 @@ const route = createRoute({ method: "get", path: "/v1/wallets", summary: "Get a list of wallets", - tags: ["Wallets"], + tags: ["Wallet"], request: { query: GetWalletRequestQuerySchema }, diff --git a/apps/api/src/billing/routes/index.ts b/apps/api/src/billing/routes/index.ts index 79da4eed4..428aa73e4 100644 --- a/apps/api/src/billing/routes/index.ts +++ b/apps/api/src/billing/routes/index.ts @@ -1,4 +1,4 @@ -export * from "@src/billing/routes/create-wallet/create-wallet.router"; +export * from "@src/billing/routes/start-trial/start-trial.router"; export * from "@src/billing/routes/get-wallet-list/get-wallet-list.router"; export * from "@src/billing/routes/checkout/checkout.router"; export * from "@src/billing/routes/sign-and-broadcast-tx/sign-and-broadcast-tx.router"; diff --git a/apps/api/src/billing/routes/sign-and-broadcast-tx/sign-and-broadcast-tx.router.ts b/apps/api/src/billing/routes/sign-and-broadcast-tx/sign-and-broadcast-tx.router.ts index 1a0d20d17..bdd814a1b 100644 --- a/apps/api/src/billing/routes/sign-and-broadcast-tx/sign-and-broadcast-tx.router.ts +++ b/apps/api/src/billing/routes/sign-and-broadcast-tx/sign-and-broadcast-tx.router.ts @@ -41,7 +41,7 @@ const route = createRoute({ method: "post", path: "/v1/tx", summary: "Signs a transaction via a user managed wallet", - tags: ["Wallets"], + tags: ["Wallet"], request: { body: { content: { diff --git a/apps/api/src/billing/routes/create-wallet/create-wallet.router.ts b/apps/api/src/billing/routes/start-trial/start-trial.router.ts similarity index 71% rename from apps/api/src/billing/routes/create-wallet/create-wallet.router.ts rename to apps/api/src/billing/routes/start-trial/start-trial.router.ts index 050f628c3..65d656cfa 100644 --- a/apps/api/src/billing/routes/create-wallet/create-wallet.router.ts +++ b/apps/api/src/billing/routes/start-trial/start-trial.router.ts @@ -6,24 +6,24 @@ import { WalletController } from "@src/billing/controllers/wallet/wallet.control import { WalletResponseOutputSchema } from "@src/billing/http-schemas/wallet.schema"; import { OpenApiHonoHandled } from "@src/core/services/open-api-hono-handled/open-api-hono-handled"; -export const CreateWalletRequestInputSchema = z.object({ +export const StartTrialRequestInputSchema = z.object({ data: z.object({ userId: z.string().openapi({}) }) }); -export type CreateWalletRequestInput = z.infer; +export type StartTrialRequestInput = z.infer; const route = createRoute({ method: "post", - path: "/v1/wallets", + path: "/v1/start-trial", summary: "Creates a managed wallet for a user", - tags: ["Wallets"], + tags: ["Wallet"], request: { body: { content: { "application/json": { - schema: CreateWalletRequestInputSchema + schema: StartTrialRequestInputSchema } } } @@ -39,8 +39,8 @@ const route = createRoute({ } } }); -export const createWalletRouter = new OpenApiHonoHandled(); +export const startTrialRouter = new OpenApiHonoHandled(); -createWalletRouter.openapi(route, async function routeCreateWallet(c) { +startTrialRouter.openapi(route, async function routeStartTrial(c) { return c.json(await container.resolve(WalletController).create(c.req.valid("json")), 200); }); diff --git a/apps/api/src/caching/helpers.ts b/apps/api/src/caching/helpers.ts index 621f997ae..b6e371982 100644 --- a/apps/api/src/caching/helpers.ts +++ b/apps/api/src/caching/helpers.ts @@ -1,8 +1,11 @@ import * as Sentry from "@sentry/node"; import { differenceInSeconds } from "date-fns"; +import { LoggerService } from "@src/core"; import MemoryCacheEngine from "./memoryCacheEngine"; +const logger = new LoggerService({ context: "Caching" }); + export const cacheEngine = new MemoryCacheEngine(); const pendingRequests: { [key: string]: Promise } = {}; @@ -30,20 +33,24 @@ export const Memoize = (options?: MemoizeOptions) => (target: object, propertyNa export async function cacheResponse(seconds: number, key: string, refreshRequest: () => Promise, keepData?: boolean): Promise { const duration = seconds * 1000; const cachedObject = cacheEngine.getFromCache(key) as CachedObject | undefined; - // console.log(`Cache key: ${key}`); + logger.debug(`Request for key: ${key}`); // If first time or expired, must refresh data if not already refreshing const cacheExpired = Math.abs(differenceInSeconds(cachedObject?.date, new Date())) > seconds; if ((!cachedObject || cacheExpired) && !(key in pendingRequests)) { - // console.log(`Making request: ${key}`); + logger.debug(`Object was not in cache or is expired, making new request for key: ${key}`); pendingRequests[key] = refreshRequest() .then(data => { cacheEngine.storeInCache(key, { date: new Date(), data: data }, keepData ? undefined : duration); return data; }) .catch(err => { - console.error(`Error making cache request ${err}`); - Sentry.captureException(err); + if (cachedObject) { + logger.error(`Error making cache request ${err}`); + Sentry.captureException(err); + } else { + throw err; + } }) .finally(() => { delete pendingRequests[key]; @@ -52,10 +59,10 @@ export async function cacheResponse(seconds: number, key: string, refreshRequ // If there is data in cache, return it even if it is expired. Otherwise, wait for the refresh request to finish if (cachedObject) { - // console.log(`Cache hit: ${key}`); + logger.debug(`Returning cached object for key: ${key}`); return cachedObject.data; } else { - // console.log(`Waiting for pending request: ${key}`); + logger.debug(`Waiting for pending request for key: ${key}`); return (await pendingRequests[key]) as T; } } diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index 979d364db..a4b7e5873 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -1,5 +1,5 @@ import "reflect-metadata"; -import "./dotenv"; +import "@akashnetwork/env-loader"; import "./open-telemetry"; import { context, trace } from "@opentelemetry/api"; diff --git a/apps/api/src/dotenv.ts b/apps/api/src/dotenv.ts deleted file mode 100644 index 0d6a34982..000000000 --- a/apps/api/src/dotenv.ts +++ /dev/null @@ -1,23 +0,0 @@ -import dotenv from "@dotenvx/dotenvx"; -import fs from "fs"; -import pino from "pino"; - -const logger = pino().child({ context: "ENV" }); - -const config = (path: string) => { - if (fs.existsSync(path)) { - dotenv.config({ path }); - logger.info(`Loaded ${path}`); - } -}; -config("env/.env.local"); -config("env/.env"); - -const deploymentEnv = process.env.DEPLOYMENT_ENV; - -if (deploymentEnv && deploymentEnv !== "local") { - config(`env/.env.${deploymentEnv}`); -} - -const network = process.env.NETWORK || "mainnet"; -config(`env/.env.${network}`); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 5885ef7ea..c058d6fbc 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; +import "@akashnetwork/env-loader"; import "./open-telemetry"; -import "./dotenv"; async function bootstrap() { /* eslint-disable @typescript-eslint/no-var-requires */ diff --git a/apps/api/src/routes/v1/addresses/deployments.ts b/apps/api/src/routes/v1/addresses/deployments.ts index 559a53c80..628581af2 100644 --- a/apps/api/src/routes/v1/addresses/deployments.ts +++ b/apps/api/src/routes/v1/addresses/deployments.ts @@ -1,6 +1,7 @@ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; import { getAddressDeployments } from "@src/services/external/apiNodeService"; +import { isValidBech32Address } from "@src/utils/addresses"; import { openApiExampleAddress } from "@src/utils/constants"; const maxLimit = 100; @@ -59,15 +60,28 @@ const route = createRoute({ }) } } + }, + 400: { + description: "Invalid address" } } }); export default new OpenAPIHono().openapi(route, async c => { + if (!isValidBech32Address(c.req.valid("param").address, "akash")) { + return c.text("Invalid address", 400); + } + const skip = parseInt(c.req.valid("param").skip); const limit = Math.min(maxLimit, parseInt(c.req.valid("param").limit)); - // TODO Add param validation + if (isNaN(skip)) { + return c.text("Invalid skip.", 400); + } + + if (isNaN(limit)) { + return c.text("Invalid limit.", 400); + } const deployments = await getAddressDeployments(c.req.valid("param").address, skip, limit, c.req.valid("query").reverseSorting === "true", { status: c.req.valid("query").status diff --git a/apps/api/src/services/external/githubService.ts b/apps/api/src/services/external/githubService.ts index 82f94e557..19728232e 100644 --- a/apps/api/src/services/external/githubService.ts +++ b/apps/api/src/services/external/githubService.ts @@ -1,5 +1,6 @@ import { Octokit } from "@octokit/rest"; import axios from "axios"; +import minutesToSeconds from "date-fns/minutesToSeconds"; import { cacheKeys, cacheResponse } from "@src/caching/helpers"; import { Auditor, ProviderAttributesSchema } from "@src/types/provider"; @@ -22,19 +23,25 @@ export function getOctokit() { export const getProviderAttributesSchema = async (): Promise => { // Fetching provider attributes schema const response = await cacheResponse( - 30, + minutesToSeconds(5), cacheKeys.getProviderAttributesSchema, - async () => await axios.get("https://raw.githubusercontent.com/akash-network/console/main/config/provider-attributes.json") + async () => await axios.get("https://raw.githubusercontent.com/akash-network/console/main/config/provider-attributes.json"), + true ); return response.data; }; export async function getAuditors() { - const response = await cacheResponse(60 * 5, cacheKeys.getAuditors, async () => { - const res = await axios.get("https://raw.githubusercontent.com/akash-network/console/main/config/auditors.json"); - return res.data; - }); + const response = await cacheResponse( + minutesToSeconds(5), + cacheKeys.getAuditors, + async () => { + const res = await axios.get("https://raw.githubusercontent.com/akash-network/console/main/config/auditors.json"); + return res.data; + }, + true + ); return response; } diff --git a/apps/api/test/functional/create-wallet.spec.ts b/apps/api/test/functional/start-trial.spec.ts similarity index 95% rename from apps/api/test/functional/create-wallet.spec.ts rename to apps/api/test/functional/start-trial.spec.ts index 24ae07e42..468eb4c8a 100644 --- a/apps/api/test/functional/create-wallet.spec.ts +++ b/apps/api/test/functional/start-trial.spec.ts @@ -10,7 +10,7 @@ import { ApiPgDatabase, POSTGRES_DB, resolveTable } from "@src/core"; jest.setTimeout(20000); -describe("wallets", () => { +describe("start trial", () => { const userWalletsTable = resolveTable("UserWallets"); const config = container.resolve(BILLING_CONFIG); const db = container.resolve(POSTGRES_DB); @@ -22,7 +22,7 @@ describe("wallets", () => { await dbService.cleanAll(); }); - describe("POST /v1/wallets", () => { + describe("POST /v1/start-trial", () => { it("should create a wallet for a user", async () => { const userResponse = await app.request("/v1/anonymous-users", { method: "POST", @@ -33,7 +33,7 @@ describe("wallets", () => { token } = await userResponse.json(); const headers = new Headers({ "Content-Type": "application/json", authorization: `Bearer ${token}` }); - const createWalletResponse = await app.request("/v1/wallets", { + const createWalletResponse = await app.request("/v1/start-trial", { method: "POST", body: JSON.stringify({ data: { userId } }), headers @@ -102,7 +102,7 @@ describe("wallets", () => { }); it("should throw 401 provided no auth header ", async () => { - const createWalletResponse = await app.request("/v1/wallets", { + const createWalletResponse = await app.request("/v1/start-trial", { method: "POST", body: JSON.stringify({ data: { userId: faker.string.uuid() } }), headers: new Headers({ "Content-Type": "application/json" }) diff --git a/apps/api/test/services/wallet-testing.service.ts b/apps/api/test/services/wallet-testing.service.ts index c738750c3..960d2a2b9 100644 --- a/apps/api/test/services/wallet-testing.service.ts +++ b/apps/api/test/services/wallet-testing.service.ts @@ -5,7 +5,7 @@ export class WalletTestingService { async createUserAndWallet() { const { user, token } = await this.createUser(); - const walletResponse = await this.app.request("/v1/wallets", { + const walletResponse = await this.app.request("/v1/start-trial", { method: "POST", body: JSON.stringify({ data: { userId: user.id } diff --git a/apps/deploy-web/README.md b/apps/deploy-web/README.md index a44b16280..986625000 100644 --- a/apps/deploy-web/README.md +++ b/apps/deploy-web/README.md @@ -11,36 +11,3 @@ 4. Start the app with `npm run dev`. The website should be accessible: [http://localhost:3000/](http://localhost:3000/) - -## Environment Variables - -### Overview -Environment variables in this Next.js app follow the standard Next.js behavior, as documented in the [Next.js environment variables documentation](https://nextjs.org/docs/basic-features/environment-variables). This means that files like `.env.local` or `.env.production` will be automatically loaded based on the environment in which the app is running. - -However, we have extended this functionality to support more granular environment-specific configurations. Environment variables are stored in the `./env` directory, where multiple `.env` files exist for different deployment environments (stages): - -- `.env` - Loaded for any environment -- `.env.production` - Loaded for the production stage -- `.env.staging` - Loaded for the staging stage - -### How Environment Variables Are Loaded -We use **dotenvx** to manage and load environment variables. This allows us to take advantage of its features, such as **variable interpolation** (i.e., using other environment variables within variable values). - -### Validation with Zod -Environment variables are validated using **Zod** schemas, ensuring that all required variables are present and have valid values. The validation logic can be found in the file `src/config/env-config.schema.ts`. - -We use two separate Zod schemas: -- **Static Build-Time Schema**: Validates variables at build time. If any variables are missing or invalid during the build process, the build will fail. -- **Dynamic Server Runtime Schema**: Validates variables at server startup. If any variables are missing or invalid at this stage, the server will fail to start. - -This validation ensures that both build and runtime configurations are secure and complete before the app runs. - -### App Configuration -App configurations, including environment variables, are located in the `src/config` directory. In our setup: -- **Environment configs** are handled separately from **hardcoded configs**. -- Hardcoded configs are organized by domain to maintain a clear structure and separation of concerns. - -### Sample Environment Variables -All environment variables required for the app, along with their expected structure and types, can be found in the `env/.env.sample` file. This sample file serves as a template for setting up your environment variables and ensures that all necessary variables are accounted for in each environment. - -By organizing environment variables and configuration this way, we ensure a consistent, safe, and scalable approach to managing different deployment environments. \ No newline at end of file diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 3a3c4cc07..b48218273 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "akash-console", - "version": "2.16.1", + "version": "2.16.2", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", diff --git a/apps/deploy-web/src/components/settings/CertificateList.tsx b/apps/deploy-web/src/components/settings/CertificateList.tsx index 885ff5593..10cd555b2 100644 --- a/apps/deploy-web/src/components/settings/CertificateList.tsx +++ b/apps/deploy-web/src/components/settings/CertificateList.tsx @@ -1,6 +1,7 @@ "use client"; +import { useState, useEffect } from "react"; import { FormattedDate } from "react-intl"; -import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@akashnetwork/ui/components"; +import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, CustomPagination } from "@akashnetwork/ui/components"; import { Check } from "iconoir-react"; import { ConnectWallet } from "@src/components/shared/ConnectWallet"; @@ -11,6 +12,24 @@ import { CertificateDisplay } from "./CertificateDisplay"; export const CertificateList: React.FunctionComponent = () => { const { validCertificates, localCert, selectedCertificate, revokeCertificate, revokeAllCertificates, isLoadingCertificates } = useCertificate(); const { address } = useWallet(); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + const sortedValidCertificates = [...validCertificates].sort((a, b) => { + return new Date(b.pem.issuedOn).getTime() - new Date(a.pem.issuedOn).getTime(); + }); + const start = pageIndex * pageSize; + const end = start + pageSize; + const currentPageCertificates = sortedValidCertificates.slice(start, end); + const pageCount = Math.ceil(sortedValidCertificates.length / pageSize); + + const handleChangePage = (newPage: number) => { + setPageIndex(newPage); + }; + + const onPageSizeChange = (value: number) => { + setPageSize(value); + setPageIndex(0); + }; return (
@@ -37,7 +56,7 @@ export const CertificateList: React.FunctionComponent = () => { - {validCertificates.map(cert => { + {currentPageCertificates.map(cert => { const isCurrentCert = cert.serial === selectedCertificate?.serial; return ( @@ -74,6 +93,18 @@ export const CertificateList: React.FunctionComponent = () => {

No certificates.

)} + + {validCertificates.length > 0 && ( +
+ +
+ )} ) : ( diff --git a/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx b/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx index e9d6b9954..deddee560 100644 --- a/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx +++ b/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx @@ -2,17 +2,16 @@ import React, { useCallback, useEffect, useState } from "react"; import { certificateManager } from "@akashnetwork/akashjs/build/certificates/certificate-manager"; import { Snackbar } from "@akashnetwork/ui/components"; -import axios from "axios"; import { event } from "nextjs-google-analytics"; import { useSnackbar } from "notistack"; -import networkStore from "@src/store/networkStore"; -import { RestApiCertificatesResponseType } from "@src/types/certificate"; +import { RestApiCertificate } from "@src/types/certificate"; import { AnalyticsEvents } from "@src/utils/analytics"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; import { getStorageWallets, updateWallet } from "@src/utils/walletUtils"; import { useSettings } from "../SettingsProvider"; import { useWallet } from "../WalletProvider"; +import { ApiUrlService, loadWithPagination } from "@src/utils/apiUtils"; export type LocalCert = { certPem: string; @@ -73,17 +72,14 @@ export const CertificateProvider = ({ children }) => { const { enqueueSnackbar } = useSnackbar(); const { address, signAndBroadcastTx } = useWallet(); const { apiEndpoint } = settings; - const selectedNetwork = networkStore.useSelectedNetwork(); const loadValidCertificates = useCallback( async (showSnackbar?: boolean) => { setIsLoadingCertificates(true); try { - const response = await axios.get( - `${apiEndpoint}/akash/cert/${selectedNetwork.apiVersion}/certificates/list?filter.state=valid&filter.owner=${address}` - ); - const certs = (response.data.certificates || []).map(cert => { + const certificates = await loadWithPagination(ApiUrlService.certificatesList(apiEndpoint, address), "certificates", 1000); + const certs = (certificates || []).map(cert => { const parsed = atob(cert.certificate.cert); const pem = certificateManager.parsePem(parsed); diff --git a/apps/deploy-web/src/types/certificate.ts b/apps/deploy-web/src/types/certificate.ts index 1b9c79b0c..4350aa95f 100644 --- a/apps/deploy-web/src/types/certificate.ts +++ b/apps/deploy-web/src/types/certificate.ts @@ -1,14 +1,16 @@ export type RestApiCertificatesResponseType = { - certificates: { - certificate: { - cert: string; - pubkey: string; - state: string; - }; - serial: string; - }[]; + certificates: RestApiCertificate[]; pagination: { next_key: string; total: string; }; }; + +export type RestApiCertificate = { + certificate: { + cert: string; + pubkey: string; + state: string; + }; + serial: string; +}; diff --git a/apps/deploy-web/src/utils/apiUtils.ts b/apps/deploy-web/src/utils/apiUtils.ts index 57f8ecb9c..8e81c56d8 100644 --- a/apps/deploy-web/src/utils/apiUtils.ts +++ b/apps/deploy-web/src/utils/apiUtils.ts @@ -8,6 +8,9 @@ export class ApiUrlService { static depositParams(apiEndpoint: string) { return `${apiEndpoint}/cosmos/params/v1beta1/params?subspace=deployment&key=MinDeposits`; } + static certificatesList(apiEndpoint: string, address: string) { + return `${apiEndpoint}/akash/cert/${networkStore.apiVersion}/certificates/list?filter.state=valid&filter.owner=${address}`; + } static deploymentList(apiEndpoint: string, address: string, isActive?: boolean) { return `${apiEndpoint}/akash/deployment/${networkStore.apiVersion}/deployments/list?filters.owner=${address}${isActive ? "&filters.state=active" : ""}`; } diff --git a/apps/indexer/env/.env b/apps/indexer/env/.env deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/indexer/env/.env.mainnet b/apps/indexer/env/.env.mainnet new file mode 100644 index 000000000..ba4636212 --- /dev/null +++ b/apps/indexer/env/.env.mainnet @@ -0,0 +1 @@ +ACTIVE_CHAIN=akash \ No newline at end of file diff --git a/apps/indexer/env/.env.production b/apps/indexer/env/.env.production new file mode 100644 index 000000000..fa6b6e04d --- /dev/null +++ b/apps/indexer/env/.env.production @@ -0,0 +1,2 @@ +HEALTH_CHECKS_ENABLED=true +STANDBY=false \ No newline at end of file diff --git a/apps/indexer/env/.env.sample b/apps/indexer/env/.env.sample new file mode 100644 index 000000000..722f04005 --- /dev/null +++ b/apps/indexer/env/.env.sample @@ -0,0 +1,7 @@ +NETWORK=sandbox +ACTIVE_CHAIN= +AKASH_DATABASE_CS= +AKASH_SANDBOX_DATABASE_CS= +AKASH_TESTNET_DATABASE_CS= +KEEP_CACHE= +DATA_FOLDER= diff --git a/apps/indexer/env/.env.sandbox b/apps/indexer/env/.env.sandbox new file mode 100644 index 000000000..8fb0a0a5d --- /dev/null +++ b/apps/indexer/env/.env.sandbox @@ -0,0 +1 @@ +ACTIVE_CHAIN=akashSandbox diff --git a/apps/indexer/env/.env.testnet b/apps/indexer/env/.env.testnet new file mode 100644 index 000000000..6a5374f18 --- /dev/null +++ b/apps/indexer/env/.env.testnet @@ -0,0 +1 @@ +ACTIVE_CHAIN=akashTestnet diff --git a/apps/indexer/package.json b/apps/indexer/package.json index e17a06c8c..6b23c4c92 100644 --- a/apps/indexer/package.json +++ b/apps/indexer/package.json @@ -15,15 +15,16 @@ "main": "server.js", "scripts": { "build": "webpack --config webpack.prod.js", + "dev": "npm run start", "format": "prettier --write ./*.{js,json} **/*.{ts,js,json}", "lint": "eslint .", "start": "webpack --mode development --config webpack.dev.js --watch", - "dev": "npm run start", "test": "jest" }, "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/database": "*", + "@akashnetwork/env-loader": "*", "@cosmjs/crypto": "^0.32.4", "@cosmjs/encoding": "^0.32.4", "@cosmjs/math": "^0.32.4", diff --git a/apps/indexer/src/index.ts b/apps/indexer/src/index.ts index f44c6266f..e6963fa69 100644 --- a/apps/indexer/src/index.ts +++ b/apps/indexer/src/index.ts @@ -1,3 +1,5 @@ +import "@akashnetwork/env-loader"; + import { activeChain, chainDefinitions } from "@akashnetwork/database/chainDefinitions"; import * as Sentry from "@sentry/node"; import express from "express"; diff --git a/doc/apps-configuration.md b/doc/apps-configuration.md new file mode 100644 index 000000000..c67e93e7a --- /dev/null +++ b/doc/apps-configuration.md @@ -0,0 +1,106 @@ +# Environment Variables & Configuration + +## Overview + +Both the **Next.js frontend** and **Node.js backend** applications use environment variables to manage configuration. This guide provides a unified approach to managing these variables, along with **Zod**-based validation for ensuring correctness across both apps. + +### Environment Files + +We use `.env*` files for managing environment variables, with a consistent loading order across both applications. These files help separate environment-specific configurations from sensitive information. + +### Next.js Environment Variables Behavior + +In addition to the logic described below environment variables in the **Next.js app** follow the standard Next.js behavior, as documented in the [Next.js environment variables documentation](https://nextjs.org/docs/basic-features/environment-variables). This means that files like `.env.local` or `.env.production` will be automatically loaded based on the environment in which the app is running, ensuring smooth transitions between development, staging, and production environments. + +### Loading Order + +The environment variables are loaded in the following order for both **Next.js** and **Node.js** applications: + +1. **System Environment Variables**: Variables set in the system environment (e.g., through CI/CD pipelines) take the highest precedence and are never overridden by values from `.env` files. +2. **`env/.env.local`**: Contains sensitive values specific to local development. This file is not tracked by Git and should contain secrets for local use. +3. **`env/.env`**: Contains default values applicable to all environments. This file is included in version control and should not contain sensitive data. +4. **`env/.env.${DEPLOYMENT_ENV}`**: Contains values specific to the deployment environment (e.g., production, staging). This file is loaded based on the `DEPLOYMENT_ENV` variable. +5. **`env/.env.${NETWORK}`**: Contains values specific to the network environment (e.g., mainnet, testnet). This file is loaded based on the `NETWORK` variable. + +### Variable Precedence + +- Variables loaded from higher-priority sources (like system environment variables or `.env.local`) will override those defined in lower-priority files (such as `.env` or `.env.production`). + +### Configuration Structure + +All application configurations should be organized and stored in specific files within the app directories: + +- **Configuration Files**: + - Configurations must be placed in `apps/*/config/.config.ts` files for each module-specific configuration. + - Environment variables should retain their original **SCREAMING_CASE** names and be parsed/validated directly using **Zod** schemas, without renaming. + +- **Split by Domain**: + - Configurations should be **split by application domain**. This helps maintain clarity and separation of concerns, with configurations logically grouped based on the features or domains they pertain to (e.g., database, authentication, API endpoints). + +### Environment Variables + +- **Separation of Environment Variables**: + - Environment variables must be separated from hardcoded configuration values to keep sensitive or environment-specific data outside of the codebase. + - Use `.env*` files as described in the **Loading Order** section, and ensure all environment variables are **validated** using schemas before they are used within the application. + +### Zod-Based Validation + +Both the **Next.js** and **Node.js** apps use **Zod** schemas to validate environment variables, ensuring that all required variables are present and have valid values. Validation is applied at two stages: + +- **Build-Time Validation** (Next.js only): In the **Next.js** app, variables are validated at build time using **Zod** schemas defined in the `src/config/env-config.schema.ts` file. If any required variables are missing or invalid, the build will fail. + +- **Runtime Validation** (All Apps): Both **Next.js** and **Node.js** applications perform **runtime validation** when the server starts. This ensures that all critical environment variables are present and valid before the server launches. If any required variables are missing or incorrect, the server will fail to start. + +### Example of Environment Variable Validation with Zod + +```typescript +// apps/config/env.config.ts +import { z } from "zod"; + +// Define the schema for environment variables +const envSchema = z.object({ + LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).optional().default("info"), + DATABASE_HOST: z.string(), + DATABASE_USER: z.string(), + SECRET_KEY: z.string(), +}); + +// Parse and validate the environment variables +export const envConfig = envSchema.parse(process.env); + +// Access the variables +console.log(envConfig.LOG_LEVEL); +console.log(envConfig.DATABASE_HOST); +``` + +### Sample Environment Variables + +Here’s an example `.env` file that corresponds to the validation schema above: + +```bash +# .env (shared across environments) +LOG_LEVEL=info +DATABASE_HOST=https://db.example.com +DATABASE_USER=myUser +SECRET_KEY=MY_SECRET_KEY + +# .env.local (development-specific, not included in version control) +DATABASE_HOST=http://localhost:5432 +DATABASE_USER=localUser +SECRET_KEY=LOCAL_SECRET_KEY + +# .env.production (production-specific) +DATABASE_HOST=https://prod-db.example.com +SECRET_KEY=PROD_SECRET_KEY +``` + +### Sample Environment Variables Template + +A template for setting up the required environment variables is provided in the `env/.env.sample` file for both types of applications. This file contains examples of all the necessary environment variables. + +By following this approach, we ensure a secure, scalable, and consistent configuration process for managing environment variables in both **Next.js** and **Node.js** applications, with robust validation through **Zod** and clear separation of configurations by application domain. + +### Disclaimer + +If you find any inconsistencies in the codebase compared to this documentation, please raise an issue or create a pull request to update the codebase accordingly. This documentation serves as the source of truth for managing environment variables and configurations across the applications. + diff --git a/package-lock.json b/package-lock.json index 956411e76..6f5620eb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,12 +28,13 @@ }, "apps/api": { "name": "console-api", - "version": "2.23.3", + "version": "2.23.4", "license": "Apache-2.0", "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/akashjs": "^0.10.0", "@akashnetwork/database": "*", + "@akashnetwork/env-loader": "*", "@akashnetwork/http-sdk": "*", "@casl/ability": "^6.7.1", "@chain-registry/assets": "^1.64.79", @@ -262,7 +263,7 @@ }, "apps/deploy-web": { "name": "akash-console", - "version": "2.16.0", + "version": "2.16.2", "license": "Apache-2.0", "dependencies": { "@akashnetwork/akash-api": "^1.3.0", @@ -455,6 +456,7 @@ "dependencies": { "@akashnetwork/akash-api": "^1.3.0", "@akashnetwork/database": "*", + "@akashnetwork/env-loader": "*", "@cosmjs/crypto": "^0.32.4", "@cosmjs/encoding": "^0.32.4", "@cosmjs/math": "^0.32.4", diff --git a/packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts b/packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts index 69d7502fa..fb566a688 100644 --- a/packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts +++ b/packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts @@ -10,7 +10,7 @@ export interface ApiWalletOutput { export class ManagedWalletHttpService extends ApiHttpService { async createWallet(userId: string) { - return this.addWalletEssentials(this.extractApiData(await this.post("v1/wallets", { data: { userId } }))); + return this.addWalletEssentials(this.extractApiData(await this.post("v1/start-trial", { data: { userId } }))); } async getWallet(userId: string) {