From 900237a38feead307349b840c04b52c512d5bd6b Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Sun, 21 Apr 2024 18:00:50 +0300 Subject: [PATCH] fix --- .env.production | 2 + .github/workflows/main.yaml | 2 +- Dockerfile | 2 +- package.json | 13 +- scripts/twitter.ts | 13 - src/app.module.ts | 4 +- src/caching.health.ts | 113 ++++--- src/dialect-sdk.ts | 10 +- src/health.controller.ts | 4 +- src/main.ts | 25 +- src/new-proposals-monitoring.service.ts | 192 ++++++------ src/proposal-state-monitoring.service.ts | 162 +++++----- src/realms-cache.ts | 262 +++++++++++++++++ src/realms-repository.ts | 297 ------------------- src/realms-sdk.ts | 174 +++++++++++ src/realms.service.ts | 62 ++-- src/utils/collection-utils.ts | 6 - src/utils/entropy-utils.ts | 19 -- src/utils/error-handling-utils.ts | 27 -- tsconfig.json | 1 + yarn.lock | 357 ++++++++++++++++++++++- 21 files changed, 1120 insertions(+), 627 deletions(-) create mode 100644 .env.production delete mode 100644 scripts/twitter.ts create mode 100644 src/realms-cache.ts delete mode 100644 src/realms-repository.ts create mode 100644 src/realms-sdk.ts delete mode 100644 src/utils/collection-utils.ts delete mode 100644 src/utils/entropy-utils.ts delete mode 100644 src/utils/error-handling-utils.ts diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..8c15c71 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +ENVIRONMENT=production +DIALECT_SDK_ENVIRONMENT=production diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6a3b38f..fd47837 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -63,7 +63,7 @@ jobs: image_tag: ${{ steps.get-image-tag.outputs.image_tag }} cd_production: - if: ${{ contains(github.ref, 'heads/master') }} +# if: ${{ contains(github.ref, 'heads/master') }} needs: - docker-image uses: ./.github/workflows/cd.yaml diff --git a/Dockerfile b/Dockerfile index 64e76b5..4908117 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine +FROM node:20-alpine3.18 WORKDIR /app diff --git a/package.json b/package.json index 9ef3e6b..7077216 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "start:dev": "dotenv -e .env.dev -e .env.service -- nest start --watch", "start:dev:debug": "dotenv -e .env.dev -e .env.service -- nest start --debug --watch", "start:local-dev": "dotenv -e .env.local-dev -e .env.service -- nest start --watch", + "start:production": "dotenv -e .env.production -e .env.service -- nest start --watch", "start:local-dev:debug": "dotenv -e .env.local-dev -e .env.service -- nest start --debug --watch", "client:start:dev": "dotenv -e .env.dev -e .env.client -- ts-node test/client.ts", "client:start:local-dev": "dotenv -e .env.local-dev -e .env.client -- ts-node test/client.ts" @@ -24,14 +25,16 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/event-emitter": "^1.3.1", "@nestjs/platform-express": "^9.0.0", "@nestjs/schedule": "^2.1.0", "@nestjs/terminus": "^9.1.0", - "@nestjs/event-emitter": "^1.3.1", - "@solana/spl-governance": "^0.0.34", - "@solana/spl-token": "0.1.8", + "@solana/spl-governance": "^0.3.28", + "@solana/spl-token": "^0.4.3", "@solana/spl-token-registry": "^0.2.3775", - "bn.js": "^5.2.1", + "bn.js": "^5.1.3", + "borsh": "^0.3.1", + "bs58": "^4.0.1", "lodash": "^4.17.21", "luxon": "^3.0.1", "nestjs-pino": "^2.6.0", @@ -47,6 +50,7 @@ "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", "@types/bn.js": "^5.1.0", + "@types/bs58": "^4.0.4", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", "@types/jest": "28.1.4", @@ -56,6 +60,7 @@ "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "dotenv-cli": "^7.4.1", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", diff --git a/scripts/twitter.ts b/scripts/twitter.ts deleted file mode 100644 index 63fcdd3..0000000 --- a/scripts/twitter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TwitterNotificationsSink } from '../src/twitter-notifications-sink'; - -async function main() { - const twitterNotificationSink = new TwitterNotificationsSink(); - await twitterNotificationSink.push( - { - message: 'Test', - }, - [], - ); -} - -main().then((it) => console.log(it)); diff --git a/src/app.module.ts b/src/app.module.ts index 49088e4..2966a5e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,7 +11,7 @@ import { DialectSdk } from './dialect-sdk'; import { ConfigModule } from '@nestjs/config'; import { RealmsRestService } from './realms-rest-service'; import { RealmsService } from './realms.service'; -import { RealmsRepository } from './realms-repository'; +import { RealmsCache } from './realms-cache'; import { ScheduleModule } from '@nestjs/schedule'; import { NewProposalsMonitoringService } from './new-proposals-monitoring.service'; import { ProposalStateChangeMonitoringService } from './proposal-state-monitoring.service'; @@ -45,9 +45,9 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; ], controllers: [HealthController], providers: [ + RealmsCache, CachingHealth, RealmsRestService, - RealmsRepository, RealmsService, NewProposalsMonitoringService, ProposalStateChangeMonitoringService, diff --git a/src/caching.health.ts b/src/caching.health.ts index a7b88e5..0db2bcb 100644 --- a/src/caching.health.ts +++ b/src/caching.health.ts @@ -4,61 +4,80 @@ import { HealthIndicatorResult, } from '@nestjs/terminus'; import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; - -export interface CachingEvent { - type: CachingEventType; -} - -export interface CachingStartedEvent extends CachingEvent { - type: CachingEventType.Started; - timeStarted: number; -} - -export interface CachingFinishedEvent extends CachingEvent { - type: CachingEventType.Finished; -} - -export enum CachingEventType { - Started = 'caching.started', - Finished = 'caching.finished', -} +import { RealmsCache } from './realms-cache'; @Injectable() export class CachingHealth extends HealthIndicator { - private static readonly MAX_CACHING_EXECUTION_TIME_MILLIS = process.env - .MAX_CACHING_EXECUTION_TIME_MILLIS - ? parseInt(process.env.MAX_CACHING_EXECUTION_TIME_MILLIS, 10) - : 600000; + private static MAX_STATIC_ACCOUNT_CACHE_AGE_MILLIS = + process.env.MAX_CACHE_AGE_MILLIS ?? 3 * 60 * 60 * 1000; + + private static MAX_DYNAMIC_ACCOUNT_CACHE_AGE_MILLIS = + process.env.MAX_CACHE_AGE_MILLIS ?? 30 * 60 * 1000; + private readonly logger = new Logger(CachingHealth.name); - private lastStartedCaching: number; - private cachingInProgress = false; + + constructor(private readonly realmsCache: RealmsCache) { + super(); + } public isHealthy(): HealthIndicatorResult { - const isHealthy = this.cachingInProgress - ? Date.now() - this.lastStartedCaching < - CachingHealth.MAX_CACHING_EXECUTION_TIME_MILLIS - : true; - if (isHealthy) { - return this.getStatus('caching', isHealthy); + if (this.realmsCache.initializationError) { + this.logger.error( + 'Caching health check failed. Service needs to be restarted, because initialization failed', + ); + throw new HealthCheckError( + 'Caching failed', + this.getStatus('caching', false), + ); } - this.logger.error( - 'Caching health check failed. Service needs to be restarted', - ); - throw new HealthCheckError( - 'Caching failed', - this.getStatus('caching', isHealthy), - ); - } - @OnEvent(CachingEventType.Started) - onCachingStarted({ timeStarted }: CachingStartedEvent) { - this.lastStartedCaching = timeStarted; - this.cachingInProgress = true; - } + if (!this.realmsCache.isInitialized) { + return this.getStatus('caching', true); + } + + if ( + !this.realmsCache.lastDynamicAccountCachingSuccessFinishedAt || + !this.realmsCache.lastStaticAccountCachingSuccessFinishedAt + ) { + this.logger.error( + `Some of the cache ages are not initialized, this should not happen. + Static cache age: ${this.realmsCache.lastStaticAccountCachingSuccessFinishedAt}, dynamic cache age: ${this.realmsCache.lastDynamicAccountCachingSuccessFinishedAt} + Service needs to be restarted`, + ); + throw new HealthCheckError( + 'Caching failed', + this.getStatus('caching', false), + ); + } + + const staticCacheAge = + Date.now() - + this.realmsCache.lastStaticAccountCachingSuccessFinishedAt.getTime(); + + if (staticCacheAge > CachingHealth.MAX_STATIC_ACCOUNT_CACHE_AGE_MILLIS) { + this.logger.error( + `Static cache age is too old: ${staticCacheAge}, service needs to be restarted`, + ); + throw new HealthCheckError( + 'Caching failed', + this.getStatus('caching', false), + ); + } + + const dynamicCacheAge = + Date.now() - + this.realmsCache.lastDynamicAccountCachingSuccessFinishedAt.getTime(); + + if (dynamicCacheAge > CachingHealth.MAX_DYNAMIC_ACCOUNT_CACHE_AGE_MILLIS) { + this.logger.error( + `Dynamic cache age is too old: ${dynamicCacheAge}, service needs to be restarted`, + ); + throw new HealthCheckError( + 'Caching failed', + this.getStatus('caching', false), + ); + } - @OnEvent(CachingEventType.Finished) - onCachingFinished() { - this.cachingInProgress = false; + return this.getStatus('caching', true); } } diff --git a/src/dialect-sdk.ts b/src/dialect-sdk.ts index 7b63697..292b306 100644 --- a/src/dialect-sdk.ts +++ b/src/dialect-sdk.ts @@ -8,9 +8,9 @@ import { } from '@dialectlabs/sdk'; export abstract class DialectSdk implements IDialectSdk { - readonly dapps: Dapps; - readonly info: DialectSdkInfo; - readonly threads: Messaging; - readonly wallet: Wallets; - readonly identity: IdentityResolver; + readonly dapps!: Dapps; + readonly info!: DialectSdkInfo; + readonly threads!: Messaging; + readonly wallet!: Wallets; + readonly identity!: IdentityResolver; } diff --git a/src/health.controller.ts b/src/health.controller.ts index 60b8188..30f9de8 100644 --- a/src/health.controller.ts +++ b/src/health.controller.ts @@ -10,12 +10,12 @@ import { CachingHealth } from './caching.health'; export class HealthController { constructor( private health: HealthCheckService, - private readonly dataIngestionHealth: CachingHealth, + private readonly cachingHealth: CachingHealth, ) {} @Get() @HealthCheck() check() { - return this.health.check([() => this.dataIngestionHealth.isHealthy()]); + return this.health.check([() => this.cachingHealth.isHealthy()]); } } diff --git a/src/main.ts b/src/main.ts index d97c716..b1b7876 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,17 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger } from 'nestjs-pino'; -import { VersioningType } from '@nestjs/common'; +import { INestApplication, VersioningType } from '@nestjs/common'; +import { Server } from 'http'; -export const NOTIF_TYPE_ID_PROPOSALS = '04827917-dde4-48c7-bf1b-780b77895e97' +export const NOTIF_TYPE_ID_PROPOSALS = '04827917-dde4-48c7-bf1b-780b77895e97'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['log', 'warn', 'error'], }); + configureHttpServer(app); + configureUnhandledErrorsHandling(); app.setGlobalPrefix('api'); app.enableVersioning({ type: VersioningType.URI, @@ -24,4 +27,22 @@ async function bootstrap() { await app.listen(process.env.PORT ?? 0); } +function configureHttpServer(app: INestApplication) { + // https://shuheikagawa.com/blog/2019/04/25/keep-alive-timeout/ + // ALB has default timeout of 60 seconds + const httpAdapter = app.getHttpAdapter(); + const server: Server = httpAdapter.getHttpServer(); + server.keepAliveTimeout = 61 * 1000; + server.headersTimeout = 65 * 1000; +} + +function configureUnhandledErrorsHandling() { + process.on('unhandledRejection', (error) => { + console.error(error); + }); + process.on('uncaughtException', (error) => { + console.error(error); + }); +} + bootstrap(); diff --git a/src/new-proposals-monitoring.service.ts b/src/new-proposals-monitoring.service.ts index d7b9a9e..1de6414 100644 --- a/src/new-proposals-monitoring.service.ts +++ b/src/new-proposals-monitoring.service.ts @@ -1,10 +1,11 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { - TwitterNotification, - TwitterNotificationsSink, -} from './twitter-notifications-sink'; +import { Injectable, Logger } from '@nestjs/common'; -import { Monitors, NotificationSink, Pipelines } from '@dialectlabs/monitor'; +import { + DialectSdkNotification, + Monitor, + Monitors, + Pipelines, +} from '@dialectlabs/monitor'; import { Duration } from 'luxon'; import { DialectSdk } from './dialect-sdk'; @@ -14,101 +15,112 @@ import { RealmData, RealmsService, } from './realms.service'; +import { ConsoleNotificationSink } from './console-notification-sink'; +import { OnEvent } from '@nestjs/event-emitter'; +import { CachingEventType } from './realms-cache'; @Injectable() -export class NewProposalsMonitoringService implements OnModuleInit { - private readonly twitterNotificationsSink: NotificationSink = - new TwitterNotificationsSink(); +export class NewProposalsMonitoringService { + // private readonly twitterNotificationsSink: NotificationSink = + // new TwitterNotificationsSink(); private readonly logger = new Logger(NewProposalsMonitoringService.name); + private readonly monitor: Monitor = this.createMonitor(); + constructor( private readonly sdk: DialectSdk, private readonly realmsService: RealmsService, ) {} - onModuleInit() { - const monitor = Monitors.builder({ - sdk: this.sdk, - }) - .defineDataSource() - .poll( - async (subscribers) => this.realmsService.getRealmsData(subscribers), - Duration.fromObject({ minutes: 7 }), - ) - .transform({ - keys: ['proposals'], - pipelines: [ - Pipelines.added((p1, p2) => - p1.proposal.pubkey.equals(p2.proposal.pubkey), - ), - ], - }) - .notify({ - type: { - id: NOTIF_TYPE_ID_PROPOSALS, - }, + @OnEvent(CachingEventType.InitialCachingFinished) + onInitialCachingFinished() { + this.monitor.start().catch(this.logger.error); + } + + createMonitor() { + return ( + Monitors.builder({ + sdk: this.sdk, }) - // .custom( - // ({ value, context }) => { - // const realmName: string = context.origin.realm.account.name; - // const realmId: string = context.origin.realm.pubkey.toBase58(); - // const message: string = this.constructMessage( - // realmName, - // realmId, - // value, - // ); - // this.logger.log( - // `Sending message for ${context.origin.subscribers.length} subscribers of realm ${realmId} : ${message}`, - // ); - // return { - // title: `New proposal for ${realmName}`, - // message, - // }; - // }, - // new ConsoleNotificationSink(), - // { - // dispatch: 'multicast', - // to: (ctx) => ctx.origin.subscribers, - // }, - // ) - .dialectSdk( - ({ value, context }) => { - const realmName: string = context.origin.realm.account.name; - const realmId: string = context.origin.realm.pubkey.toBase58(); - const message: string = this.constructMessage( - realmName, - realmId, - value, - ); - this.logger.log( - `Sending message for ${context.origin.subscribers.length} subscribers of realm ${realmId} : ${message}`, - ); - return { - title: `New proposal for ${realmName}`, - message, - }; - }, - { dispatch: 'multicast', to: ({ origin }) => origin.subscribers }, - ) - .custom( - ({ value, context }) => { - const realmName: string = context.origin.realm.account.name; - const realmId: string = context.origin.realm.pubkey.toBase58(); - const message = this.constructMessage(realmName, realmId, value); - this.logger.log(`Sending tweet for ${realmName} : ${message}`); - return { - message, - }; - }, - this.twitterNotificationsSink, - { - dispatch: 'broadcast', - }, - ) - .and() - .build(); - monitor.start(); + .defineDataSource() + .poll( + async (subscribers) => this.realmsService.getRealmsData(subscribers), + Duration.fromObject({ minutes: 1 }), + ) + .transform({ + keys: ['proposals'], + pipelines: [ + Pipelines.added((p1, p2) => + p1.proposal.pubkey.equals(p2.proposal.pubkey), + ), + ], + }) + .notify({ + type: { + id: NOTIF_TYPE_ID_PROPOSALS, + }, + }) + .custom( + ({ value, context }) => { + const realmName: string = context.origin.realm.account.name; + const realmId: string = context.origin.realm.pubkey.toBase58(); + const message: string = this.constructMessage( + realmName, + realmId, + value, + ); + this.logger.log( + `Sending message for ${context.origin.subscribers.length} subscribers of realm ${realmId} : ${message}`, + ); + return { + title: `New proposal for ${realmName}`, + message, + }; + }, + new ConsoleNotificationSink(), + { + dispatch: 'multicast', + to: (ctx) => ctx.origin.subscribers, + }, + ) + // .dialectSdk( + // ({ value, context }) => { + // const realmName: string = context.origin.realm.account.name; + // const realmId: string = context.origin.realm.pubkey.toBase58(); + // const message: string = this.constructMessage( + // realmName, + // realmId, + // value, + // ); + // this.logger.log( + // `Sending message for ${context.origin.subscribers.length} subscribers of realm ${realmId} : ${message}`, + // ); + // return { + // title: `New proposal for ${realmName}`, + // message, + // }; + // }, + // { dispatch: 'multicast', to: ({ origin }) => origin.subscribers }, + // ) + // .custom( + // ({ value, context }) => { + // const realmName: string = context.origin.realm.account.name; + // const realmId: string = context.origin.realm.pubkey.toBase58(); + // const message = this.constructMessage(realmName, realmId, value); + // this.logger.log(`Sending tweet for ${realmName} : ${message}`); + // return { + // message, + // }; + // }, + // this.twitterNotificationsSink, + // { + // dispatch: 'broadcast', + // }, + // ) + .and() + .build() + ); } private constructMessage( diff --git a/src/proposal-state-monitoring.service.ts b/src/proposal-state-monitoring.service.ts index 53e9053..3571dd4 100644 --- a/src/proposal-state-monitoring.service.ts +++ b/src/proposal-state-monitoring.service.ts @@ -1,8 +1,9 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Change, DialectSdkNotification, + Monitor, Monitors, Pipelines, } from '@dialectlabs/monitor'; @@ -17,7 +18,9 @@ import { ProposalState, Realm, } from '@solana/spl-governance'; -import { fmtTokenAmount, RealmMints } from './realms-repository'; +import { CachingEventType, fmtTokenAmount, RealmMints } from './realms-cache'; +import { ConsoleNotificationSink } from './console-notification-sink'; +import { OnEvent } from '@nestjs/event-emitter'; interface ProposalVotingStats { yesCount: number; @@ -27,7 +30,9 @@ interface ProposalVotingStats { } @Injectable() -export class ProposalStateChangeMonitoringService implements OnModuleInit { +export class ProposalStateChangeMonitoringService { + private readonly monitor: Monitor = this.createMonitor(); + constructor( private readonly sdk: DialectSdk, private readonly realmsService: RealmsService, @@ -37,80 +42,91 @@ export class ProposalStateChangeMonitoringService implements OnModuleInit { ProposalStateChangeMonitoringService.name, ); - onModuleInit() { - const monitor = Monitors.builder({ - sdk: this.sdk, - }) - .defineDataSource() - .poll( - async (subscribers) => this.realmsService.getProposalData(subscribers), - Duration.fromObject({ minutes: 7 }), - ) - .transform, Change>>({ - keys: ['proposal'], - pipelines: [ - Pipelines.change((p1, p2) => { - const terminalStates: ProposalState[] = [ - ProposalState.ExecutingWithErrors, - ProposalState.Cancelled, - ProposalState.Succeeded, - ProposalState.Defeated, - ProposalState.Completed, - ]; - const isChangedToTerminalState = Boolean( - terminalStates.find((it) => p2.account.state === it), - ); - return ( - p1.account.state === p2.account.state || !isChangedToTerminalState - ); - }), - ], - }) - .notify({ - type: { - id: NOTIF_TYPE_ID_PROPOSALS, - }, + @OnEvent(CachingEventType.InitialCachingFinished) + onInitialCachingFinished() { + this.monitor.start().catch(this.logger.error); + } + + createMonitor() { + return ( + Monitors.builder({ + sdk: this.sdk, }) - .dialectSdk( - ({ value, context }) => { - const realmId: string = context.origin.realm.pubkey.toBase58(); - const notification = this.constructNotification( - context.origin.realm.account, - realmId, - value, - ); - this.logger.log( - `Sending message for ${context.origin.realmSubscribers.length} subscribers of realm ${realmId} + .defineDataSource() + .poll( + async (subscribers) => + this.realmsService.getProposalData(subscribers), + Duration.fromObject({ minutes: 1 }), + ) + .transform, Change>>({ + keys: ['proposal'], + pipelines: [ + Pipelines.change((p1, p2) => { + const terminalStates: ProposalState[] = [ + ProposalState.ExecutingWithErrors, + ProposalState.Cancelled, + ProposalState.Succeeded, + ProposalState.Defeated, + ProposalState.Completed, + ]; + const isChangedToTerminalState = Boolean( + terminalStates.find((it) => p2.account.state === it), + ); + return ( + p1.account.state === p2.account.state || + !isChangedToTerminalState + ); + }), + ], + }) + .notify({ + type: { + id: NOTIF_TYPE_ID_PROPOSALS, + }, + }) + // .dialectSdk( + // ({ value, context }) => { + // const realmId: string = context.origin.realm.pubkey.toBase58(); + // const notification = this.constructNotification( + // context.origin.realm.account, + // realmId, + // value, + // ); + // this.logger.log( + // `Sending message for ${context.origin.realmSubscribers.length} subscribers of realm ${realmId} + // ${notification.title} + // ${notification.message} + // `, + // ); + // return notification; + // }, + // { dispatch: 'multicast', to: ({ origin }) => origin.realmSubscribers }, + // ) + .custom( + ({ value, context }) => { + const realmId: string = context.origin.realm.pubkey.toBase58(); + const notification = this.constructNotification( + context.origin.realm.account, + realmId, + value, + ); + this.logger.log( + `Sending message for ${context.origin.realmSubscribers.length} subscribers of realm ${realmId} ${notification.title} ${notification.message} `, - ); - return notification; - }, - { dispatch: 'multicast', to: ({ origin }) => origin.realmSubscribers }, - ) - // .custom( - // ({ value, context }) => { - // const realmId: string = context.origin.realm.pubkey.toBase58(); - // const notification = this.constructNotification( - // context.origin.realm.account, - // realmId, - // value, - // ); - // this.logger.log( - // `Sending message for ${context.origin.realmSubscribers.length} subscribers of realm ${realmId} - // ${notification.title} - // ${notification.message} - // `, - // ); - // return notification; - // }, - // new ConsoleNotificationSink(), - // { dispatch: 'multicast', to: ({ origin }) => origin.realmSubscribers }, - // ) - .and() - .build(); - monitor.start(); + ); + return notification; + }, + new ConsoleNotificationSink(), + { + dispatch: 'multicast', + to: ({ origin }) => origin.realmSubscribers, + }, + ) + .and() + .build() + ); } private constructNotification( diff --git a/src/realms-cache.ts b/src/realms-cache.ts new file mode 100644 index 0000000..84b496e --- /dev/null +++ b/src/realms-cache.ts @@ -0,0 +1,262 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { PublicKey } from '@solana/web3.js'; +import { Interval } from 'luxon'; +import { + Governance, + ProgramAccount, + Proposal, + ProposalState, + Realm, + TokenOwnerRecord, +} from '@solana/spl-governance'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { MintLayout, RawMint } from '@solana/spl-token'; +import * as BN from 'bn.js'; +import { + fetchGovernancePrograms, + fetchGovernances, + fetchProposals, + fetchRealms, + fetchRealmsWithMints, + fetchTokenOwnerRecords, +} from './realms-sdk'; +import { groupBy, keyBy } from 'lodash'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +export interface CachingEvent { + type: CachingEventType; +} + +export interface InitialCachingFinished extends CachingEvent { + type: CachingEventType.InitialCachingFinished; +} + +export enum CachingEventType { + InitialCachingFinished = 'caching.initial_finished', +} +export interface RealmMints { + mint?: RawMint; + councilMint?: RawMint; +} + +@Injectable() +export class RealmsCache implements OnModuleInit { + private readonly logger = new Logger(RealmsCache.name); + + // low freq data + splGovernancePrograms: PublicKey[] = []; + realms: Record> = {}; + governancesByRealm: Record[]> = {}; + tokenOwnerRecordsByRealm: Record[]> = + {}; + tokenOwnerRecordsByPublickKey: Record< + string, + ProgramAccount + > = {}; + + // high freq data + proposalsByRealm: Record[]> = {}; + + staticAccountCachingInProgress = false; + currentStaticAccountCachingStartedAt?: Date; + lastStaticAccountCachingSuccessFinishedAt?: Date; + + dynamicAccountCachingInProgress = false; + currentDynamicAccountCachingStartedAt?: Date; + lastDynamicAccountCachingSuccessFinishedAt?: Date; + + isInitialized = false; + initializationError?: Error; + + constructor(private readonly eventEmitter: EventEmitter2) {} + + async onModuleInit() { + this.runInitialCacheAllAccounts().catch((e) => { + this.initializationError = e; + this.logger.error('Error during initial caching', e); + }); + } + + private async runInitialCacheAllAccounts() { + const now = new Date(); + this.logger.log('Starting to cache all accounts'); + await this.cacheStaticAccounts(); + await this.cacheDynamicAccounts(); + this.logger.log( + `Elapsed ${Interval.fromDateTimes(now, new Date()).toDuration( + 'seconds', + )} to cache all accounts`, + ); + this.isInitialized = true; + const cachingFinishedEvent: InitialCachingFinished = { + type: CachingEventType.InitialCachingFinished, + }; + this.eventEmitter.emit( + CachingEventType.InitialCachingFinished, + cachingFinishedEvent, + ); + } + + @Cron(CronExpression.EVERY_30_MINUTES, { + name: 'cacheStaticAccounts', + }) + async periodicCacheStaticAccounts() { + if (!this.isInitialized) { + return; + } + await this.cacheStaticAccounts(); + } + + private async cacheStaticAccounts() { + if (this.staticAccountCachingInProgress) { + this.logger.warn( + `Static account caching already in progress, started at ${this.currentStaticAccountCachingStartedAt}`, + ); + return; + } + this.staticAccountCachingInProgress = true; + try { + await this.cacheGovernancePrograms(); + await this.cacheRealms(); + await this.cacheGovernances(); + await this.cacheTokenOwnerRecords(); + this.lastStaticAccountCachingSuccessFinishedAt = new Date(); + } finally { + this.staticAccountCachingInProgress = false; + this.currentStaticAccountCachingStartedAt = undefined; + } + } + + @Cron(CronExpression.EVERY_5_MINUTES, { + name: 'cacheDynamicAccounts', + }) + async periodicCacheDynamicAccounts() { + if (!this.isInitialized) { + return; + } + await this.cacheDynamicAccounts(); + } + + private async cacheDynamicAccounts() { + if (this.dynamicAccountCachingInProgress) { + this.logger.warn( + `Dynamic account caching already in progress, started at ${this.currentDynamicAccountCachingStartedAt}`, + ); + return; + } + this.dynamicAccountCachingInProgress = true; + try { + await this.cacheProposals(); + this.lastDynamicAccountCachingSuccessFinishedAt = new Date(); + } finally { + this.dynamicAccountCachingInProgress = false; + this.currentDynamicAccountCachingStartedAt = undefined; + } + } + + private async cacheGovernancePrograms() { + const now = new Date(); + this.logger.log('Starting to cache governance programs'); + this.splGovernancePrograms = await fetchGovernancePrograms(); + this.logger.log( + `Elapsed ${Interval.fromDateTimes(now, new Date()).toDuration( + 'seconds', + )} to cache ${this.splGovernancePrograms.length} governance programs`, + ); + } + + private async cacheRealms() { + const now = new Date(); + this.logger.log('Starting to cache realms'); + const realms = await fetchRealms(this.splGovernancePrograms); + const realmsWithMints = await fetchRealmsWithMints(realms); + const realmsByAddress = Object.fromEntries( + realmsWithMints.map((it) => [it.pubkey.toBase58(), it]), + ); + this.realms = Object.assign(this.realms, realmsByAddress); + this.logger.log( + `Elapsed ${Interval.fromDateTimes(now, new Date()).toDuration( + 'seconds', + )} to cache ${realmsWithMints.length} realms`, + ); + } + + private async cacheGovernances() { + const now = new Date(); + this.logger.log('Starting to cache governances'); + const governances = await fetchGovernances(this.splGovernancePrograms); + const governancesByRealm = groupBy(governances, (it) => + it.account.realm.toBase58(), + ); + this.governancesByRealm = Object.assign( + this.governancesByRealm, + governancesByRealm, + ); + this.logger.log( + `Elapsed ${Interval.fromDateTimes(now, new Date()).toDuration( + 'seconds', + )} to cache ${governances.length} governances`, + ); + } + + private async cacheTokenOwnerRecords() { + const now = new Date(); + this.logger.log('Starting to cache token owner records'); + const tokenOwnerRecords = await fetchTokenOwnerRecords( + this.splGovernancePrograms, + ); + + Object.assign( + this.tokenOwnerRecordsByPublickKey, + keyBy(tokenOwnerRecords, (it) => it.pubkey.toBase58()), + ); + const grouped = groupBy(tokenOwnerRecords, (it) => + it.account.realm.toBase58(), + ); + this.tokenOwnerRecordsByRealm = Object.assign( + this.tokenOwnerRecordsByRealm, + grouped, + ); + this.logger.log( + `Elapsed ${Interval.fromDateTimes(now, new Date()).toDuration( + 'seconds', + )} to cache ${tokenOwnerRecords.length} token owner records`, + ); + } + + private async cacheProposals() { + const now = new Date(); + this.logger.log('Starting to cache proposals'); + const proposals = (await fetchProposals(this.splGovernancePrograms)).filter( + (it) => it.account.state !== ProposalState.Draft, + ); + const proposalsByGovernance = groupBy(proposals, (it) => + it.account.governance.toBase58(), + ); + const proposalsByRealm = Object.fromEntries( + Object.keys(this.realms).map((realm) => { + const governances = this.governancesByRealm[realm] ?? []; + const proposals = governances.flatMap( + (governance) => + proposalsByGovernance[governance.pubkey.toBase58()] ?? [], + ); + return [realm, proposals]; + }), + ); + this.proposalsByRealm = Object.assign( + this.proposalsByRealm, + proposalsByRealm, + ); + this.logger.log( + `Elapsed ${Interval.fromDateTimes(now, new Date()).toDuration( + 'seconds', + )} to cache ${proposals.length} proposals`, + ); + } +} + +export function parseMintAccountData(data: Buffer) { + return MintLayout.decode(data); +} + +export const fmtTokenAmount = (c: BN, decimals?: number) => + c?.div(new BN(10).pow(new BN(decimals ?? 0))).toNumber() || 0; diff --git a/src/realms-repository.ts b/src/realms-repository.ts deleted file mode 100644 index 8608bde..0000000 --- a/src/realms-repository.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { Connection, PublicKey } from '@solana/web3.js'; -import { Interval } from 'luxon'; -import { - getAllProposals, - getAllTokenOwnerRecords, - getRealms, - ProgramAccount, - Proposal, - Realm, - TokenOwnerRecord, -} from '@solana/spl-governance'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { RealmsRestService } from './realms-rest-service'; -import { allSettledWithErrorLogging } from './utils/error-handling-utils'; -import { MintInfo, MintLayout, u64 } from '@solana/spl-token'; -import * as BN from 'bn.js'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { - CachingEventType, - CachingFinishedEvent, - CachingStartedEvent, -} from './caching.health'; - -const mainSplGovernanceProgram = new PublicKey( - 'GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw', -); - -const connection = new Connection( - process.env.DIALECT_SDK_SOLANA_RPC_URL ?? process.env.REALMS_SOLANA_RPC_URL!, -); -export type TokenProgramAccount = { - publicKey: PublicKey; - account: T; -}; - -export type MintAccount = MintInfo; - -export interface RealmMints { - mint?: MintInfo; - councilMint?: MintInfo; -} - -@Injectable() -export class RealmsRepository implements OnModuleInit { - private readonly logger = new Logger(RealmsRepository.name); - - splGovernancePrograms: PublicKey[] = []; - realms: Record> = {}; - proposalsGroupedByRealm: Record[]> = {}; - tokenOwnerRecordsByPublicKey: Record< - string, - ProgramAccount - > = {}; - - cachingInProgress = false; - isInitialized: Promise; - - constructor( - private readonly realmsRestService: RealmsRestService, - private readonly eventEmitter: EventEmitter2, - ) {} - - async onModuleInit() { - this.isInitialized = this.tryCacheData(); - } - - async initialization(): Promise { - return this.isInitialized; - } - - @Cron(CronExpression.EVERY_MINUTE) - async tryCacheData() { - if (this.cachingInProgress) { - return; - } - this.cachingInProgress = true; - const cachingStartedEvent: CachingStartedEvent = { - timeStarted: Date.now(), - type: CachingEventType.Started, - }; - this.eventEmitter.emit(CachingEventType.Started, cachingStartedEvent); - try { - await this.cacheAccounts(); - } finally { - this.cachingInProgress = false; - const cachingFinishedEvent: CachingFinishedEvent = { - type: CachingEventType.Finished, - }; - this.eventEmitter.emit(CachingEventType.Finished, cachingFinishedEvent); - } - } - - async cacheAccounts() { - const now = new Date(); - this.logger.log(`Started caching account addresses`); - this.splGovernancePrograms = await this.getSplGovernancePrograms(); - this.logger.log( - `Found ${this.splGovernancePrograms.length} spl governance programs`, - ); - const fetchedRealms = await this.getRealms(this.splGovernancePrograms); - this.realms = Object.assign(this.realms, fetchedRealms); - this.logger.log(`Found ${Object.values(this.realms).length} realms`); - this.logger.log('Start finding proposals'); - const fetchedProposals = await this.getProposalsByRealmPublicKey( - Object.values(this.realms), - ); - this.proposalsGroupedByRealm = Object.assign( - this.proposalsGroupedByRealm, - fetchedProposals, - ); - this.logger.log( - `Found ${ - Object.values(this.proposalsGroupedByRealm).flat().length - } proposals`, - ); - const fetchedTokenOwnerRecords = - await this.getAllTokenOwnerRecordsByPublicKey(Object.values(this.realms)); - this.tokenOwnerRecordsByPublicKey = Object.assign( - this.tokenOwnerRecordsByPublicKey, - fetchedTokenOwnerRecords, - ); - this.logger.log( - `Found ${ - Object.keys(this.tokenOwnerRecordsByPublicKey).length - } token owner records`, - ); - const elapsed = Interval.fromDateTimes(now, new Date()).toDuration(); - this.logger.log(`Elapsed ${elapsed.toISO()} to cache accounts.'`); - } - - private async getSplGovernancePrograms(): Promise { - // return [mainSplGovernanceProgram]; - try { - const splGovernancePrograms = - await this.realmsRestService.getSplGovernancePrograms(); - const allSplGovernancePrograms = [ - ...new Set([ - ...splGovernancePrograms.map((it) => it.toBase58()), - mainSplGovernanceProgram.toBase58(), - ]), - ]; - return allSplGovernancePrograms.map((it) => new PublicKey(it)); - } catch (e) { - const error = e as Error; - this.logger.error( - `Failed to get spl governance programs, reason: ${error.message} `, - ); - return [mainSplGovernanceProgram]; - } - } - - private async getRealms(splGovernancePrograms: PublicKey[]) { - const result = await allSettledWithErrorLogging( - splGovernancePrograms.map((it) => getRealms(connection, it)), - (errors) => `Failed to get ${errors.length} realms, reasons: ${errors}`, - ); - const allRealms = result.fulfilledResults.flat(); - - const realmsWithMints = await allSettledWithErrorLogging( - allRealms.map(async (realm) => { - const mintsArray = ( - await Promise.all([ - realm.account.communityMint - ? tryGetMint(connection, realm.account.communityMint) - : undefined, - realm.account.config?.councilMint - ? tryGetMint(connection, realm.account.config.councilMint) - : undefined, - ]) - ).filter(Boolean); - - const realmMints = Object.fromEntries( - mintsArray.map((m) => [m!.publicKey.toBase58(), m!.account]), - ); - const realmMintPk = realm.account.communityMint; - const realmMint = realmMints[realmMintPk.toBase58()]; - const realmCouncilMintPk = realm.account.config.councilMint; - const realmCouncilMint = - realmCouncilMintPk && realmMints[realmCouncilMintPk.toBase58()]; - const mints: RealmMints = { - mint: realmMint, - councilMint: realmCouncilMint, - }; - return { - ...realm, - account: { - ...realm.account, - ...mints, - }, - }; - }), - (errors) => - `Failed to get ${errors.length} realm mint data, reasons: ${errors}`, - ); - // const filtered = realmsWithMints.fulfilledResults.filter( - // (it) => - // it.pubkey.toBase58() === - // 'AzCvN6DwPozJMhT7bSUok1C2wc4oAmYgm1wTo9vCKLap' || - // it.pubkey.toBase58() === 'By2sVGZXwfQq6rAiAM3rNPJ9iQfb5e2QhnF4YjJ4Bip', - // ); - return Object.fromEntries( - // filtered.map((it) => [it.pubkey.toBase58(), it]), - realmsWithMints.fulfilledResults.map((it) => [it.pubkey.toBase58(), it]), - ); - } - - private async getProposalsByRealmPublicKey( - realms: ProgramAccount[], - ): Promise[]>> { - const result = await allSettledWithErrorLogging( - realms.map(async (it) => { - const proposals = await getAllProposals( - connection, - it.owner, - it.pubkey, - ); - return { - realmPublicKey: it.pubkey, - proposals: proposals.flat(), - }; - }), - (errors) => - `Failed to get proposals fpr ${errors.length} reams, reasons: ${errors}`, - ); - return Object.fromEntries( - result.fulfilledResults.map(({ realmPublicKey, proposals }) => [ - realmPublicKey.toBase58(), - proposals, - // proposals.map((it) => { - // const account = it.account; - // account.state = - // Math.random() > 0.5 - // ? ProposalState.Voting - // : ProposalState.Succeeded; - // return { - // ...it, - // account: account, - // }; - // }), - ]), - ); - } - - private async getAllTokenOwnerRecordsByPublicKey( - realms: ProgramAccount[], - ): Promise>> { - const result = await allSettledWithErrorLogging( - realms.map((it) => - getAllTokenOwnerRecords(connection, it.owner, it.pubkey), - ), - (errors) => - `Failed to get token owner records for ${errors.length} realms, reasons: ${errors}`, - ); - const flattened = result.fulfilledResults.flat(); - return Object.fromEntries(flattened.map((it) => [it.pubkey, it])); - } -} - -export async function tryGetMint( - connection: Connection, - publicKey: PublicKey, -): Promise | undefined> { - try { - const result = await connection.getAccountInfo(publicKey); - const data = Buffer.from(result!.data); - const account = parseMintAccountData(data); - return { - publicKey, - account, - }; - } catch (ex) { - console.warn(`Can't fetch mint ${publicKey?.toBase58()}`, ex); - } -} - -export function parseMintAccountData(data: Buffer) { - const mintInfo = MintLayout.decode(data); - if (mintInfo.mintAuthorityOption === 0) { - mintInfo.mintAuthority = null; - } else { - mintInfo.mintAuthority = new PublicKey(mintInfo.mintAuthority); - } - - mintInfo.supply = u64.fromBuffer(mintInfo.supply); - mintInfo.isInitialized = mintInfo.isInitialized != 0; - - if (mintInfo.freezeAuthorityOption === 0) { - mintInfo.freezeAuthority = null; - } else { - mintInfo.freezeAuthority = new PublicKey(mintInfo.freezeAuthority); - } - return mintInfo; -} - -export const fmtTokenAmount = (c: BN, decimals?: number) => - c?.div(new BN(10).pow(new BN(decimals ?? 0))).toNumber() || 0; diff --git a/src/realms-sdk.ts b/src/realms-sdk.ts new file mode 100644 index 0000000..6226acc --- /dev/null +++ b/src/realms-sdk.ts @@ -0,0 +1,174 @@ +import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; +import { + getAccountTypes, + getBorshProgramAccounts, + getGovernanceSchemaForAccount, + Governance, + GovernanceAccount, + GovernanceAccountClass, + MemcmpFilter, + ProgramAccount, + Proposal, + Realm, + TokenOwnerRecord, +} from '@solana/spl-governance'; +import { RealmsRestService } from './realms-rest-service'; +import { HttpService } from '@nestjs/axios'; +import { chunk, compact, keyBy, uniqBy, zip } from 'lodash'; +import { parseMintAccountData } from './realms-cache'; +import { sleepSecs } from 'twitter-api-v2/dist/v1/media-helpers.v1'; + +const connection = new Connection(process.env.DIALECT_SDK_SOLANA_RPC_URL!); + +const realmsRestService = new RealmsRestService(new HttpService()); + +export async function fetchGovernancePrograms() { + const programs = await realmsRestService.getSplGovernancePrograms(); + return programs; +} + +export async function fetchRealms(governancePrograms: PublicKey[]) { + const acc: ProgramAccount[] = []; + for (const splGovernanceProgram of governancePrograms) { + await sleepSecs(1); + const governanceAccounts: ProgramAccount[] = + await getGovernanceAccounts(connection, splGovernanceProgram, Realm, []); + acc.push(...governanceAccounts); + } + return acc; +} + +export async function fetchGovernances(governancePrograms: PublicKey[]) { + const acc: ProgramAccount[] = []; + for (const splGovernanceProgram of governancePrograms) { + await sleepSecs(1); + const governanceAccounts: ProgramAccount[] = + await getGovernanceAccounts( + connection, + splGovernanceProgram, + Governance, + [], + ); + acc.push(...governanceAccounts); + } + return acc; +} + +export async function fetchProposals(governancePrograms: PublicKey[]) { + const acc: ProgramAccount[] = []; + for (const splGovernanceProgram of governancePrograms) { + await sleepSecs(1); + const governanceAccounts: ProgramAccount[] = + await getGovernanceAccounts( + connection, + splGovernanceProgram, + Proposal, + [], + ); + acc.push(...governanceAccounts); + } + return acc; +} + +export async function fetchRealmsWithMints(realms: ProgramAccount[]) { + const communityMints = realms.map((it) => it.account.communityMint); + const councilMints = realms.map((it) => it.account.config.councilMint); + const mints = uniqBy( + [...compact(communityMints), ...compact(councilMints)], + (it) => it.toBase58(), + ); + + const accInfoAcc: (AccountInfo | null)[] = []; + const chunkedMints = chunk(mints, 100); + for (const chunked of chunkedMints) { + await sleepSecs(1); + const multipleAccountsInfo = await connection.getMultipleAccountsInfo( + chunked, + ); + accInfoAcc.push(...multipleAccountsInfo); + } + const mintAccounts = compact(accInfoAcc); + + if (mints.length !== mintAccounts.length) { + console.warn( + `Expected to fetch ${mints.length} mint accounts, but got ${mintAccounts.length}`, + ); + } + + const parsedRawMints = keyBy( + compact( + zip(mints, mintAccounts).map(([address, buffer]) => { + if (!address || !buffer) { + return null; + } + const data = Buffer.from(buffer.data); + const parsed = parseMintAccountData(data); + return { + address, + parsed, + }; + }), + ), + (it) => it.address.toBase58(), + ); + + const realmsWithMints = realms.map((it) => ({ + ...it, + mints: { + mint: parsedRawMints[it.account.communityMint.toBase58()]?.parsed, + councilMint: + it.account.config.councilMint && + parsedRawMints[it.account.config.councilMint.toBase58()]?.parsed, + }, + })); + + return realmsWithMints; +} + +export async function fetchTokenOwnerRecords(governancePrograms: PublicKey[]) { + const acc: ProgramAccount[] = []; + for (const splGovernanceProgram of governancePrograms) { + await sleepSecs(1); + const governanceAccounts: ProgramAccount[] = + await getGovernanceAccounts( + connection, + splGovernanceProgram, + TokenOwnerRecord, + [], + ); + for (const ga of governanceAccounts) { + acc.push(ga); + } + } + return acc; +} + +export async function getGovernanceAccounts( + connection: Connection, + programId: PublicKey, + accountClass: new (args: any) => TAccount, + filters: MemcmpFilter[] = [], +) { + const accountTypes = getAccountTypes( + accountClass as any as GovernanceAccountClass, + ); + + const all: ProgramAccount[] = []; + + for (const accountType of accountTypes) { + const accounts = await getBorshProgramAccounts( + connection, + programId, + (at) => getGovernanceSchemaForAccount(at), + accountClass, + filters, + accountType, + ); + + for (const account of accounts) { + all.push(account); + } + } + + return all; +} diff --git a/src/realms.service.ts b/src/realms.service.ts index 7c34d29..0dd0ddd 100644 --- a/src/realms.service.ts +++ b/src/realms.service.ts @@ -7,9 +7,9 @@ import { } from '@solana/spl-governance'; import { PublicKey } from '@solana/web3.js'; import { Injectable, Logger } from '@nestjs/common'; -import { groupBy } from './utils/collection-utils'; -import { RealmsRepository } from './realms-repository'; -import { chain } from 'lodash'; +import { RealmsCache } from './realms-cache'; +import { chain, groupBy } from 'lodash'; +import * as process from 'process'; export interface RealmData { realm: ProgramAccount; @@ -31,15 +31,16 @@ export interface ProposalData { @Injectable() export class RealmsService { - constructor(private readonly realmsRepository: RealmsRepository) {} + constructor(private readonly realmsCache: RealmsCache) {} private readonly logger = new Logger(RealmsService.name); async getRealmsData( subscribers: ResourceId[], ): Promise[]> { - await this.realmsRepository.initialization(); - const realms = Object.values(this.realmsRepository.realms); + this.checkInitialized(); + this.logger.warn(`Getting realms data`); + const realms = Object.values(this.realmsCache.realms); const proposalsWithMetadataByRealmPublicKey = this.getProposalsGroupedByRealmPublicKey(); const subscribersByRealmPublicKey = this.getSubscribers(subscribers); @@ -61,28 +62,36 @@ export class RealmsService { return sourceData; } + private checkInitialized() { + if (!this.realmsCache.isInitialized) { + this.logger.error( + `Realms cache is not initialized, this function should not be called`, + ); + throw new Error(`Realms cache is not initialized`); + } + } + private getSubscribers(subscribers: ResourceId[]) { - const tokenOwnerRecordsByPublicKey = - this.realmsRepository.tokenOwnerRecordsByPublicKey; + const realmPublicKeyToTokenOwnerRecords = + this.realmsCache.tokenOwnerRecordsByRealm; const subscribersByPublicKey = Object.fromEntries( subscribers.map((it) => [it.toBase58(), it]), ); - const realmPublicKeyToTokenOwnerRecords: Record< - string, - ProgramAccount[] - > = groupBy(Object.values(tokenOwnerRecordsByPublicKey), (it) => - it.account.realm.toBase58(), - ); const subscribersByRealmPublicKey = Object.fromEntries( - Object.entries(realmPublicKeyToTokenOwnerRecords).map(([k, v]) => [ - k, - v.flatMap((it) => { - const subscriber = - subscribersByPublicKey[it.account.governingTokenOwner.toBase58()]; - return subscriber ? [subscriber] : []; - }), - ]), + Object.entries(realmPublicKeyToTokenOwnerRecords).map( + ([realm, tokenOwnerRecords]) => [ + realm, + tokenOwnerRecords.flatMap((it) => { + const subscriber = + subscribersByPublicKey[it.account.governingTokenOwner.toBase58()]; + return subscriber ? [subscriber] : []; + }), + ], + ), + ); + this.logger.warn( + `Got ${Object.values(subscribersByRealmPublicKey).length} subscribers`, ); return subscribersByRealmPublicKey; } @@ -90,8 +99,8 @@ export class RealmsService { async getProposalData( subscribers: ResourceId[], ): Promise[]> { - await this.realmsRepository.initialization(); - const realms = Object.values(this.realmsRepository.realms); + this.checkInitialized(); + const realms = Object.values(this.realmsCache.realms); const proposals = this.getProposalsGroupedByRealmPublicKey(); const realmsByPublicKey = Object.fromEntries( realms.map((it) => [it.pubkey, it]), @@ -132,10 +141,9 @@ export class RealmsService { } private getProposalsGroupedByRealmPublicKey() { - const proposalsGroupedByRealm = - this.realmsRepository.proposalsGroupedByRealm; + const proposalsGroupedByRealm = this.realmsCache.proposalsByRealm; const tokenOwnerRecordsByPublicKey = - this.realmsRepository.tokenOwnerRecordsByPublicKey; + this.realmsCache.tokenOwnerRecordsByPublickKey; const proposalsWithMetadataByRealmPublicKey: Record< string, ProposalWithMetadata[] diff --git a/src/utils/collection-utils.ts b/src/utils/collection-utils.ts deleted file mode 100644 index fa33bd9..0000000 --- a/src/utils/collection-utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function groupBy(arr: T[], key: (i: T) => K) { - return arr.reduce((groups, item) => { - (groups[key(item)] ||= []).push(item); - return groups; - }, {} as Record); -} diff --git a/src/utils/entropy-utils.ts b/src/utils/entropy-utils.ts deleted file mode 100644 index cbabca9..0000000 --- a/src/utils/entropy-utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Duration } from 'luxon'; - -export function fakePositiveDurationOnTestMode(duration: Duration): Duration { - if (process.env.TEST_MODE === 'true') { - return Duration.fromObject({ hours: Math.round(Math.random() * 30) }); - } - return duration; -} - -export function sliceRandomlyOnTestMode(array: T[]): T[] { - if (process.env.TEST_MODE === 'true') { - return shuffle(array).slice(0, Math.round(Math.random() * 2)); - } - return array; -} - -function shuffle(array: T[]) { - return array.sort(() => Math.random() - 0.5); -} diff --git a/src/utils/error-handling-utils.ts b/src/utils/error-handling-utils.ts deleted file mode 100644 index 4efb0de..0000000 --- a/src/utils/error-handling-utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface AllSettledResult { - fulfilledResults: T[]; - rejectedReasons: Error[]; -} - -export async function allSettledWithErrorLogging( - promises: Promise[], - errorMessageBuilder: (errors: Error[]) => string, -): Promise> { - const allSettled = await Promise.allSettled(promises); - const fulfilledResults = allSettled - .filter((it) => it.status === 'fulfilled') - .map((it) => it as PromiseFulfilledResult) - .map((it) => it.value); - const rejectedReasons = allSettled - .filter((it) => it.status === 'rejected') - .map((it) => it as PromiseRejectedResult) - .map((it) => it.reason as Error); - - if (rejectedReasons.length > 0) { - console.error(errorMessageBuilder(rejectedReasons)); - } - return { - fulfilledResults, - rejectedReasons, - }; -} diff --git a/tsconfig.json b/tsconfig.json index cfbd80d..fb50356 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "outDir": "./dist", "baseUrl": "./", "incremental": true, + "strict": true, "skipLibCheck": true, "strictNullChecks": true, "noImplicitAny": false, diff --git a/yarn.lock b/yarn.lock index fae38b1..e4d9d3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -449,6 +449,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.17.2", "@babel/runtime@^7.23.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1027,6 +1034,18 @@ dependencies: tslib "2.4.0" +"@noble/curves@^1.2.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" + integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.3": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1128,6 +1147,16 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@solana/buffer-layout-utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" + integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/web3.js" "^1.32.0" + bigint-buffer "^1.1.5" + bignumber.js "^9.0.1" + "@solana/buffer-layout@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz#75b1b11adc487234821c81dfae3119b73a5fd734" @@ -1135,18 +1164,142 @@ dependencies: buffer "~6.0.3" -"@solana/spl-governance@^0.0.34": - version "0.0.34" - resolved "https://registry.yarnpkg.com/@solana/spl-governance/-/spl-governance-0.0.34.tgz#c61d81d356dbcee961bbc85e5d3538846fea57ad" - integrity sha512-tZppBiiVkUa5v+B/Ds+TqZ4yxR/vaIYLRxBk7x6R22dwk4/9SU87bVE60kRdDqTdMzqScFxIMdhaGl/fCX533A== +"@solana/buffer-layout@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" + integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== + dependencies: + buffer "~6.0.3" + +"@solana/codecs-core@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-experimental.8618508.tgz#4f6709dd50e671267f3bea7d09209bc6471b7ad0" + integrity sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA== + +"@solana/codecs-core@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-preview.2.tgz#689784d032fbc1fedbde40bb25d76cdcecf6553b" + integrity sha512-gLhCJXieSCrAU7acUJjbXl+IbGnqovvxQLlimztPoGgfLQ1wFYu+XJswrEVQqknZYK1pgxpxH3rZ+OKFs0ndQg== + dependencies: + "@solana/errors" "2.0.0-preview.2" + +"@solana/codecs-data-structures@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-experimental.8618508.tgz#c16a704ac0f743a2e0bf73ada42d830b3402d848" + integrity sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + +"@solana/codecs-data-structures@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.2.tgz#e82cb1b6d154fa636cd5c8953ff3f32959cc0370" + integrity sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg== + dependencies: + "@solana/codecs-core" "2.0.0-preview.2" + "@solana/codecs-numbers" "2.0.0-preview.2" + "@solana/errors" "2.0.0-preview.2" + +"@solana/codecs-numbers@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-experimental.8618508.tgz#d84f9ed0521b22e19125eefc7d51e217fcaeb3e4" + integrity sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + +"@solana/codecs-numbers@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.2.tgz#56995c27396cd8ee3bae8bd055363891b630bbd0" + integrity sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw== + dependencies: + "@solana/codecs-core" "2.0.0-preview.2" + "@solana/errors" "2.0.0-preview.2" + +"@solana/codecs-strings@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-experimental.8618508.tgz#72457b884d9be80b59b263bcce73892b081e9402" + integrity sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + +"@solana/codecs-strings@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.2.tgz#8bd01a4e48614d5289d72d743c3e81305d445c46" + integrity sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g== + dependencies: + "@solana/codecs-core" "2.0.0-preview.2" + "@solana/codecs-numbers" "2.0.0-preview.2" + "@solana/errors" "2.0.0-preview.2" + +"@solana/codecs@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-preview.2.tgz#d6615fec98f423166fb89409f9a4ad5b74c10935" + integrity sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA== + dependencies: + "@solana/codecs-core" "2.0.0-preview.2" + "@solana/codecs-data-structures" "2.0.0-preview.2" + "@solana/codecs-numbers" "2.0.0-preview.2" + "@solana/codecs-strings" "2.0.0-preview.2" + "@solana/options" "2.0.0-preview.2" + +"@solana/errors@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-preview.2.tgz#e0ea8b008c5c02528d5855bc1903e5e9bbec322e" + integrity sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA== + dependencies: + chalk "^5.3.0" + commander "^12.0.0" + +"@solana/options@2.0.0-experimental.8618508": + version "2.0.0-experimental.8618508" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-experimental.8618508.tgz#95385340e85f9e8a81b2bfba089404a61c8e9520" + integrity sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + +"@solana/options@2.0.0-preview.2": + version "2.0.0-preview.2" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-preview.2.tgz#13ff008bf43a5056ef9a091dc7bb3f39321e867e" + integrity sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w== + dependencies: + "@solana/codecs-core" "2.0.0-preview.2" + "@solana/codecs-numbers" "2.0.0-preview.2" + +"@solana/spl-governance@^0.3.28": + version "0.3.28" + resolved "https://registry.yarnpkg.com/@solana/spl-governance/-/spl-governance-0.3.28.tgz#63ff71f235206f069f8ea1e66a40e7cdb6252d3f" + integrity sha512-CUi1hMvzId2rAtMFTlxMwOy0EmFeT0VcmiC+iQnDhRBuM8LLLvRrbTYBWZo3xIvtPQW9HfhVBoL7P/XNFIqYVQ== dependencies: "@solana/web3.js" "^1.22.0" + axios "^1.1.3" bignumber.js "^9.0.1" bn.js "^5.1.3" borsh "^0.3.1" bs58 "^4.0.1" superstruct "^0.15.2" +"@solana/spl-token-group@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.2.tgz#23f754fd535a4df5e2b80293a03aabd58bd99167" + integrity sha512-vLePrFvT9+PfK2KZaddPebTWtRykXUR+060gqomFUcBk/2UPpZtsJGW+xshI9z9Ryrx7FieprZEUCApw34BwrQ== + dependencies: + "@solana/codecs" "2.0.0-preview.2" + "@solana/spl-type-length-value" "0.1.0" + +"@solana/spl-token-metadata@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.2.tgz#876e13432bd2960bd3cac16b9b0af63e69e37719" + integrity sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.8618508" + "@solana/codecs-data-structures" "2.0.0-experimental.8618508" + "@solana/codecs-numbers" "2.0.0-experimental.8618508" + "@solana/codecs-strings" "2.0.0-experimental.8618508" + "@solana/options" "2.0.0-experimental.8618508" + "@solana/spl-type-length-value" "0.1.0" + "@solana/spl-token-registry@^0.2.3775": version "0.2.4574" resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.4574.tgz#13f4636b7bec90d2bb43bbbb83512cd90d2ce257" @@ -1154,7 +1307,7 @@ dependencies: cross-fetch "3.0.6" -"@solana/spl-token@0.1.8", "@solana/spl-token@^0.1.8": +"@solana/spl-token@^0.1.8": version "0.1.8" resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.8.tgz#f06e746341ef8d04165e21fc7f555492a2a0faa6" integrity sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ== @@ -1166,6 +1319,24 @@ buffer-layout "^1.2.0" dotenv "10.0.0" +"@solana/spl-token@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.3.tgz#cb923184fcba3f875f5914a440a68d7f537d0bac" + integrity sha512-mRjJJE9CIBejsg9WAmDp369pWeObm42K2fwsZ4dkJAMCt1KBPb5Eb1vzM5+AYfV/BUTy3QP2oFx8kV+8Doa1xQ== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.2" + "@solana/spl-token-metadata" "^0.1.2" + buffer "^6.0.3" + +"@solana/spl-type-length-value@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz#b5930cf6c6d8f50c7ff2a70463728a4637a2f26b" + integrity sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA== + dependencies: + buffer "^6.0.3" + "@solana/wallet-adapter-base@^0.9.5": version "0.9.9" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.9.tgz#110f99bc9eee18af2625fd6170264e4363e25ffd" @@ -1193,6 +1364,27 @@ superstruct "^0.14.2" tweetnacl "^1.0.0" +"@solana/web3.js@^1.32.0": + version "1.91.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.91.4.tgz#b80295ce72aa125930dfc5b41b4b4e3f85fd87fa" + integrity sha512-zconqecIcBqEF6JiM4xYF865Xc4aas+iWK5qnu7nwKPq9ilRYcn+2GiwpYXqUqqBUe0XCO17w18KO0F8h+QATg== + dependencies: + "@babel/runtime" "^7.23.4" + "@noble/curves" "^1.2.0" + "@noble/hashes" "^1.3.3" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.0" + node-fetch "^2.7.0" + rpc-websockets "^7.5.1" + superstruct "^0.14.2" + "@stablelib/base64@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.1.tgz#bdfc1c6d3a62d7a3b7bbc65b6cce1bb4561641be" @@ -1273,6 +1465,14 @@ "@types/connect" "*" "@types/node" "*" +"@types/bs58@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/bs58/-/bs58-4.0.4.tgz#49fbcb0c7db5f7cea26f0e0f61dc4a41a2445aab" + integrity sha512-0IEpMFXXQi2zXaXl9GJ3sRwQo0uEkD+yFOv+FnAU5lkPtcu6h61xb7jc2CFPEZ5BUOaiP13ThuGc9HD4R8lR5g== + dependencies: + "@types/node" "*" + base-x "^3.0.6" + "@types/connect@*", "@types/connect@^3.4.33": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -1745,6 +1945,13 @@ agent-base@6: dependencies: debug "4" +agentkeepalive@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + ajv-formats@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -1900,6 +2107,15 @@ axios@^0.26.0, axios@^0.26.1: dependencies: follow-redirects "^1.14.8" +axios@^1.1.3: + version "1.6.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" + integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5" @@ -1965,7 +2181,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-x@^3.0.2: +base-x@^3.0.2, base-x@^3.0.6: version "3.0.9" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== @@ -1977,16 +2193,30 @@ base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + bignumber.js@^9.0.1: - version "9.0.2" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" - integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw== + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -2172,7 +2402,7 @@ buffer@6.0.1: base64-js "^1.3.1" ieee754 "^1.2.1" -buffer@6.0.3, buffer@~6.0.3: +buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -2272,6 +2502,11 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -2418,6 +2653,11 @@ commander@4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592" + integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA== + commander@^2.20.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2664,11 +2904,26 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +dotenv-cli@^7.4.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.4.1.tgz#be3895878775c257e9864e5e57ff801d7492dcf8" + integrity sha512-fE1aywjRrWGxV3miaiUr3d2zC/VAiuzEGghi+QzgIA9fEf/M5hLMaRSXb4IxbUAwGmaLi0IozdZddnVU96acag== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.3.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + dotenv-expand@8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e" integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg== +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + dotenv@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" @@ -2679,6 +2934,11 @@ dotenv@16.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== +dotenv@^16.3.0: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -3105,6 +3365,11 @@ fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + fast-url-parser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -3140,6 +3405,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -3198,6 +3468,11 @@ follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + fork-ts-checker-webpack-plugin@7.2.11: version "7.2.11" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d" @@ -3471,6 +3746,13 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3721,6 +4003,24 @@ jayson@^3.4.4: uuid "^8.3.2" ws "^7.4.5" +jayson@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.0.tgz#60dc946a85197317f2b1439d672a8b0a99cea2f9" + integrity sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.4.5" + jest-changed-files@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-28.1.3.tgz#d9aeee6792be3686c47cb988a8eaf82ff4238831" @@ -4505,7 +4805,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4585,6 +4885,13 @@ node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -4942,6 +5249,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -5086,6 +5398,11 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -5176,6 +5493,19 @@ rpc-websockets@^7.4.2: bufferutil "^4.0.1" utf-8-validate "^5.0.2" +rpc-websockets@^7.5.1: + version "7.9.0" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.9.0.tgz#a3938e16d6f134a3999fdfac422a503731bf8973" + integrity sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw== + dependencies: + "@babel/runtime" "^7.17.2" + eventemitter3 "^4.0.7" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -6102,6 +6432,11 @@ ws@^7.4.5: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== +ws@^8.5.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + xmlbuilder@^13.0.2: version "13.0.2" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7"