Skip to content

Commit

Permalink
feat(billing): add billing module with trial wallet creation
Browse files Browse the repository at this point in the history
As a part of this add:
- DI container
- Drizzle ORM
- logging

refs #247
  • Loading branch information
ygrishajev committed Jul 9, 2024
1 parent 4c2c88b commit ddcc130
Show file tree
Hide file tree
Showing 59 changed files with 2,796 additions and 48 deletions.
9 changes: 9 additions & 0 deletions apps/api/.env.functional.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MASTER_WALLET_MNEMONIC="south super just human picture mesh garlic carbon witness drill river design"
NETWORK=sandbox
POSTGRES_DB_URI=postgres://postgres:[email protected]:5432/cloudmos-users
RPC_NODE_ENDPOINT=https://rpc.sandbox-01.aksh.pw:443
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT=100000
TRIAL_FEES_ALLOWANCE_AMOUNT=100000
TRIAL_ALLOWANCE_DENOM=uakt
LOG_LEVEL=info
BILLING_ENABLED=true
12 changes: 12 additions & 0 deletions apps/api/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "drizzle-kit";

import { env } from "./src/utils/env";

export default defineConfig({
schema: "./src/billing/model-schemas",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: env.UserDatabaseCS
}
});
7 changes: 7 additions & 0 deletions apps/api/drizzle/0000_chilly_mastermind.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS "user_wallets" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" varchar,
"address" varchar,
"stripe_customer_id" varchar,
"credit_amount" numeric(10, 2) DEFAULT '0.00' NOT NULL
);
56 changes: 56 additions & 0 deletions apps/api/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"id": "777d9ff9-333b-42b3-a5f4-759b3774909b",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.user_wallets": {
"name": "user_wallets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "varchar",
"primaryKey": false,
"notNull": false
},
"address": {
"name": "address",
"type": "varchar",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar",
"primaryKey": false,
"notNull": false
},
"credit_amount": {
"name": "credit_amount",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": true,
"default": "'0.00'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
13 changes: 13 additions & 0 deletions apps/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1720022370732,
"tag": "0000_chilly_mastermind",
"breakpoints": true
}
]
}
23 changes: 17 additions & 6 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"main": "server.js",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "npm run start",
"format": "prettier --write ./*.{js,json} **/*.{ts,js,json}",
"lint": "eslint .",
"migrate": "node-pg-migrate",
"start": "webpack --config webpack.dev.js --watch",
"dev": "npm run start",
"console": "webpack --config webpack.prod.js --entry ./src/console.ts --output-filename console.js && node dist/console.js",
"test": "jest --selectProjects unit functional",
"test:cov": "jest --selectProjects unit functional --coverage",
"test:functional": "jest --selectProjects functional",
Expand All @@ -23,7 +25,8 @@
"test:unit": "jest --selectProjects unit",
"test:unit:cov": "jest --selectProjects unit --coverage",
"test:unit:watch": "jest --selectProjects unit --watch",
"test:watch": "jest --selectProjects unit functional --watch"
"test:watch": "jest --selectProjects unit functional --watch",
"migration:gen": "drizzle-kit generate"
},
"dependencies": {
"@akashnetwork/akash-api": "^1.3.0",
Expand All @@ -40,27 +43,34 @@
"@hono/zod-openapi": "0.9.5",
"@octokit/rest": "^18.12.0",
"@sentry/node": "^7.55.2",
"@supercharge/promise-pool": "^3.2.0",
"axios": "^0.27.2",
"commander": "^12.1.0",
"cosmjs-types": "^0.5.0",
"date-fns": "^2.29.2",
"date-fns-tz": "^1.3.6",
"dotenv": "^12.0.4",
"drizzle-orm": "^0.31.2",
"hono": "3.12.0",
"human-interval": "^2.0.1",
"js-sha256": "^0.9.0",
"lodash": "^4.17.21",
"markdown-to-txt": "^2.0.1",
"memory-cache": "^0.2.0",
"node-dependency-injection": "^3.1.2",
"node-fetch": "^2.6.1",
"pg": "^8.7.3",
"pg": "^8.12.0",
"pg-hstore": "^2.3.4",
"pino": "^9.2.0",
"pino-pretty": "^11.2.1",
"protobufjs": "^6.11.2",
"semver": "^7.3.8",
"sequelize": "^6.21.3",
"sequelize-typescript": "^2.1.5",
"sql-formatter": "^15.3.2",
"stripe": "^10.14.0",
"uuid": "^9.0.1",
"zod": "^3.22.4"
"tsyringe": "^4.8.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@akashnetwork/dev-config": "*",
Expand All @@ -70,11 +80,12 @@
"@types/memory-cache": "^0.2.2",
"@types/node": "20.14.0",
"@types/node-fetch": "^2.6.2",
"@types/pg": "^8.6.5",
"@types/pg": "^8.11.6",
"@types/semver": "^7.5.2",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"alias-hq": "^5.1.6",
"drizzle-kit": "^0.22.7",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.3",
"eslint-plugin-simple-import-sort": "^12.1.0",
Expand Down
41 changes: 27 additions & 14 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "reflect-metadata";

import { serve } from "@hono/node-server";
// TODO: find out how to properly import this
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -6,7 +8,10 @@ import { sentry } from "@hono/sentry";
import * as Sentry from "@sentry/node";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { container } from "tsyringe";

import { BILLING_CONFIG, BillingConfig } from "@src/billing/providers";
import { HttpLoggerService, LoggerService, RequestStorageService } from "@src/core";
import packageJson from "../package.json";
import { chainDb, syncUserSchema, userDb } from "./db/dbConnection";
import { apiRouter } from "./routers/apiRouter";
Expand All @@ -18,6 +23,7 @@ import { web3IndexRouter } from "./routers/web3indexRouter";
import { isProd } from "./utils/constants";
import { env } from "./utils/env";
import { bytesToHumanReadableSize } from "./utils/files";
import { walletRouter } from "./billing";
import { Scheduler } from "./scheduler";

const appHono = new Hono();
Expand Down Expand Up @@ -55,6 +61,8 @@ const scheduler = new Scheduler({
}
});

