Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nostr service #25

Merged
merged 3 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/publish-nostr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: publish-nostr

on:
push:
paths:
- apps/nostr/**
- libs/**
- package.json
- bun.lockb
- .github/workflows/publish-nostr.yml
workflow_dispatch:

jobs:
testing:
uses: ./.github/workflows/wait-for-tests.yml
with:
test-job-name: test

docker:
needs: testing
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
bitsacco/nostr
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha

- name: Login to Docker Hub
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: docker/login-action@v2
with:
username: okjodom
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Build and push nostr
uses: docker/build-push-action@v4
with:
file: apps/nostr/Dockerfile
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Checkout repository content
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: actions/checkout@v4

# This workflow requires the repository content to be locally available to read the README
- name: Update the Docker Hub description
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: peter-evans/dockerhub-description@v3
with:
username: okjodom
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
repository: bitsacco/nostr
readme-filepath: ./apps/nostr/README.md
19 changes: 17 additions & 2 deletions apps/api/src/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { ClientsModule, Transport } from '@nestjs/microservices';
import {
EVENTS_SERVICE_BUS,
LoggerModule,
NOSTR_PACKAGE_NAME,
NOSTR_SERVICE_NAME,
SWAP_PACKAGE_NAME,
SWAP_SERVICE_NAME,
} from '@bitsacco/common';
import { SwapController, SwapService } from './swap';
import { NostrController, NostrService } from './nostr';

@Module({
imports: [
Expand Down Expand Up @@ -37,6 +40,18 @@ import { SwapController, SwapService } from './swap';
}),
inject: [ConfigService],
},
{
name: NOSTR_SERVICE_NAME,
useFactory: (configService: ConfigService) => ({
transport: Transport.GRPC,
options: {
package: NOSTR_PACKAGE_NAME,
protoPath: join(__dirname, '../../../proto/nostr.proto'),
url: configService.getOrThrow<string>('NOSTR_GRPC_URL'),
},
}),
inject: [ConfigService],
},
{
name: EVENTS_SERVICE_BUS,
useFactory: (configService: ConfigService) => ({
Expand All @@ -50,7 +65,7 @@ import { SwapController, SwapService } from './swap';
},
]),
],
controllers: [SwapController],
providers: [SwapService],
controllers: [SwapController, NostrController],
providers: [SwapService, NostrService],
})
export class ApiModule {}
2 changes: 2 additions & 0 deletions apps/api/src/nostr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './nostr.controller';
export * from './nostr.service';
33 changes: 33 additions & 0 deletions apps/api/src/nostr/nostr.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TestingModule } from '@nestjs/testing';
import { createTestingModuleWithValidation } from '@bitsacco/testing';

import { NostrController } from './nostr.controller';
import { NostrService } from './nostr.service';

describe('NostrController', () => {
let controller: NostrController;
let nostrService: NostrService;

beforeEach(async () => {
const module: TestingModule = await createTestingModuleWithValidation({
controllers: [NostrController],
providers: [
{
provide: NostrService,
useValue: {
sendEncryptedNostrDm: jest.fn(),
configureNostrRelays: jest.fn(),
},
},
],
});

controller = module.get<NostrController>(NostrController);
nostrService = module.get<NostrService>(NostrService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
expect(nostrService).toBeDefined();
});
});
34 changes: 34 additions & 0 deletions apps/api/src/nostr/nostr.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApiOperation, ApiBody } from '@nestjs/swagger';
import { Body, Controller, Logger, Post } from '@nestjs/common';
import {
ConfigureNostrRelaysDto,
SendEncryptedNostrDmDto,
} from '@bitsacco/common';
import { NostrService } from './nostr.service';

@Controller('nostr')
export class NostrController {
private readonly logger = new Logger(NostrController.name);

constructor(private readonly nostrService: NostrService) {
this.logger.log('NostrController initialized');
}

@Post('relays')
@ApiOperation({ summary: 'Configure nostr relays' })
@ApiBody({
type: ConfigureNostrRelaysDto,
})
configureNostrRelays(@Body() req: ConfigureNostrRelaysDto) {
return this.nostrService.configureNostrRelays(req);
}

@Post('dm')
@ApiOperation({ summary: 'Send encrypted nostr dm' })
@ApiBody({
type: SendEncryptedNostrDmDto,
})
send(@Body() req: SendEncryptedNostrDmDto) {
return this.nostrService.sendEncryptedNostrDm(req);
}
}
35 changes: 35 additions & 0 deletions apps/api/src/nostr/nostr.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { TestingModule } from '@nestjs/testing';
import { ClientGrpc } from '@nestjs/microservices';
import { NostrServiceClient } from '@bitsacco/common';
import { NostrService } from './nostr.service';
import { createTestingModuleWithValidation } from '@bitsacco/testing';

describe('NostrService', () => {
let service: NostrService;
let serviceGenerator: ClientGrpc;
let mockNostrServiceClient: Partial<NostrServiceClient>;

beforeEach(async () => {
serviceGenerator = {
getService: jest.fn().mockReturnValue(mockNostrServiceClient),
getClientByServiceName: jest.fn().mockReturnValue(mockNostrServiceClient),
};

const module: TestingModule = await createTestingModuleWithValidation({
providers: [
{
provide: NostrService,
useFactory: () => {
return new NostrService(serviceGenerator);
},
},
],
});

service = module.get<NostrService>(NostrService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
27 changes: 27 additions & 0 deletions apps/api/src/nostr/nostr.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
ConfigureNostrRelaysDto,
NOSTR_SERVICE_NAME,
NostrServiceClient,
SendEncryptedNostrDmDto,
} from '@bitsacco/common';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { type ClientGrpc } from '@nestjs/microservices';

@Injectable()
export class NostrService implements OnModuleInit {
private client: NostrServiceClient;

constructor(@Inject(NOSTR_SERVICE_NAME) private readonly grpc: ClientGrpc) {}

onModuleInit() {
this.client = this.grpc.getService<NostrServiceClient>(NOSTR_SERVICE_NAME);
}

sendEncryptedNostrDm(req: SendEncryptedNostrDmDto) {
return this.client.sendEncryptedNostrDirectMessage(req);
}

configureNostrRelays(req: ConfigureNostrRelaysDto) {
return this.client.configureTrustedNostrRelays(req);
}
}
2 changes: 1 addition & 1 deletion apps/api/src/swap/swap.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
SupportedCurrencies,
} from '@bitsacco/common';
import { createTestingModuleWithValidation } from '@bitsacco/testing';
import { ClientProxy } from '@nestjs/microservices';

import { SwapController } from './swap.controller';
import { SwapService } from './swap.service';
import { ClientProxy } from '@nestjs/microservices';

describe('SwapController', () => {
let controller: SwapController;
Expand Down
32 changes: 32 additions & 0 deletions apps/nostr/Dockerfile
Original file line number Diff line number Diff line change
@@ -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/nostr apps/nostr
COPY libs libs
COPY proto proto

RUN bun install
RUN bun build:nostr

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/nostr/main.js"]
17 changes: 17 additions & 0 deletions apps/nostr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# apps/nostr

This app is a gRPC microservice for simple nostr operations with Bitsacco

## Dev

Run `bun dev nostr` 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 [nostr.proto](https://github.com/bitsacco/os/blob/main/proto/nostr.proto)
- With the microservice running, see supported gRPC methods with type reflection at http://localhost:4050

## Architecture

See [architecture.md](https://github.com/bitsacco/os/blob/main/docs/architecture.md)
33 changes: 33 additions & 0 deletions apps/nostr/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { join } from 'path';
import { Logger } from 'nestjs-pino';
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { ReflectionService } from '@grpc/reflection';
import { NostrModule } from './nostr.module';

async function bootstrap() {
const app = await NestFactory.create(NostrModule);

const configService = app.get(ConfigService);

const nostr_url = configService.getOrThrow<string>('NOSTR_GRPC_URL');
const nostr = app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC,
options: {
package: 'nostr',
url: nostr_url,
protoPath: join(__dirname, '../../../proto/nostr.proto'),
onLoadPackageDefinition: (pkg, server) => {
new ReflectionService(pkg).addToServer(server);
},
},
});

// setup pino logging
app.useLogger(app.get(Logger));

await app.startAllMicroservices();
}

bootstrap();
28 changes: 28 additions & 0 deletions apps/nostr/src/nostr.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TestingModule } from '@nestjs/testing';
import { createTestingModuleWithValidation } from '@bitsacco/testing';
import { NostrController } from './nostr.controller';
import { NostrService } from './nostr.service';

describe('NostrController', () => {
let nostrController: NostrController;
let nostrService: NostrService;

beforeEach(async () => {
const app: TestingModule = await createTestingModuleWithValidation({
imports: [],
controllers: [NostrController],
providers: [
{
provide: NostrService,
useValue: {
sendEncryptedDirectMessage: jest.fn(),
configureNostrRelays: jest.fn(),
},
},
],
});

nostrController = app.get<NostrController>(NostrController);
nostrService = app.get<NostrService>(NostrService);
});
});
24 changes: 24 additions & 0 deletions apps/nostr/src/nostr.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import {
ConfigureNostrRelaysDto,
NostrServiceControllerMethods,
SendEncryptedNostrDmDto,
} from '@bitsacco/common';
import { NostrService } from './nostr.service';

@Controller()
@NostrServiceControllerMethods()
export class NostrController {
constructor(private readonly nostrService: NostrService) {}

@GrpcMethod()
configureTrustedNostrRelays(request: ConfigureNostrRelaysDto) {
return this.nostrService.configureNostrRelays(request);
}

@GrpcMethod()
sendEncryptedNostrDirectMessage(request: SendEncryptedNostrDmDto) {
return this.nostrService.sendEncryptedDirectMessage(request);
}
}
Loading
Loading