Skip to content

Commit

Permalink
Merge pull request #107 from ckb-cell/develop
Browse files Browse the repository at this point in the history
Merge develop into main branch (20240812)
  • Loading branch information
Vibes-INS authored Aug 14, 2024
2 parents 69b3fae + 8fbe9fd commit 1b7ae41
Show file tree
Hide file tree
Showing 37 changed files with 10,707 additions and 8,210 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/backend-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ jobs:
cat .env
pnpm run test
working-directory: backend

- name: Docker Build Test
run: docker compose build
4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@ckb-lumos/bi": "^0.23.0",
"@ckb-lumos/lumos": "^0.23.0",
"@ckb-lumos/rpc": "^0.23.0",
"@nestjs/axios": "^3.0.2",
"@nestjs/bullmq": "^10.2.0",
"@nestjs/cache-manager": "^2.2.2",
"@nestjs/common": "^10.0.0",
Expand All @@ -41,6 +42,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.3.10",
"@nestjs/schedule": "^4.1.0",
"@nestjs/terminus": "^10.2.3",
"@ntegral/nestjs-sentry": "^4.0.1",
"@prisma/client": "^5.16.2",
"@rgbpp-sdk/btc": "^0.5.0",
Expand All @@ -66,6 +68,7 @@
"rpc-websockets": "^7.11.2",
"rxjs": "^7.8.1",
"ws": "^8.18.0",
"prisma": "^5.16.2",
"zod": "^3.23.8"
},
"devDependencies": {
Expand All @@ -87,7 +90,6 @@
"jest": "^29.5.0",
"lint-staged": "^15.2.7",
"prettier": "^3.0.0",
"prisma": "^5.16.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
Expand Down
2 changes: 1 addition & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import configModule from './config';
useFactory: async (configService: ConfigService<Env>) => ({
dsn: configService.get('SENTRY_DSN'),
environment: configService.get('NODE_ENV'),
enableTracing: true,
tracesSampleRate: 0.5,
profilesSampleRate: 0.5,
integrations: [nodeProfilingIntegration()],
Expand Down Expand Up @@ -66,7 +67,6 @@ import configModule from './config';
CoreModule,
ApiModule,
],
controllers: [],
providers: [
{
provide: APP_INTERCEPTOR,
Expand Down
29 changes: 29 additions & 0 deletions backend/src/core/bitcoin-api/bitcoin-api.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry';
import { BitcoinApiService } from './bitcoin-api.service';

@Injectable()
export class BitcoinApiHealthIndicator extends HealthIndicator {
constructor(
private bitcoinApiService: BitcoinApiService,
@InjectSentry() private sentryService: SentryService,
) {
super();
}

public async isHealthy(): Promise<HealthIndicatorResult> {
try {
const info = await this.bitcoinApiService.getBlockchainInfo();
const isHealthy = !!info.blocks;
const result = this.getStatus('bitcoin-api', isHealthy, { info });
if (isHealthy) {
return result;
}
throw new HealthCheckError('BitcoinApiService failed', result);
} catch (e) {
this.sentryService.instance().captureException(e);
throw new HealthCheckError('BitcoinApiService failed', e);
}
}
}
5 changes: 3 additions & 2 deletions backend/src/core/bitcoin-api/bitcoin-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BitcoinApiService } from './bitcoin-api.service';
import { Env } from 'src/env';
import { BitcoinApiHealthIndicator } from './bitcoin-api.health';

@Module({
providers: [BitcoinApiService],
exports: [BitcoinApiService],
providers: [BitcoinApiService, BitcoinApiHealthIndicator],
exports: [BitcoinApiService, BitcoinApiHealthIndicator],
})
export class BitcoinApiModule {
constructor(
Expand Down
31 changes: 31 additions & 0 deletions backend/src/core/ckb-explorer/ckb-explorer.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry';
import { CkbExplorerService } from './ckb-explorer.service';

@Injectable()
export class CkbExplorerHealthIndicator extends HealthIndicator {
constructor(
private ckbExplorerService: CkbExplorerService,
@InjectSentry() private sentryService: SentryService,
) {
super();
}

public async isHealthy(): Promise<HealthIndicatorResult> {
try {
const stats = await this.ckbExplorerService.getStatistics();
const isHealthy = !!stats.data.attributes.tip_block_number;
const result = this.getStatus('ckb-explorer', isHealthy, {
stats: stats.data.attributes,
});
if (isHealthy) {
return result;
}
throw new HealthCheckError('CkbExplorerService failed', result);
} catch (e) {
this.sentryService.instance().captureException(e);
throw new HealthCheckError('CkbExplorerService failed', e);
}
}
}
5 changes: 3 additions & 2 deletions backend/src/core/ckb-explorer/ckb-explorer.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { forwardRef, Module } from '@nestjs/common';
import { CkbExplorerService } from './ckb-explorer.service';
import { CkbRpcModule } from '../ckb-rpc/ckb-rpc.module';
import { CkbExplorerHealthIndicator } from './ckb-explorer.health';

@Module({
imports: [forwardRef(() => CkbRpcModule)],
providers: [CkbExplorerService],
exports: [CkbExplorerService],
providers: [CkbExplorerService, CkbExplorerHealthIndicator],
exports: [CkbExplorerService, CkbExplorerHealthIndicator],
})
export class CkbExplorerModule {}
51 changes: 47 additions & 4 deletions backend/src/core/ckb-rpc/ckb-rpc-websocket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,69 @@ import {
import { Cacheable } from 'src/decorators/cacheable.decorator';
import { ONE_MONTH_MS } from 'src/common/date';
import { CKB_MIN_SAFE_CONFIRMATIONS } from 'src/constants';
import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry';

class WebsocketError extends Error {}

@Injectable()
export class CkbRpcWebsocketService {
private logger = new Logger(CkbRpcWebsocketService.name);
private websocket: RpcWebsocketsClient;
private websocketReady: Promise<void>;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectInterval = 5000;

constructor(
private configService: ConfigService<Env>,
@InjectSentry() private sentryService: SentryService,
) {
this.initializeWebSocket();
}

constructor(private configService: ConfigService<Env>) {
private initializeWebSocket() {
this.websocket = new RpcWebsocketsClient(this.configService.get('CKB_RPC_WEBSOCKET_URL'));

this.websocketReady = new Promise((resolve) => {
this.websocket.on('open', () => {
this.websocket.on('error', (error) => {
this.logger.error(error.message);
});
this.logger.log('WebSocket connection established');
this.reconnectAttempts = 0;
resolve();
});

this.websocket.on('error', (error) => {
this.logger.error(error.message);
const webSocketError = new WebsocketError(error.message);
webSocketError.stack = error.stack;
this.websocketReady = Promise.reject(webSocketError);
this.sentryService.instance().captureException(webSocketError);
});

this.websocket.on('close', () => {
const error = new WebsocketError('WebSocket connection closed');
this.logger.warn(error.message);
this.websocketReady = Promise.reject(error);
this.handleReconnection();
});
});
}

private handleReconnection() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.logger.log(
`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
);
setTimeout(() => this.initializeWebSocket(), this.reconnectInterval);
} else {
const error = new WebsocketError('Max reconnection attempts reached');
this.logger.error(error.message);
this.sentryService
.instance()
.captureException(error);
}
}

private async isSafeConfirmations(blockNumber: string): Promise<boolean> {
const tipBlockNumber = await this.getTipBlockNumber();
return BI.from(blockNumber).lt(BI.from(tipBlockNumber).sub(CKB_MIN_SAFE_CONFIRMATIONS));
Expand Down
57 changes: 57 additions & 0 deletions backend/src/core/ckb-rpc/ckb-rpc.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { CkbRpcWebsocketService } from './ckb-rpc-websocket.service';
import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry';

export enum CkbRpcHealthIndicatorKey {
Websocket = 'ckb-rpc-websocket',
}

@Injectable()
export class CkbRpcHealthIndicator extends HealthIndicator {
constructor(
private ckbRpcWebsocketService: CkbRpcWebsocketService,
@InjectSentry() private sentryService: SentryService,
) {
super();
}

public async isHealthy(key: CkbRpcHealthIndicatorKey): Promise<HealthIndicatorResult> {
try {
let isHealthy = false;
let result: HealthIndicatorResult = {};

switch (key) {
case CkbRpcHealthIndicatorKey.Websocket:
const { isHealthy: websocketIsHealthy, result: websocketResult } =
await this.isWebsocketHealthy();
isHealthy = websocketIsHealthy;
result = websocketResult;
break;
default:
throw new HealthCheckError(`Unknown health indicator key`, key);
}

if (isHealthy) {
return result;
}
throw new HealthCheckError('CkbRpcWebsocketService failed', result);
} catch (e) {
this.sentryService.instance().captureException(e);
throw new HealthCheckError('CkbRpcWebsocketService failed', e);
}
}

private async isWebsocketHealthy(): Promise<{
isHealthy: boolean;
result: HealthIndicatorResult;
}> {
const tipBlockNumber = await this.ckbRpcWebsocketService.getTipBlockNumber();
const isHealthy = !!tipBlockNumber;
const result = this.getStatus('ckb-rpc.websocket', isHealthy, { tipBlockNumber });
return {
isHealthy,
result,
};
}
}
5 changes: 3 additions & 2 deletions backend/src/core/ckb-rpc/ckb-rpc.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { CkbRpcHealthIndicator } from './ckb-rpc.health';
import { CkbRpcWebsocketService } from './ckb-rpc-websocket.service';

@Module({
providers: [CkbRpcWebsocketService],
exports: [CkbRpcWebsocketService],
providers: [CkbRpcWebsocketService, CkbRpcHealthIndicator],
exports: [CkbRpcWebsocketService, CkbRpcHealthIndicator],
})
export class CkbRpcModule {}
2 changes: 2 additions & 0 deletions backend/src/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { BitcoinApiModule } from './bitcoin-api/bitcoin-api.module';
import { CkbRpcModule } from './ckb-rpc/ckb-rpc.module';
import { CkbRpcWebsocketService } from './ckb-rpc/ckb-rpc-websocket.service';
import { BitcoinApiService } from './bitcoin-api/bitcoin-api.service';
import { HealthModule } from './health/health.module';

@Module({
imports: [
// DatabaseModule,
CkbExplorerModule,
CkbRpcModule,
BitcoinApiModule,
HealthModule,
],
providers: [CkbExplorerService, CkbRpcWebsocketService, BitcoinApiService],
exports: [CkbExplorerService, CkbRpcWebsocketService, BitcoinApiService],
Expand Down
27 changes: 27 additions & 0 deletions backend/src/core/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HealthCheck, HttpHealthIndicator } from '@nestjs/terminus';
import { CkbRpcHealthIndicator, CkbRpcHealthIndicatorKey } from '../ckb-rpc/ckb-rpc.health';
import { BitcoinApiHealthIndicator } from '../bitcoin-api/bitcoin-api.health';
import { CkbExplorerHealthIndicator } from '../ckb-explorer/ckb-explorer.health';

@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private bitcoinApiHealthIndicator: BitcoinApiHealthIndicator,
private ckbRpcHealthIndicator: CkbRpcHealthIndicator,
private ckbExplorerHealthIndicator: CkbExplorerHealthIndicator,
) {}

@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.http.pingCheck('graphql', 'http://localhost:3000/graphql'),
() => this.bitcoinApiHealthIndicator.isHealthy(),
() => this.ckbRpcHealthIndicator.isHealthy(CkbRpcHealthIndicatorKey.Websocket),
() => this.ckbExplorerHealthIndicator.isHealthy(),
]);
}
}
14 changes: 14 additions & 0 deletions backend/src/core/health/health.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';
import { CkbRpcModule } from '../ckb-rpc/ckb-rpc.module';
import { CkbExplorerModule } from '../ckb-explorer/ckb-explorer.module';
import { BitcoinApiModule } from '../bitcoin-api/bitcoin-api.module';
import { HttpModule } from '@nestjs/axios';

@Module({
imports: [TerminusModule, HttpModule, BitcoinApiModule, CkbRpcModule, CkbExplorerModule],
providers: [HealthController],
controllers: [HealthController],
})
export class HealthModule {}
8 changes: 3 additions & 5 deletions backend/src/decorators/cacheable.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ const logger = new Logger('Cacheable');
export function Cacheable(options: CustomCacheableRegisterOptions): MethodDecorator {
const injectCacheService = Inject(CACHE_MANAGER);

return function(target, propertyKey, descriptor) {
return function (target, propertyKey, descriptor) {
// eslint-disable-next-line @typescript-eslint/ban-types
const originalMethod = descriptor.value as unknown as Function;

injectCacheService(target, '__cacheManager');
return {
...descriptor,
value: async function(...args: any[]) {
value: async function (...args: any[]) {
const cacheManager = this.__cacheManager as Cache;
if (!cacheManager) return originalMethod.apply(this, args);
const composeOptions: Parameters<typeof generateComposedKey>[0] = {
Expand All @@ -48,9 +48,7 @@ export function Cacheable(options: CustomCacheableRegisterOptions): MethodDecora
);

// Remove the cache if shouldCache returns false
const shouldCache = options.shouldCache
? await options.shouldCache(returnVal, this)
: true;
const shouldCache = options.shouldCache ? await options.shouldCache(returnVal, this) : true;
if (!shouldCache) {
logger.debug(`Removing cache for key: ${key}`);
await cacheManager.del(key);
Expand Down
3 changes: 1 addition & 2 deletions backend/src/modules/bitcoin/address/address.dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ export interface BitcoinAddressTransactionsLoaderParams {

@Injectable()
export class BitcoinAddressTransactionsLoader
implements
NestDataLoader<BitcoinAddressTransactionsLoaderParams, BitcoinTransaction[] | null>
implements NestDataLoader<BitcoinAddressTransactionsLoaderParams, BitcoinTransaction[] | null>
{
private logger = new Logger(BitcoinAddressTransactionsLoader.name);

Expand Down
4 changes: 1 addition & 3 deletions backend/src/modules/bitcoin/block/block.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ export class BitcoinBlockResolver {
}

@ResolveField(() => Float, { nullable: true })
public async confirmations(
@Parent() block: BitcoinBlock,
): Promise<number | null> {
public async confirmations(@Parent() block: BitcoinBlock): Promise<number | null> {
const info = await this.bitcoinApiService.getBlockchainInfo();
return info.blocks - block.height;
}
Expand Down
Loading

1 comment on commit 1b7ae41

@vercel
Copy link

@vercel vercel bot commented on 1b7ae41 Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.