appHono.use(container.resolve(RequestStorageService).intercept());
appHono.use(container.resolve(HttpLoggerService).intercept());
appHono.use(
"*",
sentry({
Expand All @@ -77,6 +85,12 @@ appHono.route("/web3-index", web3IndexRouter);
appHono.route("/dashboard", dashboardRouter);
appHono.route("/internal", internalRouter);

const billingConfig = container.resolve<BillingConfig>(BILLING_CONFIG);

if (billingConfig.BILLING_ENABLED) {
appHono.route("/", walletRouter);
}

appHono.get("/status", c => {
const version = packageJson.version;
const tasksStatus = scheduler.getTasksStatus();
Expand All @@ -95,6 +109,8 @@ function startScheduler() {
scheduler.start();
}

const appLogger = new LoggerService({ context: "APP" });

/**
* Initialize database
* Start scheduler
Expand All @@ -105,15 +121,14 @@ export async function initApp() {
await initDb();
startScheduler();

console.log("Starting server at http://localhost:" + PORT);
appLogger.info({ event: "SERVER_STARTING", url: `http://localhost:${PORT}` });
serve({
fetch: appHono.fetch,
port: typeof PORT === "string" ? parseInt(PORT) : PORT
});
} catch (err) {
console.error("Error while initializing app", err);

Sentry.captureException(err);
} catch (error) {
appLogger.error({ event: "APP_INIT_ERROR", error });
Sentry.captureException(error);
}
}

Expand All @@ -123,20 +138,18 @@ export async function initApp() {
* Create backups per version
* Load from backup if exists for current version
*/
export async function initDb(options: { log?: boolean } = { log: true }) {
const log = (value: string) => options?.log && console.log(value);

log(`Connecting to chain database (${chainDb.config.host}/${chainDb.config.database})...`);
export async function initDb() {
appLogger.debug(`Connecting to chain database (${chainDb.config.host}/${chainDb.config.database})...`);
await chainDb.authenticate();
log("Connection has been established successfully.");
appLogger.debug("Connection has been established successfully.");

log(`Connecting to user database (${userDb.config.host}/${userDb.config.database})...`);
appLogger.debug(`Connecting to user database (${userDb.config.host}/${userDb.config.database})...`);
await userDb.authenticate();
log("Connection has been established successfully.");
appLogger.debug("Connection has been established successfully.");

log("Sync user schema...");
appLogger.debug("Sync user schema...");
await syncUserSchema();
log("User schema synced.");
appLogger.debug("User schema synced.");
}

export { appHono as app };
19 changes: 19 additions & 0 deletions apps/api/src/billing/config/env.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import dotenv from "dotenv";
import { z } from "zod";

dotenv.config({ path: ".env.local" });
dotenv.config();

const envSchema = z.object({
MASTER_WALLET_MNEMONIC: z.string(),
NETWORK: z.enum(["mainnet", "testnet", "sandbox"]),
RPC_NODE_ENDPOINT: z.string(),
TRIAL_ALLOWANCE_EXPIRATION_DAYS: z.number({ coerce: true }).default(14),
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
TRIAL_FEES_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
TRIAL_ALLOWANCE_DENOM: z.string(),
GAS_SAFETY_MULTIPLIER: z.number({ coerce: true }).default(1.5),
BILLING_ENABLED: z.enum(["true", "false"]).transform(value => value === "true")
});

export const envConfig = envSchema.parse(process.env);
7 changes: 7 additions & 0 deletions apps/api/src/billing/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { envConfig } from "./env.config";
import { USDC_IBC_DENOMS } from "./network.config";

export const config = {
...envConfig,
USDC_IBC_DENOMS
};
4 changes: 4 additions & 0 deletions apps/api/src/billing/config/network.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const USDC_IBC_DENOMS = {
mainnetId: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1",
sandboxId: "ibc/12C6A0C374171B595A0A9E18B83FA09D295FB1F2D8C6DAA3AC28683471752D84"
};
38 changes: 38 additions & 0 deletions apps/api/src/billing/controllers/wallet/wallet.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PromisePool } from "@supercharge/promise-pool";
import { singleton } from "tsyringe";

import { UserWalletRepository } from "@src/billing/repositories";
import { CreateWalletInput, CreateWalletOutput } from "@src/billing/routes";
import { WalletInitializerService, WalletService } from "@src/billing/services";
import { WithTransaction } from "@src/core/services";

@singleton()
export class WalletController {
constructor(
private readonly walletManager: WalletService,
private readonly userWalletRepository: UserWalletRepository,
private readonly walletInitializer: WalletInitializerService
) {}

@WithTransaction()
async create({ userId }: CreateWalletInput): Promise<CreateWalletOutput> {
await this.walletInitializer.initialize(userId);
}

async refillAll() {
const wallets = await this.userWalletRepository.find();
const { results, errors } = await PromisePool.withConcurrency(2)
.for(wallets)
.process(async wallet => {
const refilled = await this.walletManager.refill(wallet);
console.log("DEBUG refilled", refilled);
return refilled;
});

if (errors) {
console.log("DEBUG errors", errors);
}

console.log("DEBUG results", results);
}
}
3 changes: 3 additions & 0 deletions apps/api/src/billing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import "./providers";

export * from "./routes";
1 change: 1 addition & 0 deletions apps/api/src/billing/model-schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./user-wallet/user-wallet.schema";
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { numeric, pgTable, serial, varchar } from "drizzle-orm/pg-core";

export const userWalletSchema = pgTable("user_wallets", {
id: serial("id").primaryKey(),
userId: varchar("user_id"),
address: varchar("address"),
stripeCustomerId: varchar("stripe_customer_id"),
creditAmount: numeric("credit_amount", {
precision: 10,
scale: 2
})
.notNull()
.default("0.00")
});
1 change: 1 addition & 0 deletions apps/api/src/billing/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./user-wallet/user-wallet.model";
Loading

0 comments on commit ddcc130

Please sign in to comment.