diff --git a/apps/api/src/billing/config/env.ts b/apps/api/src/billing/config/env.ts index e258a58cc..a2e28b833 100644 --- a/apps/api/src/billing/config/env.ts +++ b/apps/api/src/billing/config/env.ts @@ -6,7 +6,6 @@ dotenv.config(); const envSchema = z.object({ MASTER_WALLET_MNEMONIC: z.string(), - POSTGRES_DB_URI: z.string(), NETWORK: z.enum(["mainnet", "testnet", "sandbox"]), RPC_NODE_ENDPOINT: z.string(), TRIAL_ALLOWANCE_EXPIRATION_DAYS: z.number({ coerce: true }).default(14), diff --git a/apps/api/src/billing/config/index.ts b/apps/api/src/billing/config/index.ts index d9d673791..935c8cda0 100644 --- a/apps/api/src/billing/config/index.ts +++ b/apps/api/src/billing/config/index.ts @@ -5,4 +5,3 @@ export const config = { ...env, USDC_IBC_DENOMS }; -export type BillingConfig = typeof config; diff --git a/apps/api/src/billing/controllers/wallet/WalletController.ts b/apps/api/src/billing/controllers/wallet/WalletController.ts index 932b24ecd..7cd5e3789 100644 --- a/apps/api/src/billing/controllers/wallet/WalletController.ts +++ b/apps/api/src/billing/controllers/wallet/WalletController.ts @@ -1,24 +1,22 @@ import { PromisePool } from "@supercharge/promise-pool"; import { singleton } from "tsyringe"; -import { CreateWalletInput, CreateWalletOutput } from "@src/billing"; import { UserWalletRepository } from "@src/billing/repositories"; -import { WalletManager } from "@src/billing/services"; -import { TxManager, WithTransaction } from "@src/core/services/tx-manager/TxManager"; +import { CreateWalletInput, CreateWalletOutput } from "@src/billing/routes"; +import { WalletInitializer, WalletManager } from "@src/billing/services"; +import { WithTransaction } from "@src/core/services"; @singleton() export class WalletController { constructor( private readonly walletManager: WalletManager, private readonly userWalletRepository: UserWalletRepository, - private readonly txManager: TxManager + private readonly walletInitializer: WalletInitializer ) {} @WithTransaction() async create({ userId }: CreateWalletInput): Promise { - const userWallet = await this.userWalletRepository.create({ userId }); - const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); - await this.userWalletRepository.updateById(userWallet.id, { address: wallet.address }); + await this.walletInitializer.initialize(userId); } async refillAll() { diff --git a/apps/api/src/billing/model-schemas/userWalletSchema.ts b/apps/api/src/billing/model-schemas/userWalletSchema.ts index 5ca6637ad..74ab3f2a8 100644 --- a/apps/api/src/billing/model-schemas/userWalletSchema.ts +++ b/apps/api/src/billing/model-schemas/userWalletSchema.ts @@ -12,5 +12,3 @@ export const userWalletSchema = pgTable("user_wallets", { .notNull() .default("0.00") }); - -export type UserWalletSchema = typeof userWalletSchema; diff --git a/apps/api/src/billing/providers/configProvider.ts b/apps/api/src/billing/providers/configProvider.ts index 0d3a97d9f..3ebfe3fbc 100644 --- a/apps/api/src/billing/providers/configProvider.ts +++ b/apps/api/src/billing/providers/configProvider.ts @@ -1,7 +1,11 @@ -import { container } from "tsyringe"; +import { container, inject } from "tsyringe"; import { config } from "@src/billing/config"; export const BILLING_CONFIG = "BILLING_CONFIG"; container.register(BILLING_CONFIG, { useValue: config }); + +export type BillingConfig = typeof config; + +export const InjectBillingConfig = () => inject(BILLING_CONFIG); diff --git a/apps/api/src/billing/providers/index.ts b/apps/api/src/billing/providers/index.ts index 3fab55062..b28f1ba2a 100644 --- a/apps/api/src/billing/providers/index.ts +++ b/apps/api/src/billing/providers/index.ts @@ -1,2 +1,5 @@ import "./userWalletSchemaProvider"; import "./configProvider"; + +export * from "./userWalletSchemaProvider"; +export * from "./configProvider"; diff --git a/apps/api/src/billing/providers/userWalletSchemaProvider.ts b/apps/api/src/billing/providers/userWalletSchemaProvider.ts index 777321af7..70675d98f 100644 --- a/apps/api/src/billing/providers/userWalletSchemaProvider.ts +++ b/apps/api/src/billing/providers/userWalletSchemaProvider.ts @@ -1,7 +1,11 @@ -import { container } from "tsyringe"; +import { container, inject } from "tsyringe"; import { userWalletSchema } from "@src/billing/model-schemas"; export const USER_WALLET_SCHEMA = "USER_WALLET_SCHEMA"; container.register(USER_WALLET_SCHEMA, { useValue: userWalletSchema }); + +export type UserWalletSchema = typeof userWalletSchema; + +export const InjectUserWalletSchema = () => inject(USER_WALLET_SCHEMA); diff --git a/apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts b/apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts index 1b0b1eed0..8527fb67c 100644 --- a/apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts +++ b/apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts @@ -1,21 +1,22 @@ import { eq } from "drizzle-orm"; import first from "lodash/first"; -import { inject, singleton } from "tsyringe"; +import { singleton } from "tsyringe"; -import type { UserWalletSchema } from "@src/billing/model-schemas/userWalletSchema"; -import { USER_WALLET_SCHEMA } from "@src/billing/providers/userWalletSchemaProvider"; +import { InjectUserWalletSchema, UserWalletSchema } from "@src/billing/providers"; import { ApiPgDatabase, InjectPg } from "@src/core/providers"; -import { TxManager } from "@src/core/services/tx-manager/TxManager"; +import { TxManager } from "@src/core/services"; + +export type UserInput = Partial; @singleton() export class UserWalletRepository { constructor( @InjectPg() private readonly pg: ApiPgDatabase, - @inject(USER_WALLET_SCHEMA) private readonly userWallet: UserWalletSchema, + @InjectUserWalletSchema() private readonly userWallet: UserWalletSchema, private readonly txManager: TxManager ) {} - async create(input: Pick) { + async create(input: Pick) { const pg = this.txManager.getPgTx() || this.pg; return first( await pg @@ -30,7 +31,7 @@ export class UserWalletRepository { async updateById( id: UserWalletSchema["$inferSelect"]["id"], - payload: Partial, + payload: Partial, options?: { returning: R } ): Promise { const pg = this.txManager.getPgTx() || this.pg; diff --git a/apps/api/src/billing/services/index.ts b/apps/api/src/billing/services/index.ts index 07929d218..f91a55b2c 100644 --- a/apps/api/src/billing/services/index.ts +++ b/apps/api/src/billing/services/index.ts @@ -1 +1,3 @@ export * from "./wallet-manager/WalletManager"; +export * from "./rpc-message-service/RpcMessageService"; +export * from "./wallet-initializer/WalletInitializer"; diff --git a/apps/api/src/billing/services/wallet-initializer/WalletInitializer.ts b/apps/api/src/billing/services/wallet-initializer/WalletInitializer.ts new file mode 100644 index 000000000..8847e0ae0 --- /dev/null +++ b/apps/api/src/billing/services/wallet-initializer/WalletInitializer.ts @@ -0,0 +1,20 @@ +import { singleton } from "tsyringe"; + +import { UserInput, UserWalletRepository } from "@src/billing/repositories"; +import { WalletManager } from "@src/billing/services"; +import { WithTransaction } from "@src/core/services"; + +@singleton() +export class WalletInitializer { + constructor( + private readonly walletManager: WalletManager, + private readonly userWalletRepository: UserWalletRepository + ) {} + + @WithTransaction() + async initialize(userId: UserInput["userId"]) { + const userWallet = await this.userWalletRepository.create({ userId }); + const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); + await this.userWalletRepository.updateById(userWallet.id, { address: wallet.address }); + } +} diff --git a/apps/api/src/billing/services/wallet-manager/WalletManager.ts b/apps/api/src/billing/services/wallet-manager/WalletManager.ts index db33b8ccc..a3c54a0f7 100644 --- a/apps/api/src/billing/services/wallet-manager/WalletManager.ts +++ b/apps/api/src/billing/services/wallet-manager/WalletManager.ts @@ -2,12 +2,11 @@ import { stringToPath } from "@cosmjs/crypto"; import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing"; import { calculateFee, GasPrice, SigningStargateClient } from "@cosmjs/stargate"; import add from "date-fns/add"; -import { inject, singleton } from "tsyringe"; +import { singleton } from "tsyringe"; -import type { BillingConfig } from "@src/billing/config"; -import { BILLING_CONFIG } from "@src/billing/providers/configProvider"; +import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; import { RpcMessageService } from "@src/billing/services/rpc-message-service/RpcMessageService"; -import { ContextualLogger } from "@src/core/services/contextual-logger/ContextualLogger"; +import { ContextualLogger } from "@src/core/services"; interface SpendingAuthorizationOptions { address: string; @@ -31,7 +30,7 @@ export class WalletManager { private readonly logger = new ContextualLogger({ context: WalletManager.name }); constructor( - @inject(BILLING_CONFIG) private readonly config: BillingConfig, + @InjectBillingConfig() private readonly config: BillingConfig, private readonly rpcMessageService: RpcMessageService ) {} diff --git a/apps/api/src/core/config/env.ts b/apps/api/src/core/config/env.ts index b9379fc49..9d19d1328 100644 --- a/apps/api/src/core/config/env.ts +++ b/apps/api/src/core/config/env.ts @@ -5,7 +5,8 @@ dotenv.config({ path: ".env.local" }); dotenv.config(); const envSchema = z.object({ - LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).optional().default("info") + LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).optional().default("info"), + POSTGRES_DB_URI: z.string() }); export const env = envSchema.parse(process.env); diff --git a/apps/api/src/core/services/index.ts b/apps/api/src/core/services/index.ts index b3ba91218..a184047c8 100644 --- a/apps/api/src/core/services/index.ts +++ b/apps/api/src/core/services/index.ts @@ -1 +1,6 @@ -import "./postgres/postgres"; +export * from "./tx-manager/TxManager"; +export * from "./logger/Logger"; +export * from "./contextual-logger/ContextualLogger"; +export * from "./postgres/postgres"; +export * from "./log-http-requests/logHttpRequests"; +export * from "./request-storage/RequestStorage"; diff --git a/apps/api/src/core/services/log-http-requests/logHttpRequests.ts b/apps/api/src/core/services/log-http-requests/logHttpRequests.ts index 1e154acc6..46e87b680 100644 --- a/apps/api/src/core/services/log-http-requests/logHttpRequests.ts +++ b/apps/api/src/core/services/log-http-requests/logHttpRequests.ts @@ -1,6 +1,6 @@ import { Context, Next } from "hono"; -import { ContextualLogger } from "@src/core/services/contextual-logger/ContextualLogger"; +import { ContextualLogger } from "@src/core/services"; export const logHttpRequests = () => async (c: Context, next: Next) => { const logger = new ContextualLogger({ context: "HTTP" }); diff --git a/apps/api/src/core/services/logger/Logger.ts b/apps/api/src/core/services/logger/Logger.ts index 8c37511ce..47d596e90 100644 --- a/apps/api/src/core/services/logger/Logger.ts +++ b/apps/api/src/core/services/logger/Logger.ts @@ -1,6 +1,6 @@ import pino, { Bindings } from "pino"; -import { env } from "@src/core/config/env"; +import { config } from "@src/core/config"; export class Logger { private readonly pino: pino.Logger; @@ -10,7 +10,7 @@ export class Logger { } constructor(bindings?: Bindings) { - this.pino = pino({ level: env.LOG_LEVEL }); + this.pino = pino({ level: config.LOG_LEVEL }); if (bindings) { this.pino = this.logger.child(bindings); diff --git a/apps/api/src/core/services/postgres/postgres.ts b/apps/api/src/core/services/postgres/postgres.ts index 71f63b289..63eb2895c 100644 --- a/apps/api/src/core/services/postgres/postgres.ts +++ b/apps/api/src/core/services/postgres/postgres.ts @@ -4,11 +4,11 @@ import { migrate } from "drizzle-orm/node-postgres/migrator"; import { Pool } from "pg"; import * as billingSchemas from "@src/billing/model-schemas"; +import { config } from "@src/core/config"; import { ContextualLogger } from "@src/core/services/contextual-logger/ContextualLogger"; -import { env } from "@src/utils/env"; const pool = new Pool({ - connectionString: env.UserDatabaseCS + connectionString: config.POSTGRES_DB_URI }); class ContextualLogWriter implements LogWriter { diff --git a/apps/api/src/core/services/tx-manager/TxManager.ts b/apps/api/src/core/services/tx-manager/TxManager.ts index d31276cf2..f3bb49ac4 100644 --- a/apps/api/src/core/services/tx-manager/TxManager.ts +++ b/apps/api/src/core/services/tx-manager/TxManager.ts @@ -4,8 +4,8 @@ import type { PgTransaction } from "drizzle-orm/pg-core"; import { AsyncLocalStorage } from "node:async_hooks"; import { container, singleton } from "tsyringe"; -import { ApiPgDatabase, InjectPg } from "@src/core/providers/PostgresProvider"; -import type { ApiPgSchema } from "@src/core/services/postgres/postgres"; +import { ApiPgDatabase, InjectPg } from "@src/core/providers"; +import type { ApiPgSchema } from "@src/core/services"; type TxType = "PG_TX"; diff --git a/apps/api/test/functional/create-wallet.spec.ts b/apps/api/test/functional/create-wallet.spec.ts index f5643f241..2dc39bd1c 100644 --- a/apps/api/test/functional/create-wallet.spec.ts +++ b/apps/api/test/functional/create-wallet.spec.ts @@ -3,13 +3,12 @@ import { eq } from "drizzle-orm"; import { container } from "tsyringe"; import { app, initDb } from "@src/app"; -import type { UserWalletSchema } from "@src/billing/model-schemas"; -import { USER_WALLET_SCHEMA } from "@src/billing/providers/userWalletSchemaProvider"; -import { ApiPgDatabase, POSTGRES_DB } from "@src/core/providers"; +import { USER_WALLET_SCHEMA, UserWalletSchema } from "@src/billing/providers"; +import { ApiPgDatabase, POSTGRES_DB } from "@src/core/providers/PostgresProvider"; import { closeConnections } from "@src/db/dbConnection"; jest.setTimeout(30000); -describe("app", () => { +describe("wallets", () => { const schema = container.resolve(USER_WALLET_SCHEMA); const db = container.resolve(POSTGRES_DB); const userWalletsTable = db.query.userWalletSchema; @@ -27,7 +26,7 @@ describe("app", () => { }); describe("POST /v1/wallets", () => { - it("should create a wallet and assign it to a user", async () => { + it("should create a wallet for a user", async () => { const userId = faker.string.uuid(); const res = await app.request("/v1/wallets", { method: "POST",