Skip to content

Commit

Permalink
Merge branch 'main' into feature/improve-stats-app-resiliency
Browse files Browse the repository at this point in the history
  • Loading branch information
forbesus authored Oct 2, 2024
2 parents 3707f0a + f3f7df4 commit 5997f85
Show file tree
Hide file tree
Showing 37 changed files with 257 additions and 132 deletions.
2 changes: 2 additions & 0 deletions .commitlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"deployment",
"dx",
"stats"
"certificate",
"dx"
]
]
}
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</div>

- [Quick Start](#quick-start)
- [Apps Configuration](./doc/apps-configuration.md)
- [Services](#services)
- [Monitoring](#monitoring)
- [Example SQL Queries](#example-sql-queries)
Expand Down
17 changes: 0 additions & 17 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ if (BILLING_ENABLED === "true") {
const { AuthInterceptor } = require("./auth/services/auth.interceptor");
appHono.use(container.resolve<HonoInterceptor>(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);
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/billing/controllers/wallet/wallet.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -22,7 +22,7 @@ export class WalletController {

@WithTransaction()
@Protected([{ action: "create", subject: "UserWallet" }])
async create({ data: { userId } }: CreateWalletRequestInput): Promise<WalletOutputResponse> {
async create({ data: { userId } }: StartTrialRequestInput): Promise<WalletOutputResponse> {
return {
data: await this.walletInitializer.initializeAndGrantTrialLimits(userId)
};
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/billing/routes/checkout/checkout.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/billing/routes/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof CreateWalletRequestInputSchema>;
export type StartTrialRequestInput = z.infer<typeof StartTrialRequestInputSchema>;

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
}
}
}
Expand All @@ -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);
});
19 changes: 13 additions & 6 deletions apps/api/src/caching/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> } = {};

Expand Down Expand Up @@ -30,20 +33,24 @@ export const Memoize = (options?: MemoizeOptions) => (target: object, propertyNa
export async function cacheResponse<T>(seconds: number, key: string, refreshRequest: () => Promise<T>, keepData?: boolean): Promise<T> {
const duration = seconds * 1000;
const cachedObject = cacheEngine.getFromCache(key) as CachedObject<T> | 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];
Expand All @@ -52,10 +59,10 @@ export async function cacheResponse<T>(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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/console.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "reflect-metadata";
import "./dotenv";
import "@akashnetwork/env-loader";
import "./open-telemetry";

import { context, trace } from "@opentelemetry/api";
Expand Down
23 changes: 0 additions & 23 deletions apps/api/src/dotenv.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down
16 changes: 15 additions & 1 deletion apps/api/src/routes/v1/addresses/deployments.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions apps/api/src/services/external/githubService.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,19 +23,25 @@ export function getOctokit() {
export const getProviderAttributesSchema = async (): Promise<ProviderAttributesSchema> => {
// Fetching provider attributes schema
const response = await cacheResponse(
30,
minutesToSeconds(5),
cacheKeys.getProviderAttributesSchema,
async () => await axios.get<ProviderAttributesSchema>("https://raw.githubusercontent.com/akash-network/console/main/config/provider-attributes.json")
async () => await axios.get<ProviderAttributesSchema>("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<Auditor[]>("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<Auditor[]>("https://raw.githubusercontent.com/akash-network/console/main/config/auditors.json");
return res.data;
},
true
);

return response;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BillingConfig>(BILLING_CONFIG);
const db = container.resolve<ApiPgDatabase>(POSTGRES_DB);
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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" })
Expand Down
2 changes: 1 addition & 1 deletion apps/api/test/services/wallet-testing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading

0 comments on commit 5997f85

Please sign in to comment.