From 963808e576ca02f5b6a08c1d0fe49cfca80fbaa7 Mon Sep 17 00:00:00 2001 From: okjodom Date: Tue, 26 Nov 2024 09:35:57 +0300 Subject: [PATCH] feat: define solowallet service --- apps/solowallet/.env.manual | 3 + apps/solowallet/Dockerfile | 32 ++++++++++ apps/solowallet/README.md | 19 ++++++ apps/solowallet/src/db/index.ts | 2 + .../src/db/solowallet.repository.ts | 17 +++++ apps/solowallet/src/db/solowallet.schema.ts | 14 +++++ apps/solowallet/src/main.ts | 33 ++++++++++ .../src/solowallet.controller.spec.ts | 19 ++++++ apps/solowallet/src/solowallet.controller.ts | 16 +++++ apps/solowallet/src/solowallet.module.ts | 32 ++++++++++ apps/solowallet/src/solowallet.service.ts | 18 ++++++ apps/solowallet/test/app.e2e-spec.ts | 24 +++++++ apps/solowallet/test/jest-e2e.json | 9 +++ apps/solowallet/tsconfig.app.json | 9 +++ libs/common/src/dto/index.ts | 1 + libs/common/src/dto/solowallet.dto.ts | 18 ++++++ libs/common/src/types/index.ts | 1 + libs/common/src/types/proto/lightning.ts | 12 ++++ libs/common/src/types/proto/solowallet.ts | 63 +++++++++++++++++++ libs/common/src/types/proto/swap.ts | 6 +- nest-cli.json | 9 +++ proto/lightning.proto | 8 +++ proto/shares.proto | 2 - proto/solowallet.proto | 21 +++++++ proto/swap.proto | 9 +-- 25 files changed, 384 insertions(+), 13 deletions(-) create mode 100644 apps/solowallet/.env.manual create mode 100644 apps/solowallet/Dockerfile create mode 100644 apps/solowallet/README.md create mode 100644 apps/solowallet/src/db/index.ts create mode 100644 apps/solowallet/src/db/solowallet.repository.ts create mode 100644 apps/solowallet/src/db/solowallet.schema.ts create mode 100644 apps/solowallet/src/main.ts create mode 100644 apps/solowallet/src/solowallet.controller.spec.ts create mode 100644 apps/solowallet/src/solowallet.controller.ts create mode 100644 apps/solowallet/src/solowallet.module.ts create mode 100644 apps/solowallet/src/solowallet.service.ts create mode 100644 apps/solowallet/test/app.e2e-spec.ts create mode 100644 apps/solowallet/test/jest-e2e.json create mode 100644 apps/solowallet/tsconfig.app.json create mode 100644 libs/common/src/dto/solowallet.dto.ts create mode 100644 libs/common/src/types/proto/lightning.ts create mode 100644 libs/common/src/types/proto/solowallet.ts create mode 100644 proto/lightning.proto create mode 100644 proto/solowallet.proto diff --git a/apps/solowallet/.env.manual b/apps/solowallet/.env.manual new file mode 100644 index 0000000..eb7d277 --- /dev/null +++ b/apps/solowallet/.env.manual @@ -0,0 +1,3 @@ +NODE_ENV='development' +SOLOWALLET_GRPC_URL='0.0.0.0:4080' +DATABASE_URL=mongodb://bs:password@mongodb:27017 diff --git a/apps/solowallet/Dockerfile b/apps/solowallet/Dockerfile new file mode 100644 index 0000000..eebf512 --- /dev/null +++ b/apps/solowallet/Dockerfile @@ -0,0 +1,32 @@ +FROM oven/bun:latest AS development + +WORKDIR /usr/src/app + +COPY package.json ./ +COPY bun.lockb ./ +COPY tsconfig.json tsconfig.json +COPY nest-cli.json nest-cli.json + +COPY apps/solowallet apps/solowallet +COPY libs libs +COPY proto proto + +RUN bun install +RUN bun build:solowallet + +FROM oven/bun:latest AS production + +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /usr/src/app + +COPY package.json ./ +COPY bun.lockb ./ + +RUN bun install --production + +COPY --from=development /usr/src/app/dist ./dist +COPY --from=development /usr/src/app/proto ./proto + +CMD ["sh", "-c", "bun run dist/apps/solowallet/main.js"] diff --git a/apps/solowallet/README.md b/apps/solowallet/README.md new file mode 100644 index 0000000..18ee52f --- /dev/null +++ b/apps/solowallet/README.md @@ -0,0 +1,19 @@ +# apps/solowallet + +This app is a gRPC microservice for Bitsacco solo wallet. +The wallet full custodies ecash on behalf of individual members. +Members can withdraw at any time via lightning, or to fiat via integrated Bitsacco `swap` service. + +## Dev + +Run `bun dev solowallet` to launch the microservice in development mode. +Run `bun start` to launch this plus any other microservice and the REST api gateway in dev mode + +## Docs + +- See [solowallet.proto](https://github.com/bitsacco/os/blob/main/proto/solowallet.proto) +- With the microservice running, see supported gRPC methods with type reflection at http://localhost:4070 + +## Architecture + +See [architecture.md](https://github.com/bitsacco/os/blob/main/docs/architecture.md) diff --git a/apps/solowallet/src/db/index.ts b/apps/solowallet/src/db/index.ts new file mode 100644 index 0000000..a7091c7 --- /dev/null +++ b/apps/solowallet/src/db/index.ts @@ -0,0 +1,2 @@ +export * from './solowallet.repository'; +export * from './solowallet.schema'; diff --git a/apps/solowallet/src/db/solowallet.repository.ts b/apps/solowallet/src/db/solowallet.repository.ts new file mode 100644 index 0000000..dd82051 --- /dev/null +++ b/apps/solowallet/src/db/solowallet.repository.ts @@ -0,0 +1,17 @@ +import { Model } from 'mongoose'; +import { InjectModel } from '@nestjs/mongoose'; +import { Injectable, Logger } from '@nestjs/common'; +import { AbstractRepository } from '@bitsacco/common'; +import { SolowalletDocument } from './solowallet.schema'; + +@Injectable() +export class SolowalletRepository extends AbstractRepository { + protected readonly logger = new Logger(SolowalletRepository.name); + + constructor( + @InjectModel(SolowalletDocument.name) + reservationModel: Model, + ) { + super(reservationModel); + } +} diff --git a/apps/solowallet/src/db/solowallet.schema.ts b/apps/solowallet/src/db/solowallet.schema.ts new file mode 100644 index 0000000..32d71bd --- /dev/null +++ b/apps/solowallet/src/db/solowallet.schema.ts @@ -0,0 +1,14 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { AbstractDocument } from '@bitsacco/common'; + +@Schema({ versionKey: false }) +export class SolowalletDocument extends AbstractDocument { + @Prop({ type: String, required: true }) + userId: string; + + @Prop({ type: Number, required: true }) + quantity: number; +} + +export const SolowalletSchema = + SchemaFactory.createForClass(SolowalletDocument); diff --git a/apps/solowallet/src/main.ts b/apps/solowallet/src/main.ts new file mode 100644 index 0000000..6abcec4 --- /dev/null +++ b/apps/solowallet/src/main.ts @@ -0,0 +1,33 @@ +import { join } from 'path'; +import { Logger } from 'nestjs-pino'; +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { ReflectionService } from '@grpc/reflection'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { SolowalletModule } from './solowallet.module'; + +async function bootstrap() { + const app = await NestFactory.create(SolowalletModule); + + const configService = app.get(ConfigService); + + const solowallet_url = configService.getOrThrow( + 'SOLOWALLET_GRPC_URL', + ); + const solowallet = app.connectMicroservice({ + transport: Transport.GRPC, + options: { + package: 'solowallet', + url: solowallet_url, + protoPath: join(__dirname, '../../../proto/solowallet.proto'), + onLoadPackageDefinition: (pkg, server) => { + new ReflectionService(pkg).addToServer(server); + }, + }, + }); + + // setup pino logging + app.useLogger(app.get(Logger)); + + await app.startAllMicroservices(); +} diff --git a/apps/solowallet/src/solowallet.controller.spec.ts b/apps/solowallet/src/solowallet.controller.spec.ts new file mode 100644 index 0000000..e81aa2d --- /dev/null +++ b/apps/solowallet/src/solowallet.controller.spec.ts @@ -0,0 +1,19 @@ +import { TestingModule } from '@nestjs/testing'; +import { createTestingModuleWithValidation } from '@bitsacco/testing'; +import { SolowalletController } from './solowallet.controller'; +import { SolowalletService } from './solowallet.service'; + +describe('SolowalletController', () => { + let solowalletController: SolowalletController; + let solowalletService: SolowalletService; + + beforeEach(async () => { + const app: TestingModule = await createTestingModuleWithValidation({ + controllers: [SolowalletController], + providers: [SolowalletService], + }); + + solowalletController = app.get(SolowalletController); + solowalletService = app.get(SolowalletService); + }); +}); diff --git a/apps/solowallet/src/solowallet.controller.ts b/apps/solowallet/src/solowallet.controller.ts new file mode 100644 index 0000000..96cbfbe --- /dev/null +++ b/apps/solowallet/src/solowallet.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get } from '@nestjs/common'; +import { SolowalletService } from './solowallet.service'; +import { GrpcMethod } from '@nestjs/microservices'; +import { SolowalletServiceControllerMethods } from '@bitsacco/common'; +import { DepositFundsRequestDto } from 'libs/common/src/dto/solowallet.dto'; + +@Controller() +@SolowalletServiceControllerMethods() +export class SolowalletController { + constructor(private readonly solowalletService: SolowalletService) {} + + @GrpcMethod() + depositFunds(request: DepositFundsRequestDto): string { + return this.solowalletService.depositFunds(request); + } +} diff --git a/apps/solowallet/src/solowallet.module.ts b/apps/solowallet/src/solowallet.module.ts new file mode 100644 index 0000000..55d6f78 --- /dev/null +++ b/apps/solowallet/src/solowallet.module.ts @@ -0,0 +1,32 @@ +import * as Joi from 'joi'; +import { Module } from '@nestjs/common'; +import { DatabaseModule, LoggerModule } from '@bitsacco/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { SolowalletController } from './solowallet.controller'; +import { SolowalletService } from './solowallet.service'; +import { + SolowalletDocument, + SolowalletRepository, + SolowalletSchema, +} from './db'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + validationSchema: Joi.object({ + NODE_ENV: Joi.string().required(), + SOLOWALLET_GRPC_URL: Joi.string().required(), + DATABASE_URL: Joi.string().required(), + }), + }), + DatabaseModule, + DatabaseModule.forFeature([ + { name: SolowalletDocument.name, schema: SolowalletSchema }, + ]), + LoggerModule, + ], + controllers: [SolowalletController], + providers: [SolowalletService, ConfigService, SolowalletRepository], +}) +export class SolowalletModule {} diff --git a/apps/solowallet/src/solowallet.service.ts b/apps/solowallet/src/solowallet.service.ts new file mode 100644 index 0000000..4e9f047 --- /dev/null +++ b/apps/solowallet/src/solowallet.service.ts @@ -0,0 +1,18 @@ +import { ConfigService } from '@nestjs/config'; +import { Injectable, Logger } from '@nestjs/common'; +import { DepositFundsRequestDto } from '@bitsacco/common'; +import { SolowalletRepository } from './db'; + +@Injectable() +export class SolowalletService { + private readonly logger = new Logger(SolowalletService.name); + + constructor( + private readonly wallet: SolowalletRepository, + private readonly configService: ConfigService, + ) { + this.logger.log('SharesService created'); + } + + depositFunds({ userId, fiat_deposit }: DepositFundsRequestDto) {} +} diff --git a/apps/solowallet/test/app.e2e-spec.ts b/apps/solowallet/test/app.e2e-spec.ts new file mode 100644 index 0000000..cf6bcec --- /dev/null +++ b/apps/solowallet/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { SolowalletModule } from './../src/solowallet.module'; + +describe('SolowalletController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [SolowalletModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/solowallet/test/jest-e2e.json b/apps/solowallet/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/solowallet/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/solowallet/tsconfig.app.json b/apps/solowallet/tsconfig.app.json new file mode 100644 index 0000000..dbfa8e7 --- /dev/null +++ b/apps/solowallet/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/solowallet" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/dto/index.ts b/libs/common/src/dto/index.ts index 1dc464e..603e645 100644 --- a/libs/common/src/dto/index.ts +++ b/libs/common/src/dto/index.ts @@ -9,3 +9,4 @@ export * from './configure-nostr-relays.dto'; export * from './send-encrypted-nostr-dm.dto'; export * from './sms.dto'; export * from './shares.dto'; +export * from './solowallet.dto'; diff --git a/libs/common/src/dto/solowallet.dto.ts b/libs/common/src/dto/solowallet.dto.ts new file mode 100644 index 0000000..cd24e73 --- /dev/null +++ b/libs/common/src/dto/solowallet.dto.ts @@ -0,0 +1,18 @@ +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { CreateOnrampSwapDto } from './create-onramp-swap.dto'; +import { DepositFundsRequest } from '../types'; + +export class DepositFundsRequestDto implements DepositFundsRequest { + @IsNotEmpty() + @IsString() + @Type(() => String) + @ApiProperty() + userId: string; + + @ValidateNested() + @Type(() => CreateOnrampSwapDto) + @ApiProperty({ type: CreateOnrampSwapDto }) + fiat_deposit?: CreateOnrampSwapDto; +} diff --git a/libs/common/src/types/index.ts b/libs/common/src/types/index.ts index e84c246..9b358a6 100644 --- a/libs/common/src/types/index.ts +++ b/libs/common/src/types/index.ts @@ -3,6 +3,7 @@ export * from './proto/swap'; export * from './proto/nostr'; export * from './proto/sms'; export * from './proto/shares'; +export * from './proto/solowallet'; export * from './api'; export * from './validator'; export * from './fedimint'; diff --git a/libs/common/src/types/proto/lightning.ts b/libs/common/src/types/proto/lightning.ts new file mode 100644 index 0000000..70505c7 --- /dev/null +++ b/libs/common/src/types/proto/lightning.ts @@ -0,0 +1,12 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.2.7 +// protoc v3.21.12 +// source: lightning.proto + +/* eslint-disable */ + +export interface Bolt11 { + /** Bolt11 lightning invoice */ + invoice: string; +} diff --git a/libs/common/src/types/proto/solowallet.ts b/libs/common/src/types/proto/solowallet.ts new file mode 100644 index 0000000..150b62c --- /dev/null +++ b/libs/common/src/types/proto/solowallet.ts @@ -0,0 +1,63 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.2.7 +// protoc v3.21.12 +// source: solowallet.proto + +/* eslint-disable */ +import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; +import { Observable } from 'rxjs'; +import { OnrampSwapRequest } from './swap'; + +export interface DepositFundsRequest { + userId: string; + fiatDeposit?: OnrampSwapRequest | undefined; +} + +export interface WalletDetailsResonse { + userId: string; +} + +export interface SolowalletServiceClient { + depositFunds(request: DepositFundsRequest): Observable; +} + +export interface SolowalletServiceController { + depositFunds( + request: DepositFundsRequest, + ): + | Promise + | Observable + | WalletDetailsResonse; +} + +export function SolowalletServiceControllerMethods() { + return function (constructor: Function) { + const grpcMethods: string[] = ['depositFunds']; + for (const method of grpcMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcMethod('SolowalletService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + const grpcStreamMethods: string[] = []; + for (const method of grpcStreamMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcStreamMethod('SolowalletService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + }; +} + +export const SOLOWALLET_SERVICE_NAME = 'SolowalletService'; diff --git a/libs/common/src/types/proto/swap.ts b/libs/common/src/types/proto/swap.ts index 2713fd6..0bb029c 100644 --- a/libs/common/src/types/proto/swap.ts +++ b/libs/common/src/types/proto/swap.ts @@ -7,6 +7,7 @@ /* eslint-disable */ import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; import { Observable } from 'rxjs'; +import { Bolt11 } from './lightning'; /** Currency: Enum representing supported currencies. */ export enum Currency { @@ -136,11 +137,6 @@ export interface MobileMoney { phone: string; } -export interface Bolt11 { - /** Bolt11 lightning invoice */ - invoice: string; -} - /** FindSwapRequest: Represents a request to find a swap. */ export interface FindSwapRequest { /** Unique identifier for the swap */ diff --git a/nest-cli.json b/nest-cli.json index 02bd3bd..31b23f4 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -64,6 +64,15 @@ "compilerOptions": { "tsConfigPath": "apps/shares/tsconfig.app.json" } + }, + "solowallet": { + "type": "application", + "root": "apps/solowallet", + "entryFile": "main", + "sourceRoot": "apps/solowallet/src", + "compilerOptions": { + "tsConfigPath": "apps/solowallet/tsconfig.app.json" + } } }, "monorepo": true, diff --git a/proto/lightning.proto b/proto/lightning.proto new file mode 100644 index 0000000..30f0753 --- /dev/null +++ b/proto/lightning.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package lightning; + +message Bolt11 { + // Bolt11 lightning invoice + string invoice = 1; +} diff --git a/proto/shares.proto b/proto/shares.proto index 2793388..18e16c4 100644 --- a/proto/shares.proto +++ b/proto/shares.proto @@ -1,7 +1,5 @@ syntax = "proto3"; -import "lib.proto"; - package shares; service SharesService { diff --git a/proto/solowallet.proto b/proto/solowallet.proto new file mode 100644 index 0000000..2b0fd3b --- /dev/null +++ b/proto/solowallet.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +import "swap.proto"; + +package solowallet; + +service SolowalletService { + rpc DepositFunds(DepositFundsRequest) returns (WalletDetailsResonse){} +} + +message DepositFundsRequest { + string user_id = 1; + + optional swap.OnrampSwapRequest fiat_deposit = 2; + + // Add more otional funding sources, like direct lightning deposit +} + +message WalletDetailsResonse { + string user_id = 1; +} diff --git a/proto/swap.proto b/proto/swap.proto index d670479..c6be19d 100644 --- a/proto/swap.proto +++ b/proto/swap.proto @@ -1,5 +1,7 @@ syntax = "proto3"; +import "lightning.proto"; + package swap; // SwapService: Defines the main service for handling swap operations. @@ -106,7 +108,7 @@ message OnrampSwapSource { message OnrampSwapTarget { // Lightning protocol payout - Bolt11 payout = 2; + lightning.Bolt11 payout = 2; } message OfframpSwapRequest { @@ -138,11 +140,6 @@ message MobileMoney { string phone = 1; } -message Bolt11 { - // Bolt11 lightning invoice - string invoice = 1; -} - // FindSwapRequest: Represents a request to find a swap. message FindSwapRequest { // Unique identifier for the swap