From bdd303c216267ae8f2aa759639e3d4e8874c96f1 Mon Sep 17 00:00:00 2001 From: Alexey Immoreev Date: Fri, 4 Nov 2022 03:21:29 +0300 Subject: [PATCH] DEC-603: Unify backend & frontend types --- backend/package.json | 1 + backend/src/core/core.module.ts | 2 - backend/src/core/explorer/actions.ts | 75 +- backend/src/core/explorer/dto.ts | 46 +- .../src/core/explorer/explorer.controller.ts | 38 +- backend/src/core/explorer/explorer.module.ts | 5 +- backend/src/core/explorer/explorer.service.ts | 125 +-- .../core/{ => explorer}/indexer.service.ts | 23 +- backend/src/core/explorer/receipt-status.ts | 181 +---- .../src/core/explorer/transaction-status.ts | 9 +- backend/src/core/keys/apiKeys.service.ts | 6 +- backend/src/core/near-rpc/near-rpc.service.ts | 2 +- backend/src/core/projects/dto.ts | 134 ++-- .../src/core/projects/projects.controller.ts | 119 ++- backend/src/core/projects/projects.module.ts | 10 +- backend/src/core/projects/projects.service.ts | 94 ++- backend/src/core/users/dto.ts | 92 +-- backend/src/core/users/users.controller.ts | 71 +- backend/src/core/users/users.service.ts | 44 +- backend/src/helpers.ts | 7 +- backend/src/modules/abi/abi.controller.ts | 16 +- backend/src/modules/abi/abi.service.ts | 18 +- backend/src/modules/abi/abi.ts | 41 - backend/src/modules/abi/dto.ts | 19 +- .../src/modules/alerts/alerts.controller.ts | 134 ++-- backend/src/modules/alerts/alerts.service.ts | 747 ++++++++---------- backend/src/modules/alerts/dto.spec.ts | 150 +++- backend/src/modules/alerts/dto.ts | 315 +++----- backend/src/modules/alerts/serde/db.types.ts | 22 +- backend/src/modules/alerts/serde/dto.types.ts | 32 - .../rule-deserializer.service.ts | 52 +- .../rule-serializer.service.spec.ts | 68 +- .../rule-serializer.service.ts | 20 +- .../alerts/telegram/telegram.service.ts | 4 +- backend/src/modules/alerts/telegram/types.ts | 21 - .../triggered-alerts.controller.ts | 14 +- .../triggered-alerts.service.ts | 56 +- backend/src/modules/alerts/types.ts | 27 - backend/src/modules/rpcstats/dto.ts | 64 +- .../modules/rpcstats/rpcstats.controller.ts | 17 +- .../src/modules/rpcstats/rpcstats.service.ts | 54 +- backend/tsconfig.json | 2 + common/.prettierrc | 4 + common/README.md | 10 + common/package.json | 15 + common/types/abi/abi.schema.ts | 34 + common/types/abi/index.ts | 1 + common/types/alerts/alerts.schema.ts | 268 +++++++ common/types/alerts/index.ts | 2 + .../types/alerts/triggered-alerts.schema.ts | 42 + common/types/api.ts | 297 +++++++ .../types/core/explorer-errors.ts | 127 +-- common/types/core/explorer.schema.ts | 218 +++++ common/types/core/index.ts | 3 + common/types/core/projects.schema.ts | 98 +++ common/types/core/users.schema.ts | 106 +++ .../near-rpc/types.ts => common/types/rpc.ts | 0 common/types/rpcstats/index.ts | 1 + common/types/rpcstats/rpcstats.schema.ts | 76 ++ .../activity/AccountActivityBadge.tsx | 6 +- .../explorer/activity/AccountActivityView.tsx | 14 +- .../components/explorer/activity/types.ts | 100 --- .../explorer/transaction/InspectReceipt.tsx | 22 +- .../explorer/transaction/ReceiptDetails.tsx | 5 +- .../explorer/transaction/ReceiptInfo.tsx | 4 +- .../explorer/transaction/ReceiptKind.tsx | 4 +- .../transaction/TransactionActions.tsx | 16 +- .../transaction/TransactionReceipt.tsx | 6 +- .../explorer/transactions/ActionGroup.tsx | 7 +- .../explorer/transactions/ActionMessage.tsx | 89 +-- .../explorer/transactions/ActionRow.tsx | 4 +- .../explorer/transactions/ActionsList.tsx | 4 +- .../transactions/TransactionAction.tsx | 23 +- .../TransactionExecutionStatus.tsx | 4 +- .../components/explorer/transactions/types.ts | 145 ---- .../components/explorer/utils/NetContext.tsx | 5 +- .../DashboardLayout/DashboardLayout.tsx | 5 +- .../EnvironmentSelector.tsx | 5 +- .../layouts/DashboardLayout/Header/Header.tsx | 7 +- .../ProjectSelector/ProjectSelector.tsx | 3 +- .../layouts/DashboardLayout/types.ts | 5 - .../components/lib/SubnetIcon/SubnetIcon.tsx | 5 +- frontend/hooks/analytics.ts | 2 +- frontend/hooks/api-keys.ts | 15 +- frontend/hooks/contracts.ts | 26 +- frontend/hooks/environments.ts | 29 +- frontend/hooks/new-api-keys.ts | 20 +- frontend/hooks/organizations.ts | 125 +-- frontend/hooks/projects.ts | 16 +- frontend/hooks/selected-project.ts | 6 +- frontend/hooks/user.ts | 3 +- .../alerts/components/AlertTableRow.tsx | 6 +- frontend/modules/alerts/components/Alerts.tsx | 5 +- .../alerts/components/DeleteAlertModal.tsx | 3 +- .../components/DeleteDestinationModal.tsx | 7 +- .../alerts/components/Destinations.tsx | 8 +- .../components/DestinationsSelector.tsx | 4 +- .../components/DestinationsTableRow.tsx | 4 +- .../components/EditDestinationModal.tsx | 20 +- .../EmailDestinationVerification.tsx | 4 +- .../alerts/components/NewDestinationModal.tsx | 7 +- .../TelegramDestinationVerification.tsx | 3 +- .../alerts/components/TriggeredAlerts.tsx | 11 +- .../components/WebhookDestinationSecret.tsx | 12 +- frontend/modules/alerts/hooks/alerts.ts | 23 +- frontend/modules/alerts/hooks/destinations.ts | 21 +- .../modules/alerts/hooks/triggered-alerts.ts | 14 +- .../hooks/verify-destination-interval.ts | 3 +- frontend/modules/alerts/utils/constants.ts | 11 +- frontend/modules/alerts/utils/types.ts | 219 ----- frontend/modules/apis/components/ApiKeys.tsx | 19 +- frontend/modules/apis/components/ApiStats.tsx | 12 +- .../apis/components/CreateApiKeyForm.tsx | 13 +- frontend/modules/apis/hooks/api-stats.ts | 74 +- frontend/modules/apis/utils/constants.ts | 4 +- frontend/modules/apis/utils/types.ts | 57 -- .../contracts/components/AddContractForm.tsx | 8 +- .../contracts/components/ContractAbi.tsx | 5 +- .../contracts/components/ContractDetails.tsx | 11 +- .../contracts/components/ContractInteract.tsx | 4 +- .../components/ContractTransaction.tsx | 4 +- .../components/DeleteContractModal.tsx | 4 +- frontend/modules/contracts/hooks/abi.ts | 16 +- .../contracts/hooks/recent-transactions.ts | 21 +- .../contracts/hooks/wallet-selector.ts | 4 +- .../modules/contracts/utils/embedded-abi.ts | 4 +- .../modules/core/components/StarterGuide.tsx | 4 +- .../core/components/tutorials/SetApiKey.tsx | 2 +- frontend/package.json | 1 + .../pages/alerts/edit-alert/[alertId].tsx | 20 +- frontend/pages/alerts/new-alert.tsx | 32 +- .../triggered-alert/[triggeredAlertId].tsx | 40 +- frontend/pages/contracts/[slug].tsx | 4 +- frontend/pages/contracts/index.tsx | 4 +- frontend/pages/new-nft-tutorial.tsx | 3 +- frontend/pages/new-project.tsx | 3 +- frontend/pages/organizations/[slug].tsx | 36 +- .../pages/organizations/accept-invite.tsx | 3 +- .../pick-project-template/[templateSlug].tsx | 3 +- frontend/pages/project-analytics.tsx | 8 +- frontend/pages/projects.tsx | 4 +- frontend/pages/ui.tsx | 5 +- frontend/stores/settings/types.ts | 6 +- frontend/utils/config.ts | 9 +- frontend/utils/deploy-contract-template.ts | 6 +- frontend/utils/helpers.ts | 5 +- frontend/utils/http.ts | 35 +- frontend/utils/types.ts | 82 -- package-lock.json | 455 ++--------- package.json | 5 +- tsconfig.json | 2 - 151 files changed, 3058 insertions(+), 3373 deletions(-) rename backend/src/core/{ => explorer}/indexer.service.ts (95%) delete mode 100644 backend/src/modules/abi/abi.ts delete mode 100644 backend/src/modules/alerts/serde/dto.types.ts delete mode 100644 backend/src/modules/alerts/telegram/types.ts delete mode 100644 backend/src/modules/alerts/types.ts create mode 100644 common/.prettierrc create mode 100644 common/README.md create mode 100644 common/package.json create mode 100644 common/types/abi/abi.schema.ts create mode 100644 common/types/abi/index.ts create mode 100644 common/types/alerts/alerts.schema.ts create mode 100644 common/types/alerts/index.ts create mode 100644 common/types/alerts/triggered-alerts.schema.ts create mode 100644 common/types/api.ts rename frontend/components/explorer/transaction/types.ts => common/types/core/explorer-errors.ts (60%) create mode 100644 common/types/core/explorer.schema.ts create mode 100644 common/types/core/index.ts create mode 100644 common/types/core/projects.schema.ts create mode 100644 common/types/core/users.schema.ts rename backend/src/core/near-rpc/types.ts => common/types/rpc.ts (100%) create mode 100644 common/types/rpcstats/index.ts create mode 100644 common/types/rpcstats/rpcstats.schema.ts delete mode 100644 frontend/components/explorer/activity/types.ts delete mode 100644 frontend/components/explorer/transactions/types.ts delete mode 100644 frontend/components/layouts/DashboardLayout/types.ts delete mode 100644 frontend/modules/alerts/utils/types.ts delete mode 100644 frontend/modules/apis/utils/types.ts diff --git a/backend/package.json b/backend/package.json index 068ee8d4a..051e97fcb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "mailgun.js": "^7.0.2", "nanoid": "^3.1.30", "@pc/database": "*", + "@pc/common": "*", "passport": "^0.6.0", "passport-http-bearer": "^1.0.1", "pg": "^8.8.0", diff --git a/backend/src/core/core.module.ts b/backend/src/core/core.module.ts index e7df28464..dc2e3fc4f 100644 --- a/backend/src/core/core.module.ts +++ b/backend/src/core/core.module.ts @@ -7,7 +7,6 @@ import { AuthModule } from './auth/auth.module'; import validate from '../config/validate'; import { ApiKeysModule } from './keys/apiKeys.module'; -import { IndexerService } from './indexer.service'; import { EmailModule } from './email/email.module'; @Module({ @@ -26,7 +25,6 @@ import { EmailModule } from './email/email.module'; EmailModule, ExplorerModule, ], - providers: [IndexerService], exports: [EmailModule], }) export class CoreModule {} diff --git a/backend/src/core/explorer/actions.ts b/backend/src/core/explorer/actions.ts index e446d674c..408402286 100644 --- a/backend/src/core/explorer/actions.ts +++ b/backend/src/core/explorer/actions.ts @@ -1,68 +1,5 @@ -import * as RPC from '../near-rpc/types'; - -export type Action = - | { - kind: 'createAccount'; - args: Record; - } - | { - kind: 'deployContract'; - args: { - code: string; - }; - } - | { - kind: 'functionCall'; - args: { - methodName: string; - args: string; - gas: number; - deposit: string; - }; - } - | { - kind: 'transfer'; - args: { - deposit: string; - }; - } - | { - kind: 'stake'; - args: { - stake: string; - publicKey: string; - }; - } - | { - kind: 'addKey'; - args: { - publicKey: string; - accessKey: { - nonce: number; - permission: - | { - type: 'fullAccess'; - } - | { - type: 'functionCall'; - contractId: string; - methodNames: string[]; - }; - }; - }; - } - | { - kind: 'deleteKey'; - args: { - publicKey: string; - }; - } - | { - kind: 'deleteAccount'; - args: { - beneficiaryId: string; - }; - }; +import { Explorer } from '@pc/common/types/core'; +import * as RPC from '@pc/common/types/rpc'; type DatabaseAddKey = { kind: 'ADD_KEY'; @@ -151,7 +88,9 @@ export type DatabaseAction = { | DatabaseTransfer ); -export const mapDatabaseActionToAction = (action: DatabaseAction): Action => { +export const mapDatabaseActionToAction = ( + action: DatabaseAction, +): Explorer.Action => { switch (action.kind) { case 'ADD_KEY': { if (action.args.access_key.permission.permission_kind === 'FULL_ACCESS') { @@ -239,7 +178,9 @@ export const mapDatabaseActionToAction = (action: DatabaseAction): Action => { } }; -export const mapRpcActionToAction = (rpcAction: RPC.ActionView): Action => { +export const mapRpcActionToAction = ( + rpcAction: RPC.ActionView, +): Explorer.Action => { if (rpcAction === 'CreateAccount') { return { kind: 'createAccount', diff --git a/backend/src/core/explorer/dto.ts b/backend/src/core/explorer/dto.ts index 944e30fa9..ca3a4174d 100644 --- a/backend/src/core/explorer/dto.ts +++ b/backend/src/core/explorer/dto.ts @@ -3,34 +3,40 @@ // and had many unaddressed github issues import * as Joi from 'joi'; -import { Net } from '@pc/database/clients/core'; +import { Api } from '@pc/common/types/api'; + +const netSchema = Joi.alternatives('MAINNET', 'TESTNET'); // activity -export type ActivityInputDto = { - net: Net; - contractId: string; -}; -export const ActivityInputSchemas = Joi.object({ - net: Joi.string(), +export const ActivityInputSchemas = Joi.object< + Api.Query.Input<'/explorer/activity'>, + true +>({ + net: netSchema, contractId: Joi.string(), }); // transaction -export type TransactionInputDto = { - net: Net; - hash: string; -}; -export const TransactionInputSchemas = Joi.object({ - net: Joi.string(), +export const TransactionInputSchemas = Joi.object< + Api.Query.Input<'/explorer/transaction'>, + true +>({ + net: netSchema, hash: Joi.string(), }); // balance changes -export type BalanceChangesInputDto = { - net: Net; - receiptId: string; - accountIds: string[]; -}; -export const BalanceChangesInputSchemas = Joi.object({ - net: Joi.string(), +export const BalanceChangesInputSchemas = Joi.object< + Api.Query.Input<'/explorer/balanceChanges'>, + true +>({ + net: netSchema, receiptId: Joi.string(), accountIds: Joi.array().items(Joi.string()), }); + +export const GetTransactionsSchema = Joi.object< + Api.Query.Input<'/explorer/getTransactions'>, + true +>({ + contracts: Joi.array().items(Joi.string()), + net: netSchema, +}); diff --git a/backend/src/core/explorer/explorer.controller.ts b/backend/src/core/explorer/explorer.controller.ts index a4b4f4721..892e9a6ba 100644 --- a/backend/src/core/explorer/explorer.controller.ts +++ b/backend/src/core/explorer/explorer.controller.ts @@ -1,3 +1,4 @@ +import { Api } from '@pc/common/types/api'; import { Controller, Post, @@ -5,38 +6,36 @@ import { Body, BadRequestException, } from '@nestjs/common'; -import { - ExplorerService, - AccountActivity, - Transaction, -} from './explorer.service'; +import { ExplorerService } from './explorer.service'; import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; import { ActivityInputSchemas, TransactionInputSchemas, BalanceChangesInputSchemas, - ActivityInputDto, - TransactionInputDto, - BalanceChangesInputDto, + GetTransactionsSchema, } from './dto'; +import { IndexerService } from './indexer.service'; @Controller('explorer') export class ExplorerController { - constructor(private readonly explorerService: ExplorerService) {} + constructor( + private readonly explorerService: ExplorerService, + private readonly indexerService: IndexerService, + ) {} @Post('activity') @UsePipes(new JoiValidationPipe(ActivityInputSchemas)) async activity( - @Body() { net, contractId }: ActivityInputDto, - ): Promise { + @Body() { net, contractId }: Api.Query.Input<'/explorer/activity'>, + ): Promise> { return this.explorerService.fetchActivity(net, contractId, 50); } @Post('transaction') @UsePipes(new JoiValidationPipe(TransactionInputSchemas)) async transaction( - @Body() { net, hash }: TransactionInputDto, - ): Promise { + @Body() { net, hash }: Api.Query.Input<'/explorer/transaction'>, + ): Promise> { const tx = await this.explorerService.fetchTransaction(net, hash); if (!tx) { throw new BadRequestException('TX_NOT_FOUND'); @@ -47,8 +46,17 @@ export class ExplorerController { @Post('balanceChanges') @UsePipes(new JoiValidationPipe(BalanceChangesInputSchemas)) async balanceChanges( - @Body() { net, receiptId, accountIds }: BalanceChangesInputDto, - ): Promise<(string | undefined)[]> { + @Body() + { net, receiptId, accountIds }: Api.Query.Input<'/explorer/balanceChanges'>, + ): Promise> { return this.explorerService.fetchBalanceChanges(net, receiptId, accountIds); } + + @Post('getTransactions') + @UsePipes(new JoiValidationPipe(GetTransactionsSchema)) + async getTransactions( + @Body() { contracts, net }: Api.Query.Input<'/explorer/getTransactions'>, + ): Promise> { + return this.indexerService.fetchRecentTransactions(contracts, net); + } } diff --git a/backend/src/core/explorer/explorer.module.ts b/backend/src/core/explorer/explorer.module.ts index 65fd30cc7..58ce3cc7f 100644 --- a/backend/src/core/explorer/explorer.module.ts +++ b/backend/src/core/explorer/explorer.module.ts @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common'; import { NearRpcService } from '../near-rpc/near-rpc.service'; import { ExplorerController } from './explorer.controller'; import { ExplorerService } from './explorer.service'; +import { IndexerService } from './indexer.service'; @Module({ - providers: [ExplorerService, NearRpcService], - controllers: [ExplorerController], + providers: [ExplorerService, IndexerService, NearRpcService], + controllers: [ExplorerController, IndexerService], exports: [ExplorerService], }) export class ExplorerModule {} diff --git a/backend/src/core/explorer/explorer.service.ts b/backend/src/core/explorer/explorer.service.ts index 26f2a6f6b..7dbde899b 100644 --- a/backend/src/core/explorer/explorer.service.ts +++ b/backend/src/core/explorer/explorer.service.ts @@ -5,7 +5,6 @@ import { VError } from 'verror'; import { ExpressionBuilder, Kysely, PostgresDialect, sql } from 'kysely'; import { Pool, PoolConfig } from 'pg'; import { - Action, DatabaseAction, mapDatabaseActionToAction, mapRpcActionToAction, @@ -13,7 +12,6 @@ import { import { mapDatabaseTransactionStatus, mapRpcTransactionStatus, - TransactionStatus, } from './transaction-status'; import * as Indexer from './models/readOnlyIndexer'; import * as IndexerActivity from './models/readOnlyIndexerActivity'; @@ -22,46 +20,20 @@ import { StringReference } from 'kysely/dist/cjs/parser/reference-parser'; import { ExtractColumnType } from 'kysely/dist/cjs/util/type-utils'; import { AppConfig } from '../../config/validate'; import { NearRpcService } from '../near-rpc/near-rpc.service'; -import * as RPC from '../near-rpc/types'; -import { mapRpcReceiptStatus, ReceiptExecutionStatus } from './receipt-status'; - -type ActivityConnectionActions = { - parentAction?: AccountActivityAction & ActivityConnection; - childrenActions?: (AccountActivityAction & ActivityConnection)[]; -}; - -type ActivityConnection = { - transactionHash: string; - receiptId?: string; - sender: string; - receiver: string; -}; - -type AccountBatchAction = { - kind: 'batch'; - actions: AccountActivityAction[]; -}; - -type AccountValidatorRewardAction = { - kind: 'validatorReward'; - blockHash: string; -}; - -type AccountActivityAction = - | Action - | AccountValidatorRewardAction - | AccountBatchAction; +import * as RPC from '@pc/common/types/rpc'; +import { mapRpcReceiptStatus } from './receipt-status'; +import { Explorer } from '@pc/common/types/core'; type BasePreview = { signerId: string; receiverId: string; - actions: Action[]; + actions: Explorer.Action[]; }; type TransactionPreview = BasePreview & { type: 'transaction'; hash: string; - status: TransactionStatus; + status: Explorer.TransactionStatus; }; type ReceiptPreview = BasePreview & { @@ -222,7 +194,9 @@ const getIdsFromAccountChanges = ( ); }; -const getActivityAction = (actions: Action[]): AccountActivityAction => { +const getActivityAction = ( + actions: Explorer.Action[], +): Explorer.AccountActivityAction => { if (actions.length === 0) { throw new Error('Unexpected zero-length array of actions'); } @@ -238,7 +212,7 @@ const getActivityAction = (actions: Action[]): AccountActivityAction => { const withActivityConnection = ( input: T, source?: TransactionPreview | ReceiptPreview, -): T & Pick => { +): T & Pick => { if (!source) { return { ...input, @@ -261,7 +235,7 @@ const withActivityConnection = ( const withConnections = ( input: T, source: ReceiptPreview | TransactionPreview, -): T & Pick => { +): T & Pick => { return { ...input, sender: source.signerId, @@ -269,26 +243,7 @@ const withConnections = ( }; }; -type AccountActivityElement = { - involvedAccountId: string | null; - timestamp: number; - direction: 'inbound' | 'outbound'; - deltaAmount: string; - action: AccountActivityAction & - ActivityConnectionActions & - Omit; -}; - -export type AccountActivity = { - items: AccountActivityElement[]; - cursor?: { - blockTimestamp: string; - shardId: number; - indexInChunk: number; - }; -}; - -const getDeposit = (actions: Action[]) => +const getDeposit = (actions: Explorer.Action[]) => actions .map((action) => 'deposit' in action.args ? BigInt(action.args.deposit) : 0n, @@ -306,27 +261,11 @@ const getTransactionFee = ( BigInt(transactionOutcome.outcome.tokens_burnt), ); -type NestedReceiptWithOutcome = { - id: string; - predecessorId: string; - receiverId: string; - actions: Action[]; - outcome: { - block: { - hash: string; - height: number; - timestamp: number; - }; - tokensBurnt: string; - gasBurnt: number; - status: ReceiptExecutionStatus; - logs: string[]; - nestedReceipts: NestedReceiptWithOutcome[]; - }; -}; - -type ParsedReceipt = Omit & { - outcome: Omit & { +type ParsedReceipt = Omit & { + outcome: Omit< + Explorer.NestedReceiptWithOutcome['outcome'], + 'nestedReceipts' + > & { receiptIds: string[]; }; }; @@ -378,7 +317,7 @@ const parseOutcome = ( const collectNestedReceiptWithOutcome = ( idOrHash: string, parsedMap: Map, -): NestedReceiptWithOutcome => { +): Explorer.NestedReceiptWithOutcome => { const parsedElement = parsedMap.get(idOrHash)!; const { receiptIds, ...restOutcome } = parsedElement.outcome; return { @@ -392,17 +331,6 @@ const collectNestedReceiptWithOutcome = ( }; }; -export type Transaction = { - hash: string; - timestamp: number; - signerId: string; - receiverId: string; - fee: string; - amount: string; - status: TransactionStatus; - receipt: NestedReceiptWithOutcome; -}; - @Injectable() export class ExplorerService { private indexerDatabase: Record>; @@ -443,7 +371,7 @@ export class ExplorerService { string, { parentReceiptId: string | null; childrenReceiptIds: string[] } >, - ): AccountActivityElement['action'] | null { + ): Explorer.ActivityActionItemAction | null { switch (change.cause) { case 'RECEIPT': { const connectedReceipt = receiptsMapping.get(change.receiptId!)!; @@ -681,7 +609,7 @@ export class ExplorerService { ...(mapping.get(action.hash) || []), mapDatabaseActionToAction(action as DatabaseAction), ]), - new Map(), + new Map(), ); return transactionRows.reduce((acc, transaction) => { acc.set(transaction.hash, { @@ -714,7 +642,7 @@ export class ExplorerService { accountId: string, limit: number, cursor?: AccountActivityCursor, - ): Promise { + ): Promise { try { const changes = await this.queryBalanceChanges( net, @@ -774,7 +702,10 @@ export class ExplorerService { } } - async fetchTransaction(net: Net, hash: string): Promise { + async fetchTransaction( + net: Net, + hash: string, + ): Promise { try { const indexerDatabase = this.indexerDatabase[net]; const databaseTransaction = await indexerDatabase @@ -864,11 +795,11 @@ export class ExplorerService { net: Net, receiptId: string, accountIds: string[], - ): Promise<(string | undefined)[]> { + ): Promise<(string | null)[]> { try { const activityDatabase = this.indexerActivityDatabase[net]; if (!activityDatabase) { - return accountIds.map(() => undefined); + return accountIds.map(() => null); } const balanceChanges = await activityDatabase .selectFrom('balance_changes') @@ -881,12 +812,12 @@ export class ExplorerService { .orderBy('index_in_chunk', 'desc') .execute(); if (!balanceChanges) { - return accountIds.map(() => undefined); + return accountIds.map(() => null); } return accountIds.map( (accountId) => balanceChanges.find((change) => change.accountId === accountId) - ?.absoluteNonStakedAmount, + ?.absoluteNonStakedAmount ?? null, ); } catch (e: any) { throw new VError(e, 'Failed to fetch transaction'); diff --git a/backend/src/core/indexer.service.ts b/backend/src/core/explorer/indexer.service.ts similarity index 95% rename from backend/src/core/indexer.service.ts rename to backend/src/core/explorer/indexer.service.ts index a09c5b2eb..8ae92e170 100644 --- a/backend/src/core/indexer.service.ts +++ b/backend/src/core/explorer/indexer.service.ts @@ -30,18 +30,8 @@ const DS_INDEXER_TESTNET = 'DS_INDEXER_TESTNET'; import { Sequelize, QueryTypes } from 'sequelize'; import { Net } from '@pc/database/clients/core'; -import { AppConfig } from '../config/validate'; -import { Action } from './explorer/actions'; - -type OldTransaction = { - hash: string; - signerId: string; - receiverId: string; - blockHash: string; - blockTimestamp: number; - transactionIndex: number; - actions: Action[]; -}; +import { AppConfig } from '../../config/validate'; +import { Explorer } from '@pc/common/types/core'; @Injectable() export class IndexerService { @@ -180,7 +170,7 @@ export class IndexerService { async createTransactionsList( transactionsArray, net: Net, - ): Promise { + ): Promise { const transactionsHashes = transactionsArray.map(({ hash }) => hash); const transactionsActionsList = await this.getTransactionsActionsList( transactionsHashes, @@ -244,7 +234,7 @@ export class IndexerService { } async fetchRecentTransactions(accounts: string[], net: Net) { - const promises: Promise[] = []; + const promises: Promise[] = []; for (const account of accounts) { const paginationIndexer = { @@ -264,8 +254,9 @@ export class IndexerService { } const results = await Promise.allSettled(promises); // console.log(results); - let mergedTransactions: (OldTransaction & { sourceContract: string })[] = - []; + let mergedTransactions: (Explorer.Old.Transaction & { + sourceContract: string; + })[] = []; for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.status === 'fulfilled' && result.value?.length) { diff --git a/backend/src/core/explorer/receipt-status.ts b/backend/src/core/explorer/receipt-status.ts index 14e62fc6f..b3feddbe7 100644 --- a/backend/src/core/explorer/receipt-status.ts +++ b/backend/src/core/explorer/receipt-status.ts @@ -1,171 +1,12 @@ import { ExecutionOutcomeStatus } from './models/readOnlyIndexer'; -import * as RPC from '../near-rpc/types'; +import * as RPC from '@pc/common/types/rpc'; +import { Explorer } from '@pc/common/types/core'; -type UnknownError = { type: 'unknown' }; - -const UNKNOWN_ERROR: UnknownError = { type: 'unknown' }; - -type FunctionCallError = - | { - type: 'compilationError'; - error: CompilationError; - } - | { type: 'linkError'; msg: string } - | { type: 'methodResolveError' } - | { type: 'wasmTrap' } - | { type: 'wasmUnknownError' } - | { type: 'hostError' } - | { type: 'evmError' } - | { type: 'executionError'; error: string } - | UnknownError; - -type NewReceiptValidationError = - | { type: 'invalidPredecessorId'; accountId: string } - | { type: 'invalidReceiverId'; accountId: string } - | { type: 'invalidSignerId'; accountId: string } - | { type: 'invalidDataReceiverId'; accountId: string } - | { type: 'returnedValueLengthExceeded'; length: number; limit: number } - | { - type: 'numberInputDataDependenciesExceeded'; - numberOfInputDataDependencies: number; - limit: number; - } - | { type: 'actionsValidation' } - | UnknownError; - -type CompilationError = - | { type: 'codeDoesNotExist'; accountId: string } - | { type: 'prepareError' } - | { type: 'wasmerCompileError'; msg: string } - | { type: 'unsupportedCompiler'; msg: string } - | UnknownError; - -type ReceiptActionError = - | { - type: 'accountAlreadyExists'; - accountId: string; - } - | { - type: 'accountDoesNotExist'; - accountId: string; - } - | { - type: 'createAccountOnlyByRegistrar'; - accountId: string; - registrarAccountId: string; - predecessorId: string; - } - | { - type: 'createAccountNotAllowed'; - accountId: string; - predecessorId: string; - } - | { - type: 'actorNoPermission'; - accountId: string; - actorId: string; - } - | { - type: 'deleteKeyDoesNotExist'; - accountId: string; - publicKey: string; - } - | { - type: 'addKeyAlreadyExists'; - accountId: string; - publicKey: string; - } - | { - type: 'deleteAccountStaking'; - accountId: string; - } - | { - type: 'lackBalanceForState'; - accountId: string; - amount: string; - } - | { - type: 'triesToUnstake'; - accountId: string; - } - | { - type: 'triesToStake'; - accountId: string; - stake: string; - locked: string; - balance: string; - } - | { - type: 'insufficientStake'; - accountId: string; - stake: string; - minimumStake: string; - } - | { - type: 'functionCallError'; - error: FunctionCallError; - } - | { - type: 'newReceiptValidationError'; - error: NewReceiptValidationError; - } - | { type: 'onlyImplicitAccountCreationAllowed'; accountId: string } - | { type: 'deleteAccountWithLargeState'; accountId: string } - | UnknownError; - -type ReceiptTransactionError = - | { type: 'invalidAccessKeyError' } - | { type: 'invalidSignerId'; signerId: string } - | { type: 'signerDoesNotExist'; signerId: string } - | { type: 'invalidNonce'; transactionNonce: number; akNonce: number } - | { type: 'nonceTooLarge'; transactionNonce: number; upperBound: number } - | { type: 'invalidReceiverId'; receiverId: string } - | { type: 'invalidSignature' } - | { - type: 'notEnoughBalance'; - signerId: string; - balance: string; - cost: string; - } - | { type: 'lackBalanceForState'; signerId: string; amount: string } - | { type: 'costOverflow' } - | { type: 'invalidChain' } - | { type: 'expired' } - | { type: 'actionsValidation' } - | { type: 'transactionSizeExceeded'; size: number; limit: number } - | UnknownError; - -type ReceiptExecutionStatusError = - | { - type: 'action'; - error: ReceiptActionError; - } - | { - type: 'transaction'; - error: ReceiptTransactionError; - } - | UnknownError; - -export type ReceiptExecutionStatus = - | { - type: 'failure'; - error: ReceiptExecutionStatusError; - } - | { - type: 'successValue'; - value: string; - } - | { - type: 'successReceiptId'; - receiptId: string; - } - | { - type: 'unknown'; - }; +const UNKNOWN_ERROR: Explorer.Errors.UnknownError = { type: 'unknown' }; const mapRpcCompilationError = ( error: RPC.CompilationError, -): CompilationError => { +): Explorer.Errors.CompilationError => { if ('CodeDoesNotExist' in error) { return { type: 'codeDoesNotExist', @@ -194,7 +35,7 @@ const mapRpcCompilationError = ( const mapRpcFunctionCallError = ( error: RPC.FunctionCallError, -): FunctionCallError => { +): Explorer.Errors.FunctionCallError => { if ('CompilationError' in error) { return { type: 'compilationError', @@ -242,7 +83,7 @@ const mapRpcFunctionCallError = ( }; const mapRpcNewReceiptValidationError = ( error: RPC.NewReceiptValidationError, -): NewReceiptValidationError => { +): Explorer.Errors.NewReceiptValidationError => { if ('InvalidPredecessorId' in error) { return { type: 'invalidPredecessorId', @@ -293,7 +134,7 @@ const mapRpcNewReceiptValidationError = ( const mapRpcReceiptError = ( error: RPC.TxExecutionError, -): ReceiptExecutionStatusError => { +): Explorer.Errors.ReceiptExecutionStatusError => { if ('ActionError' in error) { return { type: 'action', @@ -311,7 +152,7 @@ const mapRpcReceiptError = ( const mapRpcReceiptInvalidTxError = ( error: RPC.InvalidTxError, -): ReceiptTransactionError => { +): Explorer.Errors.ReceiptTransactionError => { if ('InvalidAccessKeyError' in error) { return { type: 'invalidAccessKeyError', @@ -401,7 +242,7 @@ const mapRpcReceiptInvalidTxError = ( const mapRpcReceiptActionError = ( error: RPC.ActionError, -): ReceiptActionError => { +): Explorer.Errors.ReceiptActionError => { const kind = error.kind; if ('AccountAlreadyExists' in kind) { return { @@ -517,7 +358,7 @@ const mapRpcReceiptActionError = ( export const mapRpcReceiptStatus = ( status: RPC.ExecutionStatusView, -): ReceiptExecutionStatus => { +): Explorer.ReceiptExecutionStatus => { if ('SuccessValue' in status) { return { type: 'successValue', value: status.SuccessValue }; } @@ -532,7 +373,7 @@ export const mapRpcReceiptStatus = ( export const mapDatabaseReceiptStatus = ( status: ExecutionOutcomeStatus, -): ReceiptExecutionStatus['type'] => { +): Explorer.ReceiptExecutionStatus['type'] => { switch (status) { case 'SUCCESS_RECEIPT_ID': return 'successReceiptId'; diff --git a/backend/src/core/explorer/transaction-status.ts b/backend/src/core/explorer/transaction-status.ts index 70385b297..dd42f23d4 100644 --- a/backend/src/core/explorer/transaction-status.ts +++ b/backend/src/core/explorer/transaction-status.ts @@ -1,11 +1,10 @@ import { ExecutionOutcomeStatus } from './models/readOnlyIndexer'; -import * as RPC from '../near-rpc/types'; - -export type TransactionStatus = 'unknown' | 'failure' | 'success'; +import { Explorer } from '@pc/common/types/core'; +import * as RPC from '@pc/common/types/rpc'; export const mapRpcTransactionStatus = ( status: RPC.FinalExecutionStatus, -): TransactionStatus => { +): Explorer.TransactionStatus => { if ('SuccessValue' in status) { return 'success'; } @@ -17,7 +16,7 @@ export const mapRpcTransactionStatus = ( export const mapDatabaseTransactionStatus = ( status: ExecutionOutcomeStatus, -): TransactionStatus => { +): Explorer.TransactionStatus => { switch (status) { case 'SUCCESS_VALUE': case 'SUCCESS_RECEIPT_ID': diff --git a/backend/src/core/keys/apiKeys.service.ts b/backend/src/core/keys/apiKeys.service.ts index 3aa7eb903..0862de53a 100644 --- a/backend/src/core/keys/apiKeys.service.ts +++ b/backend/src/core/keys/apiKeys.service.ts @@ -235,7 +235,11 @@ export class ApiKeysService { try { const key = await this.provisioningService.fetch(slug); - return { keySlug: slug, description: keyDetails.description, key }; + return { + keySlug: slug, + description: keyDetails.description, + key, + }; } catch (e: any) { throw new VError(e, 'Failed while getting a key'); } diff --git a/backend/src/core/near-rpc/near-rpc.service.ts b/backend/src/core/near-rpc/near-rpc.service.ts index 4bc7acede..2b7439652 100644 --- a/backend/src/core/near-rpc/near-rpc.service.ts +++ b/backend/src/core/near-rpc/near-rpc.service.ts @@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config'; import { VError } from 'verror'; import { Net } from '@pc/database/clients/core'; import { AppConfig } from '../../config/validate'; -import * as RPC from './types'; +import * as RPC from '@pc/common/types/rpc'; type AccountStatus = 'EXISTS' | 'NOT_FOUND'; diff --git a/backend/src/core/projects/dto.ts b/backend/src/core/projects/dto.ts index 639bd7a13..4cbc19bd2 100644 --- a/backend/src/core/projects/dto.ts +++ b/backend/src/core/projects/dto.ts @@ -2,143 +2,117 @@ // because class-validator was experiencing issues at the time of implementation // and had many unaddressed github issues -import { Net, ProjectTutorial } from '@pc/database/clients/core'; +import { Api } from '@pc/common/types/api'; import * as Joi from 'joi'; const projectNameSchema = Joi.string().required().max(50); // create project -export interface CreateProjectDto { - org?: string; - name: string; - tutorial?: ProjectTutorial; -} -export const CreateProjectSchema = Joi.object({ +export const CreateProjectSchema = Joi.object< + Api.Mutation.Input<'/projects/create'>, + true +>({ org: Joi.string(), name: projectNameSchema, - tutorial: Joi.string(), + tutorial: Joi.alternatives('NFT_MARKET', 'CROSSWORD'), }); // eject tutorial project -export interface EjectTutorialProjectDto { - slug: string; -} -export const EjectTutorialProjectSchema = Joi.object({ +export const EjectTutorialProjectSchema = Joi.object< + Api.Mutation.Input<'/projects/ejectTutorial'>, + true +>({ slug: Joi.string().required(), }); // delete project -export interface DeleteProjectDto { - slug: string; -} -export const DeleteProjectSchema = Joi.object({ +export const DeleteProjectSchema = Joi.object< + Api.Mutation.Input<'/projects/delete'>, + true +>({ slug: Joi.string().required(), }); // get project details -export interface GetProjectDetailsDto { - slug: string; -} -export const GetProjectDetailsSchema = Joi.object({ +export const GetProjectDetailsSchema = Joi.object< + Api.Query.Input<'/projects/getDetails'>, + true +>({ slug: Joi.string().required(), }); // add contract -export interface AddContractDto { - project: string; - environment: number; - address: string; -} -export const AddContractSchema = Joi.object({ +export const AddContractSchema = Joi.object< + Api.Mutation.Input<'/projects/addContract'>, + true +>({ project: Joi.string().required(), environment: Joi.number().integer().required(), address: Joi.string().required(), }); // remove contract -export interface RemoveContractDto { - slug: string; -} -export const RemoveContractSchema = Joi.object({ +export const RemoveContractSchema = Joi.object< + Api.Mutation.Input<'/projects/removeContract'>, + true +>({ slug: Joi.string().required(), }); // get contracts -export interface GetContractsDto { - project: string; - environment: number; -} -export const GetContractsSchema = Joi.object({ +export const GetContractsSchema = Joi.object< + Api.Query.Input<'/projects/getContracts'>, + true +>({ project: Joi.string().required(), environment: Joi.number().integer().required(), }); // get contract -export interface GetContractDto { - slug: string; -} -export const GetContractSchema = Joi.object({ +export const GetContractSchema = Joi.object< + Api.Query.Input<'/projects/getContract'>, + true +>({ slug: Joi.string().required(), }); // get environments -export interface GetEnvironmentsDto { - project: string; -} -export const GetEnvironmentsSchema = Joi.object({ +export const GetEnvironmentsSchema = Joi.object< + Api.Query.Input<'/projects/getEnvironments'>, + true +>({ project: Joi.string().required(), }); -// get environment details -export interface GetEnvironmentsDetailsDto { - project: string; - environment: number; -} -export const GetEnvironmentsDetailsSchema = Joi.object({ - project: Joi.string().required(), - environment: Joi.number().integer().required(), -}); - // get keys -export interface GetKeysDto { - project: string; -} -export const GetKeysSchema = Joi.object({ +export const GetKeysSchema = Joi.object< + Api.Query.Input<'/projects/getKeys'>, + true +>({ project: Joi.string().required(), }); // rotate key -export interface RotateKeyDto { - slug: string; -} -export const RotateKeySchema = Joi.object({ +export const RotateKeySchema = Joi.object< + Api.Mutation.Input<'/projects/rotateKey'>, + true +>({ slug: Joi.string().required(), }); // generate key -export interface GenerateKeyDto { - project: string; - description: string; -} -export const GenerateKeySchema = Joi.object({ +export const GenerateKeySchema = Joi.object< + Api.Mutation.Input<'/projects/generateKey'>, + true +>({ project: Joi.string().required(), description: Joi.string().required(), }); // delete key -export interface DeleteKeyDto { - slug: string; -} -export const DeleteKeySchema = Joi.object({ +export const DeleteKeySchema = Joi.object< + Api.Mutation.Input<'/projects/deleteKey'>, + true +>({ slug: Joi.string().required(), }); - -// rotate transactions -export interface GetTransactionsDto { - contracts: string[]; - net: Net; -} -export const GetTransactionsSchema = Joi.object({ - contracts: Joi.array().items(Joi.string()), - net: Joi.string(), -}); diff --git a/backend/src/core/projects/projects.controller.ts b/backend/src/core/projects/projects.controller.ts index a189e2aad..54297fda3 100644 --- a/backend/src/core/projects/projects.controller.ts +++ b/backend/src/core/projects/projects.controller.ts @@ -14,54 +14,34 @@ import { BearerAuthGuard } from '../auth/bearer-auth.guard'; import { ProjectsService } from './projects.service'; import { VError } from 'verror'; import { - AddContractDto, AddContractSchema, - CreateProjectDto, CreateProjectSchema, - DeleteKeyDto, DeleteKeySchema, - DeleteProjectDto, DeleteProjectSchema, - EjectTutorialProjectDto, EjectTutorialProjectSchema, - GenerateKeyDto, GenerateKeySchema, - GetContractDto, GetContractSchema, - GetContractsDto, GetContractsSchema, - GetEnvironmentsDetailsDto, - GetEnvironmentsDetailsSchema, - GetEnvironmentsDto, GetEnvironmentsSchema, - GetKeysDto, GetKeysSchema, - GetProjectDetailsDto, GetProjectDetailsSchema, - GetTransactionsDto, - GetTransactionsSchema, - RemoveContractDto, RemoveContractSchema, - RotateKeyDto, RotateKeySchema, } from './dto'; import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; -import { IndexerService } from '../indexer.service'; +import { Api } from '@pc/common/types/api'; @Controller('projects') export class ProjectsController { - constructor( - private readonly projectsService: ProjectsService, - private readonly indexerService: IndexerService, - ) {} + constructor(private readonly projectsService: ProjectsService) {} @Post('create') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(CreateProjectSchema)) async create( @Request() req, - @Body() { org, name, tutorial }: CreateProjectDto, - ) { + @Body() { org, name, tutorial }: Api.Mutation.Input<'/projects/create'>, + ): Promise> { try { return await this.projectsService.create( req.user, @@ -80,8 +60,8 @@ export class ProjectsController { @UsePipes(new JoiValidationPipe(EjectTutorialProjectSchema)) async ejectTutorial( @Request() req, - @Body() { slug }: EjectTutorialProjectDto, - ) { + @Body() { slug }: Api.Mutation.Input<'/projects/ejectTutorial'>, + ): Promise> { try { return await this.projectsService.ejectTutorial(req.user, { slug }); } catch (e: any) { @@ -93,7 +73,10 @@ export class ProjectsController { @HttpCode(204) @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(DeleteProjectSchema)) - async delete(@Request() req, @Body() { slug }: DeleteProjectDto) { + async delete( + @Request() req, + @Body() { slug }: Api.Mutation.Input<'/projects/delete'>, + ): Promise> { try { return await this.projectsService.delete(req.user, { slug }); } catch (e: any) { @@ -104,7 +87,10 @@ export class ProjectsController { @Post('getDetails') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(GetProjectDetailsSchema)) - async getDetails(@Request() req, @Body() { slug }: GetProjectDetailsDto) { + async getDetails( + @Request() req, + @Body() { slug }: Api.Query.Input<'/projects/getDetails'>, + ): Promise> { try { return await this.projectsService.getProjectDetails(req.user, { slug }); } catch (e: any) { @@ -117,8 +103,13 @@ export class ProjectsController { @UsePipes(new JoiValidationPipe(AddContractSchema)) async addContract( @Request() req, - @Body() { project, environment, address }: AddContractDto, - ) { + @Body() + { + project, + environment, + address, + }: Api.Mutation.Input<'/projects/addContract'>, + ): Promise> { try { return await this.projectsService.addContract( req.user, @@ -135,7 +126,10 @@ export class ProjectsController { @HttpCode(204) @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(RemoveContractSchema)) - async removeContract(@Request() req, @Body() { slug }: RemoveContractDto) { + async removeContract( + @Request() req, + @Body() { slug }: Api.Mutation.Input<'/projects/removeContract'>, + ): Promise> { try { return await this.projectsService.removeContract(req.user, { slug, @@ -150,8 +144,8 @@ export class ProjectsController { @UsePipes(new JoiValidationPipe(GetContractsSchema)) async getContracts( @Request() req, - @Body() { project, environment }: GetContractsDto, - ) { + @Body() { project, environment }: Api.Query.Input<'/projects/getContracts'>, + ): Promise> { try { return await this.projectsService.getContracts( req.user, @@ -166,7 +160,10 @@ export class ProjectsController { @Post('getContract') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(GetContractSchema)) - async getContract(@Request() req, @Body() { slug }: GetContractDto) { + async getContract( + @Request() req, + @Body() { slug }: Api.Query.Input<'/projects/getContract'>, + ): Promise> { try { return await this.projectsService.getContract(req.user, slug); } catch (e: any) { @@ -176,7 +173,10 @@ export class ProjectsController { @Post('list') @UseGuards(BearerAuthGuard) - async list(@Request() req) { + async list( + @Request() req, + @Body() _: Api.Query.Input<'/projects/list'>, + ): Promise> { return await this.projectsService.list(req.user); } @@ -185,8 +185,8 @@ export class ProjectsController { @UsePipes(new JoiValidationPipe(GetEnvironmentsSchema)) async getEnvironments( @Request() req, - @Body() { project }: GetEnvironmentsDto, - ) { + @Body() { project }: Api.Query.Input<'/projects/getEnvironments'>, + ): Promise> { try { return await this.projectsService.getEnvironments(req.user, { slug: project, @@ -196,28 +196,13 @@ export class ProjectsController { } } - @Post('getEnvironmentDetails') - @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetEnvironmentsDetailsSchema)) - async getEnvironmentDetails( - @Request() req, - @Body() { project, environment }: GetEnvironmentsDetailsDto, - ) { - try { - return await this.projectsService.getEnvironmentDetails( - req.user, - project, - environment, - ); - } catch (e: any) { - throw mapError(e); - } - } - @Post('getKeys') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(GetKeysSchema)) - async getKeys(@Request() req, @Body() { project }: GetKeysDto) { + async getKeys( + @Request() req, + @Body() { project }: Api.Query.Input<'/projects/getKeys'>, + ): Promise> { try { return await this.projectsService.getKeys(req.user, project); } catch (e: any) { @@ -228,7 +213,10 @@ export class ProjectsController { @Post('rotateKey') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(RotateKeySchema)) - async rotateKey(@Request() req, @Body() { slug }: RotateKeyDto) { + async rotateKey( + @Request() req, + @Body() { slug }: Api.Mutation.Input<'/projects/rotateKey'>, + ): Promise> { try { return await this.projectsService.rotateKey(req.user, slug); } catch (e: any) { @@ -241,8 +229,9 @@ export class ProjectsController { @UsePipes(new JoiValidationPipe(GenerateKeySchema)) async generateKey( @Request() req, - @Body() { project, description }: GenerateKeyDto, - ) { + @Body() + { project, description }: Api.Mutation.Input<'/projects/generateKey'>, + ): Promise> { try { return await this.projectsService.generateKey( req.user, @@ -258,20 +247,16 @@ export class ProjectsController { @HttpCode(204) @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(DeleteKeySchema)) - async deleteKey(@Request() req, @Body() { slug }: DeleteKeyDto) { + async deleteKey( + @Request() req, + @Body() { slug }: Api.Mutation.Input<'/projects/deleteKey'>, + ): Promise> { try { return await this.projectsService.deleteKey(req.user, slug); } catch (e: any) { throw mapError(e); } } - - @Post('getTransactions') - @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetTransactionsSchema)) - async getTransactions(@Body() { contracts, net }: GetTransactionsDto) { - return this.indexerService.fetchRecentTransactions(contracts, net); - } } function mapError(e: Error) { diff --git a/backend/src/core/projects/projects.module.ts b/backend/src/core/projects/projects.module.ts index eeb31fb40..5990504b7 100644 --- a/backend/src/core/projects/projects.module.ts +++ b/backend/src/core/projects/projects.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { IndexerService } from '../indexer.service'; import { ApiKeysModule } from '../keys/apiKeys.module'; import { NearRpcService } from '@/src/core/near-rpc/near-rpc.service'; import { PrismaService } from '../prisma.service'; @@ -13,19 +12,12 @@ import { UsersModule } from '../users/users.module'; providers: [ ProjectsService, PrismaService, - IndexerService, PermissionsService, ReadonlyService, NearRpcService, ], controllers: [ProjectsController], - imports: [ - PrismaService, - ApiKeysModule, - IndexerService, - NearRpcService, - UsersModule, - ], + imports: [PrismaService, ApiKeysModule, NearRpcService, UsersModule], exports: [ProjectsService, PermissionsService, ReadonlyService], }) export class ProjectsModule {} diff --git a/backend/src/core/projects/projects.service.ts b/backend/src/core/projects/projects.service.ts index 9d8f1ba5e..c8ae1eebd 100644 --- a/backend/src/core/projects/projects.service.ts +++ b/backend/src/core/projects/projects.service.ts @@ -581,7 +581,7 @@ export class ProjectsService { project: Project['slug'], subId: Environment['subId'], ) { - const environment = await this.getActiveEnvironment(project, subId, true); + const environment = await this.getActiveEnvironmentAssert(project, subId); // throw an error if the user doesn't have permission to perform this action await this.checkUserPermission({ @@ -639,43 +639,57 @@ export class ProjectsService { return project; } - /** - * - * @param environmentWhereUnique - * @param assert Should be set to true when this function is being called - * for an assertion and the full project info does not need - * to be returned - * @returns full Prisma.Project is assert is false - */ - async getActiveEnvironment( + async getActiveEnvironmentAssert( projectSlug: Project['slug'], subId: Environment['subId'], - assert = false, ) { // check that project is active - let environment; - if (assert) { - // quick check, only return minimal info - environment = await this.prisma.environment.findFirst({ - where: { - subId, - project: { - slug: projectSlug, - }, - }, - select: { id: true, active: true, project: true }, - }); - } else { - environment = await this.prisma.environment.findFirst({ - where: { - subId, - project: { - slug: projectSlug, - }, + // quick check, only return minimal info + const environment = await this.prisma.environment.findFirst({ + where: { + subId, + project: { + slug: projectSlug, }, - include: { project: true }, - }); + }, + select: { id: true, active: true, project: true }, + }); + if (!environment) { + throw new VError( + { info: { code: 'BAD_ENVIRONMENT' } }, + 'Environment not found', + ); + } + if (!environment.active) { + throw new VError( + { info: { code: 'BAD_ENVIRONMENT' } }, + 'Environment not active', + ); } + if (!environment.project.active) { + throw new VError( + { info: { code: 'BAD_ENVIRONMENT' } }, + 'Project not active', + ); + } + + return environment; + } + + async getActiveEnvironment( + projectSlug: Project['slug'], + subId: Environment['subId'], + ) { + // check that project is active + const environment = await this.prisma.environment.findFirst({ + where: { + subId, + project: { + slug: projectSlug, + }, + }, + include: { project: true }, + }); if (!environment) { throw new VError( { info: { code: 'BAD_ENVIRONMENT' } }, @@ -732,17 +746,15 @@ export class ProjectsService { }, }); - return projects.map((p) => { - const isPersonal = !!p.org.personalForUserId; - return { - ...p, + return projects.map( + ({ org: { personalForUserId, ...org }, ...project }) => ({ + ...project, org: { - name: isPersonal ? undefined : p.org.name, - slug: p.org.slug, - isPersonal, + ...org, + isPersonal: !!personalForUserId, }, - }; - }); + }), + ); } async getEnvironments( diff --git a/backend/src/core/users/dto.ts b/backend/src/core/users/dto.ts index 9609cc415..ddd9594c3 100644 --- a/backend/src/core/users/dto.ts +++ b/backend/src/core/users/dto.ts @@ -2,101 +2,85 @@ // because class-validator was experiencing issues at the time of implementation // and had many unaddressed github issues -import { OrgRole, OrgMember, User, Org } from '@pc/database/clients/core'; +import { Api } from '@pc/common/types/api'; import * as Joi from 'joi'; -// Composable Response DTOs - -export type OrgMemberData = Pick & { - user: Pick | { uid: null; email: string }; -}; - -export type OrgData = Pick & { isPersonal: boolean }; - -// Request DTOs - -const OrgRoleSchema = Joi.string().valid('ADMIN', 'COLLABORATOR'); +const OrgRoleSchema = Joi.alternatives('ADMIN', 'COLLABORATOR'); // create org -export interface CreateOrgDto { - name: string; -} -export const CreateOrgSchema = Joi.object({ +export const CreateOrgSchema = Joi.object< + Api.Mutation.Input<'/users/createOrg'>, + true +>({ name: Joi.string(), }); // invite to org -export interface InviteToOrgDto { - org: Org['slug']; - email: string; - role: OrgRole; -} -export const InviteToOrgSchema = Joi.object({ +export const InviteToOrgSchema = Joi.object< + Api.Mutation.Input<'/users/inviteToOrg'>, + true +>({ org: Joi.string(), email: Joi.string().email(), role: OrgRoleSchema, }); // accept org invite -export interface AcceptOrgInviteDto { - token: string; -} -export const AcceptOrgInviteSchema = Joi.object({ +export const AcceptOrgInviteSchema = Joi.object< + Api.Mutation.Input<'/users/acceptOrgInvite'>, + true +>({ token: Joi.string(), }); // remove org invite -export interface RemoveOrgInviteDto { - org: Org['slug']; - email: string; -} -export const RemoveOrgInviteSchema = Joi.object({ +export const RemoveOrgInviteSchema = Joi.object< + Api.Mutation.Input<'/users/removeOrgInvite'>, + true +>({ org: Joi.string(), email: Joi.string().email(), }); // remove from org -export interface RemoveFromOrgDto { - org: Org['slug']; - user: User['uid']; -} -export const RemoveFromOrgSchema = Joi.object({ +export const RemoveFromOrgSchema = Joi.object< + Api.Mutation.Input<'/users/removeFromOrg'>, + true +>({ org: Joi.string(), user: Joi.string(), }); // list org members -export interface ListOrgMembersDto { - org: Org['slug']; -} -export const ListOrgMembersSchema = Joi.object({ +export const ListOrgMembersSchema = Joi.object< + Api.Query.Input<'/users/listOrgMembers'>, + true +>({ org: Joi.string(), }); // delete org -export interface DeleteOrgDto { - org: Org['slug']; -} -export const DeleteOrgSchema = Joi.object({ +export const DeleteOrgSchema = Joi.object< + Api.Mutation.Input<'/users/deleteOrg'>, + true +>({ org: Joi.string(), }); // change org role -export interface ChangeOrgRoleDto { - org: Org['slug']; - role: OrgRole; - user: User['uid']; -} -export const ChangeOrgRoleSchema = Joi.object({ +export const ChangeOrgRoleSchema = Joi.object< + Api.Mutation.Input<'/users/changeOrgRole'>, + true +>({ org: Joi.string(), role: OrgRoleSchema, user: Joi.string(), }); // reset password -export interface ResetPasswordDto { - email: string; -} -export const ResetPasswordSchema = Joi.object({ +export const ResetPasswordSchema = Joi.object< + Api.Mutation.Input<'/users/resetPassword'>, + true +>({ email: Joi.string().required(), }); diff --git a/backend/src/core/users/users.controller.ts b/backend/src/core/users/users.controller.ts index 316fe3fd7..238e542f3 100644 --- a/backend/src/core/users/users.controller.ts +++ b/backend/src/core/users/users.controller.ts @@ -13,31 +13,20 @@ import { import { UsersService } from './users.service'; import { BearerAuthGuard } from '../auth/bearer-auth.guard'; import { - AcceptOrgInviteDto, AcceptOrgInviteSchema, - ChangeOrgRoleDto, ChangeOrgRoleSchema, - CreateOrgDto, CreateOrgSchema, - DeleteOrgDto, DeleteOrgSchema, - InviteToOrgDto, InviteToOrgSchema, - ListOrgMembersDto, ListOrgMembersSchema, - OrgData, - OrgMemberData, - RemoveFromOrgDto, RemoveFromOrgSchema, - RemoveOrgInviteDto, RemoveOrgInviteSchema, - ResetPasswordDto, ResetPasswordSchema, } from './dto'; import { JoiValidationPipe } from '@/src/pipes/JoiValidationPipe'; import { VError } from 'verror'; import { UserError } from './user-error'; -import { Org } from '@pc/database/clients/core'; +import { Api } from '@pc/common/types/api'; @Controller('users') export class UsersController { @@ -49,7 +38,10 @@ export class UsersController { // ! request @Post('getAccountDetails') @UseGuards(BearerAuthGuard) - async getAccountDetails(@Request() req) { + async getAccountDetails( + @Request() req, + @Body() _: Api.Query.Input<'/users/getAccountDetails'>, + ): Promise> { const { uid, email, name, photoUrl } = req.user; return { uid, email, name, photoUrl }; } @@ -57,7 +49,10 @@ export class UsersController { // Gets a list of orgs that this user is the sole admin of. @Post('listOrgsWithOnlyAdmin') @UseGuards(BearerAuthGuard) - async listOrgsWithOnlyAdmin(@Request() req) { + async listOrgsWithOnlyAdmin( + @Request() req, + @Body() _: Api.Query.Input<'/users/listOrgsWithOnlyAdmin'>, + ): Promise> { try { return await this.usersService.listOrgsWithOnlyAdmin(req.user.uid); } catch (e: any) { @@ -68,10 +63,13 @@ export class UsersController { @Post('deleteAccount') @HttpCode(204) @UseGuards(BearerAuthGuard) - async deleteAccount(@Request() req) { + async deleteAccount( + @Request() req, + @Body() _: Api.Mutation.Input<'/users/deleteAccount'>, + ): Promise> { const { uid } = req.user; try { - await this.usersService.deactivateUser(uid); + return await this.usersService.deactivateUser(uid); } catch (e: any) { throw mapError(e); } @@ -82,8 +80,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(CreateOrgSchema)) async create( @Request() req, - @Body() { name }: CreateOrgDto, - ): Promise { + @Body() { name }: Api.Mutation.Input<'/users/createOrg'>, + ): Promise> { try { return await this.usersService.createOrg(req.user, name.trim()); } catch (e: any) { @@ -97,8 +95,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(InviteToOrgSchema)) async inviteToOrg( @Request() req, - @Body() { org, email, role }: InviteToOrgDto, - ): Promise { + @Body() { org, email, role }: Api.Mutation.Input<'/users/inviteToOrg'>, + ): Promise> { try { return await this.usersService.inviteToOrg(req.user, org, email, role); } catch (e: any) { @@ -111,8 +109,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(AcceptOrgInviteSchema)) async acceptOrgInvite( @Request() req, - @Body() { token }: AcceptOrgInviteDto, - ): Promise<{ org: Pick }> { + @Body() { token }: Api.Mutation.Input<'/users/acceptOrgInvite'>, + ): Promise> { try { return await this.usersService.acceptOrgInvite(req.user, token); } catch (e: any) { @@ -125,8 +123,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(ListOrgMembersSchema)) async listOrgMembers( @Request() req, - @Body() { org }: ListOrgMembersDto, - ): Promise { + @Body() { org }: Api.Query.Input<'/users/listOrgMembers'>, + ): Promise> { try { return await this.usersService.listOrgMembers(req.user, org); } catch (e: any) { @@ -136,7 +134,10 @@ export class UsersController { @Post('listOrgs') @UseGuards(BearerAuthGuard) - async listOrgs(@Request() req): Promise { + async listOrgs( + @Request() req, + @Body() _: Api.Query.Input<'/users/listOrgs'>, + ): Promise> { try { return await this.usersService.listOrgs(req.user); } catch (e: any) { @@ -150,8 +151,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(DeleteOrgSchema)) async deleteOrg( @Request() req, - @Body() { org }: DeleteOrgDto, - ): Promise { + @Body() { org }: Api.Mutation.Input<'/users/deleteOrg'>, + ): Promise> { try { return await this.usersService.deleteOrg(req.user, org); } catch (e: any) { @@ -164,8 +165,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(ChangeOrgRoleSchema)) async changeOrgRole( @Request() req, - @Body() { org, user, role }: ChangeOrgRoleDto, - ): Promise { + @Body() { org, user, role }: Api.Mutation.Input<'/users/changeOrgRole'>, + ): Promise> { try { return await this.usersService.changeOrgRole(req.user, org, user, role); } catch (e: any) { @@ -178,8 +179,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(RemoveFromOrgSchema)) async removeFromOrg( @Request() req, - @Body() { org, user }: RemoveFromOrgDto, - ): Promise { + @Body() { org, user }: Api.Mutation.Input<'/users/removeFromOrg'>, + ): Promise> { try { return await this.usersService.removeFromOrg(req.user, org, user); } catch (e: any) { @@ -192,8 +193,8 @@ export class UsersController { @UsePipes(new JoiValidationPipe(RemoveOrgInviteSchema)) async removeOrgInvite( @Request() req, - @Body() { org, email }: RemoveOrgInviteDto, - ): Promise { + @Body() { org, email }: Api.Mutation.Input<'/users/removeOrgInvite'>, + ): Promise> { try { return await this.usersService.removeOrgInvite(req.user, org, email); } catch (e: any) { @@ -204,7 +205,9 @@ export class UsersController { @Post('resetPassword') @UsePipes(new JoiValidationPipe(ResetPasswordSchema)) @HttpCode(204) - async resetPassword(@Body() { email }: ResetPasswordDto) { + async resetPassword( + @Body() { email }: Api.Mutation.Input<'/users/resetPassword'>, + ): Promise> { try { await this.usersService.resetPassword(email); } catch (e: any) { diff --git a/backend/src/core/users/users.service.ts b/backend/src/core/users/users.service.ts index 6e6e13bf1..ac0c5220d 100644 --- a/backend/src/core/users/users.service.ts +++ b/backend/src/core/users/users.service.ts @@ -331,7 +331,7 @@ export class UsersService implements OnModuleInit { return { ...created, - isPersonal: false, + isPersonal: false as const, }; } @@ -605,9 +605,7 @@ export class UsersService implements OnModuleInit { ); } - return { - org, - }; + return org; } async removeFromOrg( @@ -706,32 +704,26 @@ export class UsersService implements OnModuleInit { // I'm not sure this is what we want but this ensures the latest invites are at the top // of the list, followed by accepted members of the org. + const existingMembers = members.map((member) => ({ + isInvite: false as const, + ...member, + })); + const nonExistingMembers = invites.map((i) => ({ + isInvite: true as const, + orgSlug: i.orgSlug, + role: i.role, + user: { + uid: null, + email: i.email, + }, + })); return ( [] as ( - | typeof members[number] - | { - isInvite: true; - orgSlug: string; - role: OrgRole; - user: { - uid: null; - email: string; - }; - } + | typeof existingMembers[number] + | typeof nonExistingMembers[number] )[] ) - .concat( - members, - invites.map((i) => ({ - isInvite: true, - orgSlug: i.orgSlug, - role: i.role, - user: { - uid: null, - email: i.email, - }, - })), - ) + .concat(existingMembers, nonExistingMembers) .reverse(); } diff --git a/backend/src/helpers.ts b/backend/src/helpers.ts index 70ca40757..4f390337a 100644 --- a/backend/src/helpers.ts +++ b/backend/src/helpers.ts @@ -1,3 +1,6 @@ -export function assertUnreachable(x: never): never { - throw new Error(`Unreachable Case: ${x}`); +export function assertUnreachable( + x: never, + extract?: (input: unknown) => string, +): never { + throw new Error(`Unreachable Case: ${extract ? extract(x) : x}`); } diff --git a/backend/src/modules/abi/abi.controller.ts b/backend/src/modules/abi/abi.controller.ts index 38c046773..52f704d30 100644 --- a/backend/src/modules/abi/abi.controller.ts +++ b/backend/src/modules/abi/abi.controller.ts @@ -11,13 +11,9 @@ import { UsePipes, } from '@nestjs/common'; import { VError } from 'verror'; +import { Api } from '@pc/common/types/api'; import { AbiService } from './abi.service'; -import { - AddContractAbiDto, - AddContractAbiSchema, - GetContractAbiDto, - GetContractAbiSchema, -} from './dto'; +import { AddContractAbiSchema, GetContractAbiSchema } from './dto'; @Controller('abi') export class AbiController { @@ -28,8 +24,8 @@ export class AbiController { @UsePipes(new JoiValidationPipe(AddContractAbiSchema)) async addContractAbi( @Request() req, - @Body() { contract, abi }: AddContractAbiDto, - ) { + @Body() { contract, abi }: Api.Mutation.Input<'/abi/addContractAbi'>, + ): Promise> { try { return await this.abi.addContractAbi(req.user, contract, abi); } catch (e: any) { @@ -42,8 +38,8 @@ export class AbiController { @UsePipes(new JoiValidationPipe(GetContractAbiSchema)) async getContractAbi( @Request() req, - @Body() { contract }: GetContractAbiDto, - ) { + @Body() { contract }: Api.Query.Input<'/abi/getContractAbi'>, + ): Promise> { try { return await this.abi.getContractAbi(req.user, contract); } catch (e: any) { diff --git a/backend/src/modules/abi/abi.service.ts b/backend/src/modules/abi/abi.service.ts index 6c9250454..afab6ee2d 100644 --- a/backend/src/modules/abi/abi.service.ts +++ b/backend/src/modules/abi/abi.service.ts @@ -1,7 +1,7 @@ import { Abi } from '@pc/database/clients/abi'; import { User } from '@pc/database/clients/core'; import { Injectable } from '@nestjs/common'; -import { ABI } from './abi'; +import type { AbiRoot } from 'near-abi-client-js'; import { PrismaService } from './prisma.service'; import { PermissionsService as ProjectPermissionsService } from '../../core/projects/permissions.service'; import { VError } from 'verror'; @@ -16,14 +16,14 @@ export class AbiService { async addContractAbi( user: User, contractSlug: Abi['contractSlug'], - abi: ABI, + abi: AbiRoot, ) { await this.projectPermissions.checkUserContractPermission( user.id, contractSlug, ); - let createdAbi; + let createdAbi: Abi; try { createdAbi = await this.prisma.abi.create({ data: { @@ -36,7 +36,10 @@ export class AbiService { throw new VError(e, 'Failed while creating abi'); } - return { contractSlug, abi: createdAbi.abi }; + return { + contractSlug, + abi: createdAbi.abi as unknown as AbiRoot, + }; } async getContractAbi(user: User, contractSlug: Abi['contractSlug']) { @@ -45,7 +48,7 @@ export class AbiService { contractSlug, ); - let abi; + let abi: Pick | null = null; try { abi = await this.prisma.abi.findFirst({ where: { @@ -75,6 +78,9 @@ export class AbiService { ); } - return abi; + return { + contractSlug: abi.contractSlug, + abi: abi.abi as unknown as AbiRoot, + }; } } diff --git a/backend/src/modules/abi/abi.ts b/backend/src/modules/abi/abi.ts deleted file mode 100644 index d548f6f9e..000000000 --- a/backend/src/modules/abi/abi.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copied from https://github.com/near/near-abi-client-js/blob/main/src/abi.ts -import { JSONSchema7 } from 'json-schema'; - -export interface ABI { - schema_version: string; - metadata?: ContractMetadata; - body: ABIData; -} - -export interface ABIData { - functions: ABIFunction[]; - /** Root JSON schema for the ABI */ - root_schema: JSONSchema7; -} - -export interface ContractMetadata { - name?: string; - version?: string; - authors?: string[]; -} - -export interface ABIFunction { - name: string; - is_view?: boolean; - is_init?: boolean; - is_payable?: boolean; - is_private?: boolean; - params?: ABIParameterInfo[]; - callbacks?: any[]; - callbacks_vec?: ABITypeInfo; - result?: ABITypeInfo; -} - -export interface ABITypeInfo { - type_schema: JSONSchema7; - serialization_type: string; -} - -export interface ABIParameterInfo extends ABITypeInfo { - name: string; -} diff --git a/backend/src/modules/abi/dto.ts b/backend/src/modules/abi/dto.ts index 152fee017..c01faa8e4 100644 --- a/backend/src/modules/abi/dto.ts +++ b/backend/src/modules/abi/dto.ts @@ -2,14 +2,10 @@ // because class-validator was experiencing issues at the time of implementation // and had many unaddressed github issues +import { Api } from '@pc/common/types/api'; import * as Joi from 'joi'; -import { ABI } from './abi'; // add contract abi -export interface AddContractAbiDto { - contract: string; - abi: ABI; -} const JsonSchemaSchema = Joi.object({}).unknown(true).required(); export const AbiSchema = Joi.object({ schema_version: Joi.string().required(), @@ -44,15 +40,18 @@ export const AbiSchema = Joi.object({ root_schema: JsonSchemaSchema, }).required(), }).required(); -export const AddContractAbiSchema = Joi.object({ +export const AddContractAbiSchema = Joi.object< + Api.Mutation.Input<'/abi/addContractAbi'>, + true +>({ contract: Joi.string().required(), abi: AbiSchema, }); // get contract abi -export interface GetContractAbiDto { - contract: string; -} -export const GetContractAbiSchema = Joi.object({ +export const GetContractAbiSchema = Joi.object< + Api.Query.Input<'/abi/getContractAbi'>, + true +>({ contract: Joi.string().required(), }); diff --git a/backend/src/modules/alerts/alerts.controller.ts b/backend/src/modules/alerts/alerts.controller.ts index 712e8cdb1..645e6c292 100644 --- a/backend/src/modules/alerts/alerts.controller.ts +++ b/backend/src/modules/alerts/alerts.controller.ts @@ -17,49 +17,27 @@ import { AppConfig } from 'src/config/validate'; import { assertUnreachable } from 'src/helpers'; import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; import { VError } from 'verror'; +import { AlertsService } from './alerts.service'; import { - AlertsService, - CreateAcctBalAlertSchema, - CreateEventAlertSchema, - CreateFnCallAlertSchema, - CreateTxAlertSchema, -} from './alerts.service'; -import { - CreateAlertDto, CreateAlertSchema, - DeleteAlertDto, DeleteAlertSchema, - ListAlertDto, ListAlertSchema, UpdateAlertSchema, - UpdateAlertDto, GetAlertDetailsSchema, - GetAlertDetailsDto, - ListDestinationDto, ListDestinationSchema, - DeleteDestinationDto, DeleteDestinationSchema, - AlertDetailsResponseDto, - CreateDestinationDto, CreateDestinationSchema, - DisableDestinationDto, DisableDestinationSchema, - EnableDestinationDto, EnableDestinationSchema, UpdateDestinationSchema, - UpdateDestinationDto, VerifyEmailSchema, - VerifyEmailDto, - ResendEmailVerificationDto, ResendEmailVerificationSchema, - UnsubscribeFromEmailAlertDto, UnsubscribeFromEmailAlertSchema, RotateWebhookDestinationSecretSchema, - RotateWebhookDestinationSecretDto, } from './dto'; import { TooManyRequestsException } from './exception/tooManyRequestsException'; import { TelegramService } from './telegram/telegram.service'; -import { TgUpdate } from './telegram/types'; +import { Api } from '@pc/common/types/api'; @Controller('alerts') export class AlertsController { @@ -81,39 +59,48 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(CreateAlertSchema)) async createAlert( @Request() req, - @Body() dto: CreateAlertDto, - ): Promise { + @Body() dto: Api.Mutation.Input<'/alerts/createAlert'>, + ): Promise> { try { - const ruleType = dto.type; - switch (ruleType) { + const { rule, ...rest } = dto; + switch (rule.type) { case 'TX_SUCCESS': return await this.alertsService.createTxSuccessAlert( req.user, - dto as CreateTxAlertSchema, + rest, + rule, ); case 'TX_FAILURE': return await this.alertsService.createTxFailureAlert( req.user, - dto as CreateTxAlertSchema, + rest, + rule, ); case 'FN_CALL': return await this.alertsService.createFnCallAlert( req.user, - dto as CreateFnCallAlertSchema, + rest, + rule, ); case 'EVENT': return await this.alertsService.createEventAlert( req.user, - dto as CreateEventAlertSchema, + rest, + rule, ); case 'ACCT_BAL_NUM': case 'ACCT_BAL_PCT': return await this.alertsService.createAcctBalAlert( req.user, - dto as CreateAcctBalAlertSchema, + rest, + rule, ); default: - assertUnreachable(ruleType); + assertUnreachable( + rule, + (rule) => + (rule as Api.Mutation.Input<'/alerts/createAlert'>['rule']).type, + ); } } catch (e: any) { throw mapError(e); @@ -125,8 +112,8 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(UpdateAlertSchema)) async updateAlert( @Request() req, - @Body() { id, name, isPaused }: UpdateAlertDto, - ): Promise { + @Body() { id, name, isPaused }: Api.Mutation.Input<'/alerts/updateAlert'>, + ): Promise> { try { return await this.alertsService.updateAlert(req.user, id, name, isPaused); } catch (e: any) { @@ -139,8 +126,9 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(ListAlertSchema)) async listAlerts( @Request() req, - @Body() { projectSlug, environmentSubId }: ListAlertDto, - ): Promise { + @Body() + { projectSlug, environmentSubId }: Api.Query.Input<'/alerts/listAlerts'>, + ): Promise> { try { return await this.alertsService.listAlerts( req.user, @@ -156,7 +144,10 @@ export class AlertsController { @HttpCode(204) @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(DeleteAlertSchema)) - async deleteAlert(@Request() req, @Body() { id }: DeleteAlertDto) { + async deleteAlert( + @Request() req, + @Body() { id }: Api.Mutation.Input<'/alerts/deleteAlert'>, + ): Promise> { try { return await this.alertsService.deleteAlert(req.user, id); } catch (e: any) { @@ -169,8 +160,8 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(GetAlertDetailsSchema)) async getAlertDetails( @Request() req, - @Body() { id }: GetAlertDetailsDto, - ): Promise { + @Body() { id }: Api.Query.Input<'/alerts/getAlertDetails'>, + ): Promise> { try { return await this.alertsService.getAlertDetails(req.user, id); } catch (e: any) { @@ -181,7 +172,10 @@ export class AlertsController { @Post('createDestination') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(CreateDestinationSchema)) - async createDestination(@Request() req, @Body() dto: CreateDestinationDto) { + async createDestination( + @Request() req, + @Body() dto: Api.Mutation.Input<'/alerts/createDestination'>, + ): Promise> { try { const type = dto.type; switch (type) { @@ -211,8 +205,8 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(DeleteDestinationSchema)) async deleteDestination( @Request() req, - @Body() { id }: DeleteDestinationDto, - ) { + @Body() { id }: Api.Mutation.Input<'/alerts/deleteDestination'>, + ): Promise> { try { return await this.alertsService.deleteDestination(req.user, id); } catch (e: any) { @@ -225,8 +219,8 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(ListDestinationSchema)) async listDestinations( @Request() req, - @Body() { projectSlug }: ListDestinationDto, - ) { + @Body() { projectSlug }: Api.Query.Input<'/alerts/listDestinations'>, + ): Promise> { try { return await this.alertsService.listDestinations(req.user, projectSlug); } catch (e: any) { @@ -240,8 +234,9 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(EnableDestinationSchema)) async enableDestination( @Request() req, - @Body() { alert, destination }: EnableDestinationDto, - ) { + @Body() + { alert, destination }: Api.Mutation.Input<'/alerts/enableDestination'>, + ): Promise> { try { return await this.alertsService.enableDestination( req.user, @@ -259,8 +254,9 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(DisableDestinationSchema)) async disableDestination( @Request() req, - @Body() { alert, destination }: DisableDestinationDto, - ) { + @Body() + { alert, destination }: Api.Mutation.Input<'/alerts/disableDestination'>, + ): Promise> { try { return await this.alertsService.disableDestination( req.user, @@ -275,7 +271,10 @@ export class AlertsController { @Post('updateDestination') @UseGuards(BearerAuthGuard) @UsePipes(new JoiValidationPipe(UpdateDestinationSchema)) - async updateDestination(@Request() req, @Body() dto: UpdateDestinationDto) { + async updateDestination( + @Request() req, + @Body() dto: Api.Mutation.Input<'/alerts/updateDestination'>, + ): Promise> { try { const type = dto.type; switch (type) { @@ -302,9 +301,11 @@ export class AlertsController { @Post('verifyEmailDestination') @HttpCode(204) @UsePipes(new JoiValidationPipe(VerifyEmailSchema)) - async verifyEmailDestination(@Body() { token }: VerifyEmailDto) { + async verifyEmailDestination( + @Body() { token }: Api.Mutation.Input<'/alerts/verifyEmailDestination'>, + ): Promise> { try { - await this.alertsService.verifyEmailDestination(token); + return await this.alertsService.verifyEmailDestination(token); } catch (e: any) { throw mapError(e); } @@ -315,8 +316,8 @@ export class AlertsController { @HttpCode(200) async start( @Headers('X-Telegram-Bot-Api-Secret-Token') secret: string, - @Body() body: TgUpdate, - ) { + @Body() body: Api.Mutation.Input<'/alerts/telegramWebhook'>, + ): Promise> { if (!this.tgEnableWebhook) { throw new ForbiddenException(); } @@ -382,10 +383,14 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(ResendEmailVerificationSchema)) async resendEmailVerification( @Request() req, - @Body() { destinationId }: ResendEmailVerificationDto, - ) { + @Body() + { destinationId }: Api.Mutation.Input<'/alerts/resendEmailVerification'>, + ): Promise> { try { - await this.alertsService.resendEmailVerification(req.user, destinationId); + return await this.alertsService.resendEmailVerification( + req.user, + destinationId, + ); } catch (e: any) { throw mapError(e); } @@ -395,10 +400,10 @@ export class AlertsController { @HttpCode(204) @UsePipes(new JoiValidationPipe(UnsubscribeFromEmailAlertSchema)) async unsubscribeFromEmailAlert( - @Body() { token }: UnsubscribeFromEmailAlertDto, - ) { + @Body() { token }: Api.Mutation.Input<'/alerts/unsubscribeFromEmailAlert'>, + ): Promise> { try { - await this.alertsService.unsubscribeFromEmailAlert(token); + return await this.alertsService.unsubscribeFromEmailAlert(token); } catch (e: any) { throw mapError(e); } @@ -410,8 +415,11 @@ export class AlertsController { @UsePipes(new JoiValidationPipe(RotateWebhookDestinationSecretSchema)) async rotateWebhookDestinationSecret( @Request() req, - @Body() { destinationId }: RotateWebhookDestinationSecretDto, - ) { + @Body() + { + destinationId, + }: Api.Mutation.Input<'/alerts/rotateWebhookDestinationSecret'>, + ): Promise> { try { return await this.alertsService.rotateWebhookDestinationSecret( req.user, diff --git a/backend/src/modules/alerts/alerts.service.ts b/backend/src/modules/alerts/alerts.service.ts index 9dc113203..9d1691500 100644 --- a/backend/src/modules/alerts/alerts.service.ts +++ b/backend/src/modules/alerts/alerts.service.ts @@ -2,13 +2,12 @@ import { Injectable } from '@nestjs/common'; import { Prisma, Alert, - WebhookDestination, AlertRuleKind, Destination, + ChainId, + WebhookDestination, EmailDestination, - DestinationType, TelegramDestination, - ChainId, } from '@pc/database/clients/alerts'; // TODO should we re-export these types from the core module? So there is no dependency on the core prisma/client @@ -20,11 +19,12 @@ import { customAlphabet } from 'nanoid'; import { PrismaService } from './prisma.service'; import { VError } from 'verror'; -import { PremapDestination, RuleType } from './types'; import { RuleSerializerService } from './serde/rule-serializer/rule-serializer.service'; import { RuleDeserializerService } from './serde/rule-deserializer/rule-deserializer.service'; import { AcctBalMatchingRule, + EventMatchingRule, + FnCallMatchingRule, MatchingRule, TxMatchingRule, } from './serde/db.types'; @@ -35,127 +35,7 @@ import { ConfigService } from '@nestjs/config'; import { DateTime } from 'luxon'; import { NearRpcService } from '@/src/core/near-rpc/near-rpc.service'; import { EmailVerificationService } from './email-verification.service'; - -type TxRuleSchema = { - rule: { - contract: string; - }; -}; - -type FnCallRuleSchema = { - rule: { - contract: string; - function: string; - }; -}; - -type EventRuleSchema = { - rule: { - contract: string; - standard: string; - version: string; - event: string; - }; -}; - -type AcctBalRuleSchema = { - type: 'ACCT_BAL_NUM' | 'ACCT_BAL_PCT'; - rule: { - contract: string; - from: string | null; - to: string | null; - }; -}; - -type CreateAlertBaseSchema = { - name: Alert['name']; - type: RuleType; - projectSlug: Alert['projectSlug']; - environmentSubId: Alert['environmentSubId']; - destinations?: Array; -}; -type RuleWithContractSchema = { - rule: { - contract: string; - }; -}; -export type CreateTxAlertSchema = CreateAlertBaseSchema & TxRuleSchema; -export type CreateFnCallAlertSchema = CreateAlertBaseSchema & FnCallRuleSchema; -export type CreateEventAlertSchema = CreateAlertBaseSchema & EventRuleSchema; -export type CreateAcctBalAlertSchema = CreateAlertBaseSchema & - AcctBalRuleSchema; - -type CreateWebhookDestinationSchema = { - name?: Destination['name']; - projectSlug: Destination['projectSlug']; - config: { - url: WebhookDestination['url']; - }; -}; - -type CreateWebhookDestinationResponse = { - id: WebhookDestination['id']; - name?: Destination['name']; - type: DestinationType; - projectSlug: Destination['projectSlug']; - config: { - url: WebhookDestination['url']; - secret: WebhookDestination['secret']; - }; -}; - -type CreateEmailDestinationSchema = { - name?: Destination['name']; - projectSlug: Destination['projectSlug']; - config: { - email: EmailDestination['email']; - }; -}; - -type CreateEmailDestinationResponse = { - id: EmailDestination['id']; - name?: Destination['name']; - type: DestinationType; - projectSlug: Destination['projectSlug']; - config: { - email: EmailDestination['email']; - isVerified: EmailDestination['isVerified']; - }; -}; - -type UpdateWebhookDestinationSchema = { - id: Destination['id']; - name?: Destination['name']; - config?: { - url?: WebhookDestination['url']; - }; -}; - -type UpdateEmailDestinationSchema = { - id: Destination['id']; - name?: Destination['name']; -}; - -type UpdateTelegramDestinationSchema = { - id: Destination['id']; - name?: Destination['name']; -}; - -type CreateTelegramDestinationSchema = { - name?: Destination['name']; - projectSlug: Destination['projectSlug']; -}; - -type CreateTelegramDestinationResponse = { - id: EmailDestination['id']; - name?: Destination['name']; - type: DestinationType; - projectSlug: Destination['projectSlug']; - config: { - startToken: TelegramDestination['startToken']; - chatTitle: TelegramDestination['chatTitle']; - }; -}; +import { Alerts } from '@pc/common/types/alerts'; const nanoid = customAlphabet( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', @@ -168,23 +48,16 @@ const nanoidLong = customAlphabet( ); type AlertWithDestinations = Alert & { - enabledDestinations: Array<{ - destination: { - id: Destination['id']; - name: Destination['name']; - type: Destination['type']; - webhookDestination: { - url: WebhookDestination['url']; - } | null; - emailDestination: { - email: EmailDestination['email']; - } | null; + enabledDestinations: { + destination: Pick & { + webhookDestination: Pick | null; + emailDestination: Pick | null; telegramDestination: Pick< TelegramDestination, 'startToken' | 'chatTitle' > | null; }; - }>; + }[]; }; @Injectable() @@ -224,80 +97,98 @@ export class AlertsService { )!; } - async createTxSuccessAlert(user: User, alert: CreateTxAlertSchema) { - const { contract } = alert.rule; + async createTxSuccessAlert( + user: User, + alert: Alerts.CreateAlertBaseInput, + rule: Alerts.TransactionRule, + ) { + const { contract } = rule; const defaultName = `Successful action in ${contract}`; alert = { ...alert, name: alert.name || defaultName, }; - const matchingRule = this.ruleSerializer.toTxSuccessJson(alert.rule); + const matchingRule = this.ruleSerializer.toTxSuccessJson(rule); - return this.createAlertRuleWithContract(user, alert, matchingRule); + return this.createAlertRuleWithContract(user, alert, rule, matchingRule); } - async createTxFailureAlert(user: User, alert: CreateTxAlertSchema) { - const { contract } = alert.rule; + async createTxFailureAlert( + user: User, + alert: Alerts.CreateAlertBaseInput, + rule: Alerts.TransactionRule, + ) { + const { contract } = rule; const defaultName = `Failed action in ${contract}`; alert = { ...alert, name: alert.name || defaultName, }; - const matchingRule = this.ruleSerializer.toTxFailureJson(alert.rule); + const matchingRule = this.ruleSerializer.toTxFailureJson(rule); - return this.createAlertRuleWithContract(user, alert, matchingRule); + return this.createAlertRuleWithContract(user, alert, rule, matchingRule); } - async createFnCallAlert(user: User, alert: CreateFnCallAlertSchema) { - const { contract } = alert.rule; - const defaultName = `Function ${alert.rule.function} called in ${contract}`; + async createFnCallAlert( + user: User, + alert: Alerts.CreateAlertBaseInput, + rule: Alerts.FunctionCallRule, + ) { + const { contract } = rule; + const defaultName = `Function ${rule.function} called in ${contract}`; alert = { ...alert, name: alert.name || defaultName, }; - const matchingRule = this.ruleSerializer.toFnCallJson(alert.rule); + const matchingRule = this.ruleSerializer.toFnCallJson(rule); - return this.createAlertRuleWithContract(user, alert, matchingRule); + return this.createAlertRuleWithContract(user, alert, rule, matchingRule); } - async createEventAlert(user: User, alert: CreateEventAlertSchema) { - const { event, contract } = alert.rule; + async createEventAlert( + user: User, + alert: Alerts.CreateAlertBaseInput, + rule: Alerts.EventRule, + ) { + const { event, contract } = rule; const defaultName = `Event ${event} logged in ${contract}`; alert = { ...alert, name: alert.name || defaultName, }; - const matchingRule = this.ruleSerializer.toEventJson(alert.rule); + const matchingRule = this.ruleSerializer.toEventJson(rule); - return this.createAlertRuleWithContract(user, alert, matchingRule); + return this.createAlertRuleWithContract(user, alert, rule, matchingRule); } - async createAcctBalAlert(user: User, alert: CreateAcctBalAlertSchema) { - const { contract } = alert.rule; + async createAcctBalAlert( + user: User, + alert: Alerts.CreateAlertBaseInput, + rule: Alerts.AcctBalPctRule | Alerts.AcctBalNumRule, + ) { + const { contract } = rule; const defaultName = `Account balance changed in ${contract}`; alert = { ...alert, name: alert.name || defaultName, }; - const matchingRule = this.ruleSerializer.toAcctBalJson( - alert.rule, - alert.type, - ); + const matchingRule = this.ruleSerializer.toAcctBalJson(rule); - return this.createAlertRuleWithContract(user, alert, matchingRule); + return this.createAlertRuleWithContract(user, alert, rule, matchingRule); } private async createAlertRuleWithContract( user: User, - alert: CreateAlertBaseSchema & RuleWithContractSchema, + alert: Alerts.CreateAlertBaseInput, + rule: Alerts.Rule, matchingRule: MatchingRule, ) { const chainId = await this.projects.getEnvironmentNet( alert.projectSlug, alert.environmentSubId, ); - const address = alert.rule.contract; + const address = rule.contract; await Promise.all([ this.checkUserCreateAlertPermission(user, alert), @@ -343,14 +234,14 @@ export class AlertsService { private async buildCreateAlertInput( user: User, chainId: ChainId, - alert: CreateAlertBaseSchema, + alert: Alerts.CreateAlertBaseInput, alertRuleKind: AlertRuleKind, matchingRule, ): Promise { const { name, projectSlug, environmentSubId, destinations } = alert; const alertInput: Prisma.AlertCreateInput = { - name, + name: name!, alertRuleKind, matchingRule, projectSlug, @@ -565,7 +456,7 @@ export class AlertsService { } } - private toAlertDto(alert: AlertWithDestinations) { + private toAlertDto(alert: AlertWithDestinations): Alerts.Alert { const { id, name, @@ -578,7 +469,6 @@ export class AlertsService { const rule = matchingRule as object as MatchingRule; return { id, - type: this.toAlertType(rule), name, isPaused, projectSlug, @@ -617,18 +507,18 @@ export class AlertsService { }; } - public toAlertType(rule: MatchingRule): RuleType { - if ( - rule.rule === 'ACTION_ANY' && - (rule as TxMatchingRule).status === 'SUCCESS' - ) { + public toAlertType( + rule: + | TxMatchingRule + | FnCallMatchingRule + | EventMatchingRule + | AcctBalMatchingRule, + ): Alerts.RuleType { + if (rule.rule === 'ACTION_ANY' && rule.status === 'SUCCESS') { return 'TX_SUCCESS'; } - if ( - rule.rule === 'ACTION_ANY' && - (rule as TxMatchingRule).status === 'FAIL' - ) { + if (rule.rule === 'ACTION_ANY' && rule.status === 'FAIL') { return 'TX_FAILURE'; } @@ -641,8 +531,7 @@ export class AlertsService { } if (rule.rule === 'STATE_CHANGE_ACCOUNT_BALANCE') { - return (rule as AcctBalMatchingRule).comparator_kind === - 'RELATIVE_PERCENTAGE_AMOUNT' + return rule.comparator_kind === 'RELATIVE_PERCENTAGE_AMOUNT' ? 'ACCT_BAL_PCT' : 'ACCT_BAL_NUM'; } @@ -677,47 +566,51 @@ export class AlertsService { name = 'Webhook Destination', config: { url }, projectSlug, - }: CreateWebhookDestinationSchema, - ): Promise { + }: Alerts.CreateWebhookDestinationInput, + ) { await this.projectPermissions.checkUserProjectPermission( user.id, projectSlug, ); try { - const res = await this.prisma.destination.create({ - data: { - name, - projectSlug, - type: 'WEBHOOK', - isValid: true, - createdBy: user.id, - updatedBy: user.id, - webhookDestination: { - create: { - secret: nanoid(), - createdBy: user.id, - updatedBy: user.id, - url, + const { webhookDestination, ...destination } = + await this.prisma.destination.create({ + data: { + name, + projectSlug, + type: 'WEBHOOK', + isValid: true, + createdBy: user.id, + updatedBy: user.id, + webhookDestination: { + create: { + secret: nanoid(), + createdBy: user.id, + updatedBy: user.id, + url, + }, }, }, - }, - select: { - id: true, - name: true, - type: true, - projectSlug: true, - isValid: true, - webhookDestination: { - select: { - url: true, - secret: true, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + webhookDestination: { + select: { + url: true, + secret: true, + }, }, }, - }, - }); + }); - return this.transformDestinationRes(res); + return { + ...destination, + type: 'WEBHOOK' as const, + config: webhookDestination!, + }; } catch (e: any) { throw new VError(e, 'Failed to create webhook destination'); } @@ -729,88 +622,87 @@ export class AlertsService { name = 'Email Destination', config: { email }, projectSlug, - }: CreateEmailDestinationSchema, - ): Promise { + }: Alerts.CreateEmailDestinationInput, + ) { await this.projectPermissions.checkUserProjectPermission( user.id, projectSlug, ); - let res; - try { const expiryDate = this.calculateExpiryDate(this.emailTokenExpiryMin); const token = nanoid(); const tokenCreatedAt = DateTime.now().toUTC().toJSDate(); - res = await this.prisma.destination.create({ - data: { - name, - projectSlug, - type: 'EMAIL', - createdBy: user.id, - updatedBy: user.id, - emailDestination: { - create: { - email, - createdBy: user.id, - updatedBy: user.id, - isVerified: false, - tokenCreatedAt, - tokenExpiresAt: expiryDate, - token, + const { emailDestination, ...destination } = + await this.prisma.destination.create({ + data: { + name, + projectSlug, + type: 'EMAIL', + createdBy: user.id, + updatedBy: user.id, + emailDestination: { + create: { + email, + createdBy: user.id, + updatedBy: user.id, + isVerified: false, + tokenCreatedAt, + tokenExpiresAt: expiryDate, + token, + }, }, }, - }, - select: { - id: true, - name: true, - type: true, - projectSlug: true, - isValid: true, - emailDestination: { - select: { - id: true, - email: true, - isVerified: true, - token: true, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + emailDestination: { + select: { + id: true, + email: true, + isVerified: true, + token: true, + }, }, }, - }, - }); - } catch (e: any) { - throw new VError(e, 'Failed to create email destination'); - } + }); - try { - await this.emailVerification.sendVerificationEmail( - email, - res.emailDestination.token, - ); - } catch (e: any) { try { - await this.prisma.$transaction([ - this.prisma.emailDestination.delete({ - where: { - id: res.emailDestination.id, - }, - }), - this.prisma.destination.delete({ - where: { - id: res.id, - }, - }), - ]); + await this.emailVerification.sendVerificationEmail(email, token); + + return { + ...destination, + type: 'EMAIL' as const, + config: emailDestination!, + }; } catch (e: any) { - console.error( - 'Failed while rolling back email destination creation', - e, - ); - } + try { + await this.prisma.$transaction([ + this.prisma.emailDestination.delete({ + where: { + id: emailDestination!.id, + }, + }), + this.prisma.destination.delete({ + where: { + id: destination.id, + }, + }), + ]); + } catch (e) { + console.error( + 'Failed while rolling back email destination creation', + e, + ); + } - throw new VError(e, 'Failed to send an email verification message'); + throw new VError(e, 'Failed to send an email verification message'); + } + } catch (e: any) { + throw new VError(e, 'Failed to create email destination'); } - - return this.transformDestinationRes(res); } async createTelegramDestination( @@ -818,46 +710,50 @@ export class AlertsService { { name = 'Telegram Destination', projectSlug, - }: CreateTelegramDestinationSchema, - ): Promise { + }: Alerts.CreateTelegramDestinationInput, + ) { await this.projectPermissions.checkUserProjectPermission( user.id, projectSlug, ); try { const expiryDate = this.calculateExpiryDate(this.telegramTokenExpiryMin); - const res = await this.prisma.destination.create({ - data: { - name, - projectSlug, - type: 'TELEGRAM', - createdBy: user.id, - updatedBy: user.id, - telegramDestination: { - create: { - startToken: nanoid(), - tokenExpiresAt: expiryDate, - createdBy: user.id, - updatedBy: user.id, + const { telegramDestination, ...destination } = + await this.prisma.destination.create({ + data: { + name, + projectSlug, + type: 'TELEGRAM', + createdBy: user.id, + updatedBy: user.id, + telegramDestination: { + create: { + startToken: nanoid(), + tokenExpiresAt: expiryDate, + createdBy: user.id, + updatedBy: user.id, + }, }, }, - }, - select: { - id: true, - name: true, - type: true, - projectSlug: true, - isValid: true, - telegramDestination: { - select: { - startToken: true, - chatTitle: true, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + telegramDestination: { + select: { + startToken: true, + chatTitle: true, + }, }, }, - }, - }); + }); - return this.transformDestinationRes(res); + return { + ...destination, + type: 'TELEGRAM' as const, + config: telegramDestination!, + }; } catch (e: any) { throw new VError(e, 'Failed to create telegram destination'); } @@ -925,7 +821,26 @@ export class AlertsService { }, }); - return destinations.map(this.transformDestinationRes); + return destinations.map( + ({ + type, + webhookDestination, + emailDestination, + telegramDestination, + ...rest + }) => { + switch (type) { + case 'WEBHOOK': + return { ...rest, type, config: webhookDestination! }; + case 'EMAIL': + return { ...rest, type, config: emailDestination! }; + case 'TELEGRAM': + return { ...rest, type, config: telegramDestination! }; + default: + assertUnreachable(type); + } + }, + ); } async enableDestination( @@ -1001,41 +916,45 @@ export class AlertsService { async updateWebhookDestination( callingUser: User, - dto: UpdateWebhookDestinationSchema, + dto: Alerts.UpdateWebhookDestinationInput, ) { const { id, name, config } = dto; await this.checkUserDestinationPermission(callingUser.id, id); try { - const res = await this.prisma.destination.update({ - where: { - id, - }, - data: { - name, - updatedBy: callingUser.id, - webhookDestination: { - update: { - url: config?.url, - updatedBy: callingUser.id, + const { webhookDestination, ...destination } = + await this.prisma.destination.update({ + where: { + id, + }, + data: { + name, + updatedBy: callingUser.id, + webhookDestination: { + update: { + url: config.url, + updatedBy: callingUser.id, + }, }, }, - }, - select: { - id: true, - name: true, - type: true, - projectSlug: true, - isValid: true, - webhookDestination: { - select: { - url: true, - secret: true, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + webhookDestination: { + select: { + url: true, + secret: true, + }, }, }, - }, - }); - return this.transformDestinationRes(res); + }); + return { + ...destination, + type: 'WEBHOOK' as const, + config: webhookDestination!, + }; } catch (e: any) { throw new VError(e, 'Failed while updating webhook destination'); } @@ -1043,34 +962,38 @@ export class AlertsService { async updateEmailDestination( callingUser: User, - dto: UpdateEmailDestinationSchema, + dto: Alerts.UpdateEmailDestinationInput, ) { const { id, name } = dto; await this.checkUserDestinationPermission(callingUser.id, id); try { - const res = await this.prisma.destination.update({ - where: { - id, - }, - data: { - name, - updatedBy: callingUser.id, - }, - select: { - id: true, - name: true, - type: true, - projectSlug: true, - isValid: true, - emailDestination: { - select: { - email: true, + const { emailDestination, ...destination } = + await this.prisma.destination.update({ + where: { + id, + }, + data: { + name, + updatedBy: callingUser.id, + }, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + emailDestination: { + select: { + email: true, + }, }, }, - }, - }); - return this.transformDestinationRes(res); + }); + return { + ...destination, + type: 'EMAIL' as const, + config: emailDestination!, + }; } catch (e: any) { throw new VError(e, 'Failed while updating email destination'); } @@ -1078,35 +1001,39 @@ export class AlertsService { async updateTelegramDestination( callingUser: User, - dto: UpdateTelegramDestinationSchema, + dto: Alerts.UpdateTelegramDestinationInput, ) { const { id, name } = dto; await this.checkUserDestinationPermission(callingUser.id, id); try { - const res = await this.prisma.destination.update({ - where: { - id, - }, - data: { - name, - updatedBy: callingUser.id, - }, - select: { - id: true, - name: true, - type: true, - projectSlug: true, - isValid: true, - telegramDestination: { - select: { - startToken: true, - chatTitle: true, + const { telegramDestination, ...destination } = + await this.prisma.destination.update({ + where: { + id, + }, + data: { + name, + updatedBy: callingUser.id, + }, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + telegramDestination: { + select: { + startToken: true, + chatTitle: true, + }, }, }, - }, - }); - return this.transformDestinationRes(res); + }); + return { + ...destination, + type: 'TELEGRAM' as const, + config: telegramDestination!, + }; } catch (e: any) { throw new VError(e, 'Failed while updating telegram destination'); } @@ -1314,34 +1241,38 @@ export class AlertsService { ); } - const result = await this.prisma.destination.update({ - where: { - id, - }, - data: { - webhookDestination: { - update: { - secret: nanoid(), - updatedBy: callingUser.id, + const { webhookDestination, ...destination } = + await this.prisma.destination.update({ + where: { + id, + }, + data: { + webhookDestination: { + update: { + secret: nanoid(), + updatedBy: callingUser.id, + }, }, }, - }, - select: { - id: true, - type: true, - name: true, - projectSlug: true, - isValid: true, - webhookDestination: { - select: { - secret: true, - url: true, + select: { + id: true, + name: true, + projectSlug: true, + isValid: true, + webhookDestination: { + select: { + secret: true, + url: true, + }, }, }, - }, - }); + }); - return this.transformDestinationRes(result); + return { + ...destination, + type: 'WEBHOOK' as const, + config: webhookDestination!, + }; } catch (e: any) { throw new VError(e, 'Failed while rotating webhook destination secret'); } @@ -1482,7 +1413,7 @@ export class AlertsService { // Checks user permission to create alert as well as permission for webhook destinations private async checkUserCreateAlertPermission( user: User, - alert: CreateAlertBaseSchema, + alert: Alerts.CreateAlertBaseInput, ) { // Verify the user has access to this project in order to create the alert. await this.projectPermissions.checkUserProjectEnvPermission( @@ -1541,32 +1472,6 @@ export class AlertsService { } } - private transformDestinationRes(destination: PremapDestination) { - const { id, name, projectSlug, type, isValid } = destination; - let config; - switch (type) { - case 'WEBHOOK': - config = destination.webhookDestination; - break; - case 'EMAIL': - config = destination.emailDestination; - break; - case 'TELEGRAM': - config = destination.telegramDestination; - break; - default: - assertUnreachable(type); - } - return { - id, - name, - projectSlug, - type, - isValid, - config, - }; - } - private calculateExpiryDate(expiryMin: number): Date { return DateTime.now().plus({ minutes: expiryMin }).toUTC().toJSDate(); } diff --git a/backend/src/modules/alerts/dto.spec.ts b/backend/src/modules/alerts/dto.spec.ts index 0018cb2f9..d995a65a5 100644 --- a/backend/src/modules/alerts/dto.spec.ts +++ b/backend/src/modules/alerts/dto.spec.ts @@ -5,67 +5,105 @@ const projectSlug = '123xyz'; const environmentSubId = 1; test.each([ - { type: 'TX_SUCCESS', projectSlug, environmentSubId, rule: { contract } }, - { type: 'TX_FAILURE', projectSlug, environmentSubId, rule: { contract } }, + { projectSlug, environmentSubId, rule: { type: 'TX_SUCCESS', contract } }, + { projectSlug, environmentSubId, rule: { type: 'TX_FAILURE', contract } }, { - type: 'FN_CALL', projectSlug, environmentSubId, - rule: { contract, function: '*' }, + rule: { + type: 'FN_CALL', + contract, + function: '*', + }, }, { - type: 'EVENT', projectSlug, environmentSubId, - rule: { contract, standard: '*', event: '*', version: '*' }, + rule: { + type: 'EVENT', + contract, + standard: '*', + event: '*', + version: '*', + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract, from: null, to: '34028236692463463374607000000' }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + from: null, + to: '34028236692463463374607000000', + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract, from: '340283463374607000000', to: null }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + from: '340283463374607000000', + to: null, + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: '10', to: null }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: '10', + to: null, + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: null, to: '100' }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: null, + to: '100', + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract, from: '0', to: '0' }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + from: '0', + to: '0', + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: '0', to: '0' }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: '0', + to: '0', + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: '0' }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: '0', + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, to: '0' }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + to: '0', + }, }, ])('%o should be valid', (input) => { const result = CreateAlertSchema.validate(input); @@ -73,74 +111,104 @@ test.each([ }); test.each([ - { type: 'INCORRECT', projectSlug, environmentSubId, rule: { contract } }, + { projectSlug, environmentSubId, rule: { type: 'INCORRECT', contract } }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract, from: null, to: null }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + from: null, + to: null, + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract, from: '-1', to: null }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + from: '-1', + to: null, + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, - rule: { contract, from: '3', to: '1' }, + rule: { + type: 'ACCT_BAL_NUM', + contract, + from: '3', + to: '1', + }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, rule: { + type: 'ACCT_BAL_NUM', contract, from: '340282366920938463463374607431768211456', to: null, }, }, { - type: 'ACCT_BAL_NUM', projectSlug, environmentSubId, rule: { + type: 'ACCT_BAL_NUM', contract, from: null, to: '340282366920938463463374607431768211456', }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: null, to: null }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: null, + to: null, + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: '101', to: null }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: '101', + to: null, + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: '0', to: '101' }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: '0', + to: '101', + }, }, { - type: 'ACCT_BAL_PCT', projectSlug, environmentSubId, - rule: { contract, from: '-1', to: null }, + rule: { + type: 'ACCT_BAL_PCT', + contract, + from: '-1', + to: null, + }, }, ])('%o should throw errors', (input) => { const result = CreateAlertSchema.validate(input); diff --git a/backend/src/modules/alerts/dto.ts b/backend/src/modules/alerts/dto.ts index 7277cfe2e..8be388764 100644 --- a/backend/src/modules/alerts/dto.ts +++ b/backend/src/modules/alerts/dto.ts @@ -1,25 +1,22 @@ // Note: we use Joi instead of Nest's default recommendation of class-validator // because class-validator was experiencing issues at the time of implementation // and had many unaddressed github issues -import { DestinationType } from '@pc/database/clients/alerts'; import * as Joi from 'joi'; -import { - AcctBalRuleDto, - EventRuleDto, - FnCallRuleDto, - RuleType, - TxRuleDto, -} from './serde/dto.types'; +import { Api } from '@pc/common/types/api'; +import { Alerts } from '@pc/common/types/alerts'; -const TxRuleSchema = Joi.object({ +const TxRuleSchema = Joi.object({ + type: Joi.alternatives('TX_SUCCESS', 'TX_FAILURE'), contract: Joi.string().required(), }); -const FnCallRuleSchema = Joi.object({ +const FnCallRuleSchema = Joi.object({ + type: Joi.string().valid('FN_CALL'), contract: Joi.string().required(), function: Joi.string().required(), // params: Joi.object(), }); -const EventRuleSchema = Joi.object({ +const EventRuleSchema = Joi.object({ + type: Joi.string().valid('EVENT'), contract: Joi.string().required(), standard: Joi.string().required(), version: Joi.string().required(), @@ -37,7 +34,8 @@ const validateRange = (value, { original: rule }) => { return value; }; -const AcctBalPctRuleSchema = Joi.object({ +const AcctBalPctRuleSchema = Joi.object({ + type: Joi.string().valid('ACCT_BAL_PCT'), contract: Joi.string().required(), from: Joi.string() .empty(null) @@ -60,7 +58,8 @@ const validateYoctonearAmount = (value, _) => { return value; }; -const AcctBalNumRuleSchema = Joi.object({ +const AcctBalNumRuleSchema = Joi.object({ + type: Joi.string().valid('ACCT_BAL_NUM'), contract: Joi.string().required(), from: Joi.string() .empty(null) @@ -81,51 +80,16 @@ const AcctBalNumRuleSchema = Joi.object({ }).custom(validateRange, 'Validating range'); // create alert -interface CreateAlertBaseDto { - name?: string; - type: RuleType; - projectSlug: string; - environmentSubId: number; - destinations?: Array; -} -export interface CreateTxAlertDto extends CreateAlertBaseDto { - type: 'TX_SUCCESS' | 'TX_FAILURE'; - rule: TxRuleDto; -} -export interface CreateFnCallAlertDto extends CreateAlertBaseDto { - type: 'FN_CALL'; - rule: FnCallRuleDto; -} -export interface CreateEventAlertDto extends CreateAlertBaseDto { - type: 'EVENT'; - rule: EventRuleDto; -} -export interface CreateAcctBalAlertDto extends CreateAlertBaseDto { - type: 'ACCT_BAL_NUM' | 'ACCT_BAL_PCT'; - rule: AcctBalRuleDto; -} -export type CreateAlertDto = - | CreateTxAlertDto - | CreateFnCallAlertDto - | CreateEventAlertDto - | CreateAcctBalAlertDto; -export const CreateAlertSchema = Joi.object({ +export const CreateAlertSchema = Joi.object< + Api.Mutation.Input<'/alerts/createAlert'>, + true +>({ name: Joi.string(), - type: Joi.string() - .valid( - 'TX_SUCCESS', - 'TX_FAILURE', - 'FN_CALL', - 'EVENT', - 'ACCT_BAL_PCT', - 'ACCT_BAL_NUM', - ) - .required(), projectSlug: Joi.string().required(), environmentSubId: Joi.number().required(), destinations: Joi.array().items(Joi.number()).optional(), rule: Joi.alternatives() - .conditional('type', { + .conditional('rule.type', { switch: [ { is: 'TX_SUCCESS', then: TxRuleSchema }, { is: 'TX_FAILURE', then: TxRuleSchema }, @@ -141,199 +105,142 @@ export const CreateAlertSchema = Joi.object({ }, ], }) - .required(), + // TODO: fix any + .required() as any, }); // update alert -export interface UpdateAlertDto { - id: number; - name?: string; - isPaused?: boolean; -} -export const UpdateAlertSchema = Joi.object({ +export const UpdateAlertSchema = Joi.object< + Api.Mutation.Input<'/alerts/updateAlert'>, + true +>({ id: Joi.number().required(), name: Joi.string(), - isPaused: Joi.boolean(), + // see https://github.com/hapijs/joi/issues/2848 + isPaused: Joi.boolean().optional() as unknown as Joi.AlternativesSchema, }); // list alerts -export interface ListAlertDto { - projectSlug: string; - environmentSubId: number; -} -export const ListAlertSchema = Joi.object({ +export const ListAlertSchema = Joi.object< + Api.Query.Input<'/alerts/listAlerts'>, + true +>({ projectSlug: Joi.string().required(), environmentSubId: Joi.number().required(), }); // delete alert -export interface DeleteAlertDto { - id: number; -} -export const DeleteAlertSchema = Joi.object({ +export const DeleteAlertSchema = Joi.object< + Api.Mutation.Input<'/alerts/deleteAlert'>, + true +>({ id: Joi.number().required(), }); // get alert details -export interface GetAlertDetailsDto { - id: number; -} -export const GetAlertDetailsSchema = Joi.object({ +export const GetAlertDetailsSchema = Joi.object< + Api.Query.Input<'/alerts/getAlertDetails'>, + true +>({ id: Joi.number().required(), }); -interface WebhookDestinationResponseDto { - url: string; -} -export interface AlertDetailsResponseDto { - id: number; - type: RuleType; - name: string | null; - isPaused: boolean; - projectSlug: string; - environmentSubId: number; - rule: TxRuleDto | FnCallRuleDto | EventRuleDto | AcctBalRuleDto; - enabledDestinations: Array<{ - id: number; - name: string | null; - type: DestinationType; - config: WebhookDestinationResponseDto; - }>; -} - -interface CreateBaseDestinationDto { - name?: string; - type: DestinationType; - projectSlug: string; -} -interface CreateWebhookDestinationDto extends CreateBaseDestinationDto { - type: 'WEBHOOK'; - config: { - url: string; - }; -} -interface CreateEmailDestinationDto extends CreateBaseDestinationDto { - type: 'EMAIL'; - config: { - email: string; - }; -} -interface CreateTelegramDestinationDto extends CreateBaseDestinationDto { - type: 'TELEGRAM'; - config?: Record; // eslint recommended typing for empty object -} - -export type CreateDestinationDto = - | CreateWebhookDestinationDto - | CreateEmailDestinationDto - | CreateTelegramDestinationDto; - -const WebhookDestinationSchema = Joi.object({ +const WebhookDestinationSchema = Joi.object< + Alerts.CreateWebhookDestinationInput['config'], + true +>({ url: Joi.string().required(), }); -const EmailDestinationSchema = Joi.object({ +const EmailDestinationSchema = Joi.object< + Alerts.CreateEmailDestinationInput['config'], + true +>({ email: Joi.string().required(), }); -const TelegramDestinationSchema = Joi.object({}); -export const CreateDestinationSchema = Joi.object({ +const TelegramDestinationSchema = Joi.object< + Alerts.CreateTelegramDestinationInput['config'], + true +>({}); +export const CreateDestinationSchema = Joi.object< + Api.Mutation.Input<'/alerts/createDestination'>, + true +>({ name: Joi.string(), type: Joi.string().valid('WEBHOOK', 'EMAIL', 'TELEGRAM').required(), projectSlug: Joi.string().required(), config: Joi.alternatives() - .conditional('type', { + .conditional('config.type', { switch: [ { is: 'WEBHOOK', then: WebhookDestinationSchema }, { is: 'EMAIL', then: EmailDestinationSchema }, { is: 'TELEGRAM', then: TelegramDestinationSchema }, ], }) - .required(), + // TODO: fix any + .required() as any, }); // delete destinations -export interface DeleteDestinationDto { - id: number; -} -export const DeleteDestinationSchema = Joi.object({ +export const DeleteDestinationSchema = Joi.object< + Api.Mutation.Input<'/alerts/deleteDestination'>, + true +>({ id: Joi.number().required(), }); // list destinations -export interface ListDestinationDto { - projectSlug: string; -} -export const ListDestinationSchema = Joi.object({ +export const ListDestinationSchema = Joi.object< + Api.Query.Input<'/alerts/listDestinations'>, + true +>({ projectSlug: Joi.string().required(), }); // enable destination -export interface EnableDestinationDto { - alert: number; - destination: number; -} -export const EnableDestinationSchema = Joi.object({ +export const EnableDestinationSchema = Joi.object< + Api.Mutation.Input<'/alerts/enableDestination'>, + true +>({ alert: Joi.number().required(), destination: Joi.number().required(), }); // disable destination -export type DisableDestinationDto = EnableDestinationDto; export const DisableDestinationSchema = EnableDestinationSchema; // update destination -export interface UpdateDestinationBaseDto { - id: number; - type: DestinationType; - name?: string; -} -export interface UpdateWebhookDestinationDto extends UpdateDestinationBaseDto { - type: 'WEBHOOK'; - config?: { - url?: string; - }; -} -export interface UpdateEmailDestinationDto extends UpdateDestinationBaseDto { - type: 'EMAIL'; -} -export interface UpdateTelegramDestinationDto extends UpdateDestinationBaseDto { - type: 'TELEGRAM'; -} - -export type UpdateDestinationDto = - | UpdateWebhookDestinationDto - | UpdateEmailDestinationDto - | UpdateTelegramDestinationDto; - -const UpdateWebhookDestinationSchema = Joi.object({ +const UpdateWebhookDestinationSchema = Joi.object< + Alerts.UpdateWebhookDestinationInput['config'], + true +>({ url: Joi.string(), }); -export const UpdateDestinationSchema = Joi.object({ +export const UpdateDestinationSchema = Joi.object< + Api.Mutation.Input<'/alerts/updateDestination'>, + true +>({ id: Joi.number().required(), type: Joi.string().required(), name: Joi.string(), - config: Joi.alternatives().conditional('type', { + config: Joi.alternatives().conditional('config.type', { switch: [{ is: 'WEBHOOK', then: UpdateWebhookDestinationSchema }], - }), + // TODO: fix any + }) as any, }); // verify email -export interface VerifyEmailDto { - token: string; -} -export const VerifyEmailSchema = Joi.object({ +export const VerifyEmailSchema = Joi.object< + Api.Mutation.Input<'/alerts/verifyEmailDestination'>, + true +>({ token: Joi.string().required(), }); // Triggered Alerts -export interface ListTriggeredAlertDto { - projectSlug: string; - environmentSubId: number; - skip?: number; - take?: number; - pagingDateTime?: Date; - alertId?: number; -} -export const ListTriggeredAlertSchema = Joi.object({ +export const ListTriggeredAlertSchema = Joi.object< + Api.Query.Input<'/triggeredAlerts/listTriggeredAlerts'>, + true +>({ projectSlug: Joi.string().required(), environmentSubId: Joi.number().required(), skip: Joi.number().integer().min(0).optional(), @@ -342,49 +249,33 @@ export const ListTriggeredAlertSchema = Joi.object({ alertId: Joi.number().integer().positive().optional(), }); -export interface GetTriggeredAlertDetailsDto { - slug: string; -} -export const GetTriggeredAlertDetailsSchema = Joi.object({ +export const GetTriggeredAlertDetailsSchema = Joi.object< + Api.Query.Input<'/triggeredAlerts/getTriggeredAlertDetails'>, + true +>({ slug: Joi.string().required(), }); -export interface TriggeredAlertsResponseDto { - count: number; - page: Array; -} -export interface TriggeredAlertDetailsResponseDto { - slug: string; - alertId: number; - name: string; - type: RuleType; - triggeredInBlockHash: string; - triggeredInTransactionHash: string | null; - triggeredInReceiptId: string | null; - triggeredAt: Date; - extraData?: Record; -} - // resend verification email -export interface ResendEmailVerificationDto { - destinationId: number; -} -export const ResendEmailVerificationSchema = Joi.object({ +export const ResendEmailVerificationSchema = Joi.object< + Api.Mutation.Input<'/alerts/resendEmailVerification'>, + true +>({ destinationId: Joi.number().required(), }); // unsubscribe from alerts email -export interface UnsubscribeFromEmailAlertDto { - token: string; -} -export const UnsubscribeFromEmailAlertSchema = Joi.object({ +export const UnsubscribeFromEmailAlertSchema = Joi.object< + Api.Mutation.Input<'/alerts/unsubscribeFromEmailAlert'>, + true +>({ token: Joi.string().required(), }); // rotate webhook destination secret -export interface RotateWebhookDestinationSecretDto { - destinationId: number; -} -export const RotateWebhookDestinationSecretSchema = Joi.object({ +export const RotateWebhookDestinationSecretSchema = Joi.object< + Api.Mutation.Input<'/alerts/rotateWebhookDestinationSecret'>, + true +>({ destinationId: Joi.number().required(), }); diff --git a/backend/src/modules/alerts/serde/db.types.ts b/backend/src/modules/alerts/serde/db.types.ts index 3c2440dfb..4ce59eca3 100644 --- a/backend/src/modules/alerts/serde/db.types.ts +++ b/backend/src/modules/alerts/serde/db.types.ts @@ -2,28 +2,20 @@ export type ComparatorKind = | 'RELATIVE_YOCTONEAR_AMOUNT' | 'RELATIVE_PERCENTAGE_AMOUNT'; -export interface MatchingRule { - rule: - | 'ACTION_ANY' - | 'ACTION_FUNCTION_CALL' - | 'EVENT' - | 'STATE_CHANGE_ACCOUNT_BALANCE'; -} - -export interface TxMatchingRule extends MatchingRule { +export interface TxMatchingRule { rule: 'ACTION_ANY'; status: 'SUCCESS' | 'FAIL' | 'ANY'; affected_account_id: string; } -export interface FnCallMatchingRule extends MatchingRule { +export interface FnCallMatchingRule { rule: 'ACTION_FUNCTION_CALL'; affected_account_id: string; status: 'ANY'; function: string; } -export interface EventMatchingRule extends MatchingRule { +export interface EventMatchingRule { rule: 'EVENT'; contract_account_id: string; standard: string; @@ -31,7 +23,7 @@ export interface EventMatchingRule extends MatchingRule { event: string; } -export interface AcctBalMatchingRule extends MatchingRule { +export interface AcctBalMatchingRule { rule: 'STATE_CHANGE_ACCOUNT_BALANCE'; affected_account_id: string; comparator_kind: ComparatorKind; @@ -40,3 +32,9 @@ export interface AcctBalMatchingRule extends MatchingRule { to: string | null; // yoctoNEAR }; } + +export type MatchingRule = + | TxMatchingRule + | FnCallMatchingRule + | EventMatchingRule + | AcctBalMatchingRule; diff --git a/backend/src/modules/alerts/serde/dto.types.ts b/backend/src/modules/alerts/serde/dto.types.ts deleted file mode 100644 index 8ee3b9787..000000000 --- a/backend/src/modules/alerts/serde/dto.types.ts +++ /dev/null @@ -1,32 +0,0 @@ -// API rule types - this is what the UI/client consumes - -export interface TxRuleDto { - contract: string; -} -export interface FnCallRuleDto { - contract: string; - function: string; - // params?: object; -} -export interface EventRuleDto { - contract: string; - standard: string; - version: string; - event: string; - // data?: object; -} -export interface AcctBalRuleDto { - contract: string; - from: string | null; - to: string | null; -} - -export type RuleType = - | 'TX_SUCCESS' - | 'TX_FAILURE' - | 'FN_CALL' - | 'EVENT' - | 'ACCT_BAL_PCT' - | 'ACCT_BAL_NUM'; - -export type Net = 'MAINNET' | 'TESTNET'; diff --git a/backend/src/modules/alerts/serde/rule-deserializer/rule-deserializer.service.ts b/backend/src/modules/alerts/serde/rule-deserializer/rule-deserializer.service.ts index cab7d0728..6382e9a4d 100644 --- a/backend/src/modules/alerts/serde/rule-deserializer/rule-deserializer.service.ts +++ b/backend/src/modules/alerts/serde/rule-deserializer/rule-deserializer.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { Alerts } from '@pc/common/types/alerts'; import { assertUnreachable } from 'src/helpers'; import { AcctBalMatchingRule, @@ -7,48 +8,49 @@ import { MatchingRule, TxMatchingRule, } from '../db.types'; -import { - AcctBalRuleDto, - EventRuleDto, - FnCallRuleDto, - TxRuleDto, -} from '../dto.types'; @Injectable() export class RuleDeserializerService { - toRuleDto( - rule: MatchingRule, - ): TxRuleDto | FnCallRuleDto | EventRuleDto | AcctBalRuleDto { - const ruleType = rule.rule; - switch (ruleType) { + toRuleDto(matchingRule: MatchingRule): Alerts.Rule { + switch (matchingRule.rule) { case 'ACTION_ANY': - return this.toTxDto(rule as TxMatchingRule); + return this.toTxDto(matchingRule); case 'ACTION_FUNCTION_CALL': - return this.toFnCallDto(rule as FnCallMatchingRule); + return this.toFnCallDto(matchingRule); case 'EVENT': - return this.toEventDto(rule as EventMatchingRule); + return this.toEventDto(matchingRule); case 'STATE_CHANGE_ACCOUNT_BALANCE': - return this.toAcctBalDto(rule as AcctBalMatchingRule); + if (matchingRule.comparator_kind === 'RELATIVE_PERCENTAGE_AMOUNT') { + return this.toAcctBalPctDto(matchingRule); + } + return this.toAcctBalNumDto(matchingRule); default: - assertUnreachable(ruleType); + assertUnreachable( + matchingRule, + (matchingRule) => (matchingRule as MatchingRule).rule, + ); } } - private toTxDto(rule: TxMatchingRule): TxRuleDto { + private toTxDto(rule: TxMatchingRule): Alerts.TransactionRule { return { + // TODO: What's with ANY status? + type: rule.status === 'SUCCESS' ? 'TX_SUCCESS' : 'TX_FAILURE', contract: rule.affected_account_id, }; } - private toFnCallDto(rule: FnCallMatchingRule): FnCallRuleDto { + private toFnCallDto(rule: FnCallMatchingRule): Alerts.FunctionCallRule { return { + type: 'FN_CALL', contract: rule.affected_account_id, function: rule.function, }; } - private toEventDto(rule: EventMatchingRule): EventRuleDto { + private toEventDto(rule: EventMatchingRule): Alerts.EventRule { return { + type: 'EVENT', contract: rule.contract_account_id, event: rule.event, standard: rule.standard, @@ -56,8 +58,18 @@ export class RuleDeserializerService { }; } - private toAcctBalDto(rule: AcctBalMatchingRule): AcctBalRuleDto { + private toAcctBalPctDto(rule: AcctBalMatchingRule): Alerts.AcctBalPctRule { + return { + type: 'ACCT_BAL_PCT', + contract: rule.affected_account_id, + from: rule.comparator_range.from, + to: rule.comparator_range.to, + }; + } + + private toAcctBalNumDto(rule: AcctBalMatchingRule): Alerts.AcctBalNumRule { return { + type: 'ACCT_BAL_NUM', contract: rule.affected_account_id, from: rule.comparator_range.from, to: rule.comparator_range.to, diff --git a/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.spec.ts b/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.spec.ts index 5c2ef959f..15ff6970f 100644 --- a/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.spec.ts +++ b/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.spec.ts @@ -17,7 +17,9 @@ describe('RuleSerializerService', () => { }); it('should serialize success tx rule', () => { - expect(service.toTxSuccessJson({ contract: 'pagoda.near' })).toStrictEqual({ + expect( + service.toTxSuccessJson({ type: 'TX_SUCCESS', contract: 'pagoda.near' }), + ).toStrictEqual({ affected_account_id: 'pagoda.near', rule: 'ACTION_ANY', status: 'SUCCESS', @@ -25,7 +27,9 @@ describe('RuleSerializerService', () => { }); it('should serialize failure tx rule', () => { - expect(service.toTxFailureJson({ contract: 'pagoda.near' })).toStrictEqual({ + expect( + service.toTxFailureJson({ type: 'TX_FAILURE', contract: 'pagoda.near' }), + ).toStrictEqual({ affected_account_id: 'pagoda.near', rule: 'ACTION_ANY', status: 'FAIL', @@ -34,7 +38,11 @@ describe('RuleSerializerService', () => { it('should serialize function call rule', () => { expect( - service.toFnCallJson({ contract: 'pagoda.near', function: 'mint_nft' }), + service.toFnCallJson({ + type: 'FN_CALL', + contract: 'pagoda.near', + function: 'mint_nft', + }), ).toStrictEqual({ affected_account_id: 'pagoda.near', rule: 'ACTION_FUNCTION_CALL', @@ -46,6 +54,7 @@ describe('RuleSerializerService', () => { it('should serialize event rule', () => { expect( service.toEventJson({ + type: 'EVENT', contract: 'pagoda.near', standard: '*', version: '*', @@ -62,7 +71,12 @@ describe('RuleSerializerService', () => { test.each([ { - dto: { contract: 'pagoda.near', from: '0', to: null }, + dto: { + type: 'ACCT_BAL_NUM' as const, + contract: 'pagoda.near', + from: '0', + to: null, + }, expected: { rule: 'STATE_CHANGE_ACCOUNT_BALANCE', affected_account_id: 'pagoda.near', @@ -74,7 +88,12 @@ describe('RuleSerializerService', () => { }, }, { - dto: { contract: 'pagoda.near', to: '330', from: null }, + dto: { + type: 'ACCT_BAL_NUM' as const, + contract: 'pagoda.near', + to: '330', + from: null, + }, expected: { rule: 'STATE_CHANGE_ACCOUNT_BALANCE', affected_account_id: 'pagoda.near', @@ -86,7 +105,12 @@ describe('RuleSerializerService', () => { }, }, { - dto: { contract: 'pagoda.near', from: '0', to: '9000000000000000' }, + dto: { + type: 'ACCT_BAL_NUM' as const, + contract: 'pagoda.near', + from: '0', + to: '9000000000000000', + }, expected: { rule: 'STATE_CHANGE_ACCOUNT_BALANCE', affected_account_id: 'pagoda.near', @@ -98,15 +122,17 @@ describe('RuleSerializerService', () => { }, }, ])('should serialize account balance num rule', ({ dto, expected }) => { - expect(service.toAcctBalJson(dto, 'ACCT_BAL_NUM')).toStrictEqual(expected); + expect(service.toAcctBalJson(dto)).toStrictEqual(expected); }); it('should serialize account balance pct rule', () => { expect( - service.toAcctBalJson( - { contract: 'pagoda.near', from: '0', to: null }, - 'ACCT_BAL_PCT', - ), + service.toAcctBalJson({ + type: 'ACCT_BAL_PCT', + contract: 'pagoda.near', + from: '0', + to: null, + }), ).toStrictEqual({ rule: 'STATE_CHANGE_ACCOUNT_BALANCE', affected_account_id: 'pagoda.near', @@ -120,19 +146,23 @@ describe('RuleSerializerService', () => { it('should fail to serialize account balance rule with invalid range', () => { expect(() => { - service.toAcctBalJson( - { contract: 'pagoda.near', from: null, to: null }, - 'ACCT_BAL_PCT', - ); + service.toAcctBalJson({ + type: 'ACCT_BAL_PCT', + contract: 'pagoda.near', + from: null, + to: null, + }); }).toThrow('Invalid range'); }); it('should fail to serialize account balance rule with from > to', () => { expect(() => { - service.toAcctBalJson( - { contract: 'pagoda.near', from: '3', to: '0' }, - 'ACCT_BAL_PCT', - ); + service.toAcctBalJson({ + type: 'ACCT_BAL_PCT', + contract: 'pagoda.near', + from: '3', + to: '0', + }); }).toThrow('Invalid range'); }); }); diff --git a/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.ts b/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.ts index 824ed7070..b800d31d7 100644 --- a/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.ts +++ b/backend/src/modules/alerts/serde/rule-serializer/rule-serializer.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { Alerts } from '@pc/common/types/alerts'; import { VError } from 'verror'; import { AcctBalMatchingRule, @@ -7,16 +8,10 @@ import { FnCallMatchingRule, TxMatchingRule, } from '../db.types'; -import { - AcctBalRuleDto, - EventRuleDto, - FnCallRuleDto, - TxRuleDto, -} from '../dto.types'; @Injectable() export class RuleSerializerService { - toTxSuccessJson(rule: TxRuleDto): TxMatchingRule { + toTxSuccessJson(rule: Alerts.TransactionRule): TxMatchingRule { return { rule: 'ACTION_ANY', affected_account_id: rule.contract, @@ -24,7 +19,7 @@ export class RuleSerializerService { }; } - toTxFailureJson(rule: TxRuleDto): TxMatchingRule { + toTxFailureJson(rule: Alerts.TransactionRule): TxMatchingRule { return { rule: 'ACTION_ANY', affected_account_id: rule.contract, @@ -32,7 +27,7 @@ export class RuleSerializerService { }; } - toFnCallJson(rule: FnCallRuleDto): FnCallMatchingRule { + toFnCallJson(rule: Alerts.FunctionCallRule): FnCallMatchingRule { return { rule: 'ACTION_FUNCTION_CALL', affected_account_id: rule.contract, @@ -41,7 +36,7 @@ export class RuleSerializerService { }; } - toEventJson(rule: EventRuleDto): EventMatchingRule { + toEventJson(rule: Alerts.EventRule): EventMatchingRule { return { rule: 'EVENT', contract_account_id: rule.contract, @@ -52,8 +47,7 @@ export class RuleSerializerService { } toAcctBalJson( - rule: AcctBalRuleDto, - ruleType: 'ACCT_BAL_NUM' | 'ACCT_BAL_PCT', + rule: Alerts.AcctBalPctRule | Alerts.AcctBalNumRule, ): AcctBalMatchingRule { if (!rule.from && !rule.to) { throw new VError('Invalid range'); @@ -66,7 +60,7 @@ export class RuleSerializerService { return { rule: 'STATE_CHANGE_ACCOUNT_BALANCE', affected_account_id: rule.contract, - comparator_kind: this.ruleTypeToComparatorKind(ruleType), + comparator_kind: this.ruleTypeToComparatorKind(rule.type), comparator_range: { from: rule.from, to: rule.to, diff --git a/backend/src/modules/alerts/telegram/telegram.service.ts b/backend/src/modules/alerts/telegram/telegram.service.ts index ee29c23c3..c3f03bdcc 100644 --- a/backend/src/modules/alerts/telegram/telegram.service.ts +++ b/backend/src/modules/alerts/telegram/telegram.service.ts @@ -5,7 +5,7 @@ import { DateTime } from 'luxon'; import { AppConfig } from 'src/config/validate'; import { VError } from 'verror'; import { PrismaService } from '../prisma.service'; -import { TgChat } from './types'; +import { Alerts } from '@pc/common/types/alerts'; @Injectable() export class TelegramService { @@ -24,7 +24,7 @@ export class TelegramService { }); } - async start(startToken: string | undefined, chat: TgChat) { + async start(startToken: string | undefined, chat: Alerts.TgChat) { const tgDestination = await this.prisma.telegramDestination.findUnique({ where: { startToken, diff --git a/backend/src/modules/alerts/telegram/types.ts b/backend/src/modules/alerts/telegram/types.ts deleted file mode 100644 index 23bbb4994..000000000 --- a/backend/src/modules/alerts/telegram/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface TgUpdate { - update_id: number; - message?: { - chat: TgChat; - text?: string; - }; -} - -export type TgChat = TgPrivateChat | TgGroupChat; - -interface TgPrivateChat { - id: number; - type: 'private'; - username?: string; -} - -interface TgGroupChat { - id: number; - type: 'group' | 'supergroup' | 'channel'; - title?: string; -} diff --git a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts index 920b79287..3e3a9c407 100644 --- a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts +++ b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts @@ -14,12 +14,9 @@ import { VError } from 'verror'; import { TriggeredAlertsService } from './triggered-alerts.service'; import { ListTriggeredAlertSchema, - ListTriggeredAlertDto, - TriggeredAlertsResponseDto, GetTriggeredAlertDetailsSchema, - TriggeredAlertDetailsResponseDto, - GetTriggeredAlertDetailsDto, } from '../dto'; +import { Api } from '@pc/common/types/api'; @Controller('triggeredAlerts') export class TriggeredAlertsController { @@ -42,8 +39,8 @@ export class TriggeredAlertsController { take, pagingDateTime, alertId, - }: ListTriggeredAlertDto, - ): Promise { + }: Api.Query.Input<'/triggeredAlerts/listTriggeredAlerts'>, + ): Promise> { try { return await this.triggeredAlertsService.listTriggeredAlertsByProject( req.user, @@ -64,8 +61,9 @@ export class TriggeredAlertsController { @UsePipes(new JoiValidationPipe(GetTriggeredAlertDetailsSchema)) async getTriggeredAlertDetails( @Request() req, - @Body() { slug }: GetTriggeredAlertDetailsDto, - ): Promise { + @Body() + { slug }: Api.Query.Input<'/triggeredAlerts/getTriggeredAlertDetails'>, + ): Promise> { try { return await this.triggeredAlertsService.getTriggeredAlertDetails( req.user, diff --git a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts index 339af73fa..09da8c64e 100644 --- a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts +++ b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts @@ -4,17 +4,14 @@ import { Alert, Prisma, TriggeredAlert } from '@pc/database/clients/alerts'; import { PrismaService } from '../prisma.service'; import { PermissionsService as ProjectPermissionsService } from '../../../core/projects/permissions.service'; import { AlertsService } from '../alerts.service'; -import { MatchingRule } from '../serde/db.types'; import { - TriggeredAlertDetailsResponseDto, - TriggeredAlertsResponseDto, -} from '../dto'; + AcctBalMatchingRule, + EventMatchingRule, + FnCallMatchingRule, + TxMatchingRule, +} from '../serde/db.types'; import { VError } from 'verror'; -type TriggeredAlertWithAlert = TriggeredAlert & { - alert: Alert; -}; - @Injectable() export class TriggeredAlertsService { constructor( @@ -31,7 +28,7 @@ export class TriggeredAlertsService { take: number, pagingDateTime: Date, alertId?: number, - ): Promise { + ) { await this.projectPermissions.checkUserProjectEnvPermission( user.id, projectSlug, @@ -80,8 +77,15 @@ export class TriggeredAlertsService { public async getTriggeredAlertDetails( user: User, slug: TriggeredAlert['slug'], - ): Promise { - const triggeredAlert = await this.getTriggeredAlertWithAlert(slug); + ) { + const triggeredAlert = await this.prisma.triggeredAlert.findFirst({ + where: { + slug, + }, + include: { + alert: true, + }, + }); if (!triggeredAlert) { throw new VError( @@ -125,8 +129,10 @@ export class TriggeredAlertsService { } private toTriggeredAlertDto( - triggeredAlert: TriggeredAlertWithAlert, - ): TriggeredAlertDetailsResponseDto { + triggeredAlert: TriggeredAlert & { + alert: Alert; + }, + ) { const { slug, alert, @@ -136,7 +142,11 @@ export class TriggeredAlertsService { triggeredAt, } = triggeredAlert; const extraData = triggeredAlert.extraData as Record; - const rule = alert.matchingRule as object as MatchingRule; + const rule = alert.matchingRule as object as + | TxMatchingRule + | FnCallMatchingRule + | EventMatchingRule + | AcctBalMatchingRule; return { slug, @@ -146,24 +156,8 @@ export class TriggeredAlertsService { triggeredInBlockHash, triggeredInTransactionHash, triggeredInReceiptId, - triggeredAt, + triggeredAt: triggeredAt.toString(), extraData, }; } - - private async getTriggeredAlertWithAlert( - slug: TriggeredAlert['slug'], - ): Promise { - const triggeredAlert: TriggeredAlertWithAlert = - (await this.prisma.triggeredAlert.findFirst({ - where: { - slug, - }, - include: { - alert: true, - }, - }))!; - - return triggeredAlert; - } } diff --git a/backend/src/modules/alerts/types.ts b/backend/src/modules/alerts/types.ts deleted file mode 100644 index a56760e0f..000000000 --- a/backend/src/modules/alerts/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - Destination, - EmailDestination, - TelegramDestination, - WebhookDestination, -} from '@pc/database/clients/alerts'; - -export type RuleType = - | 'TX_SUCCESS' - | 'TX_FAILURE' - | 'FN_CALL' - | 'EVENT' - | 'ACCT_BAL_PCT' - | 'ACCT_BAL_NUM'; - -export type NumberComparator = 'GT' | 'GTE' | 'LT' | 'LTE' | 'EQ'; - -export type Net = 'MAINNET' | 'TESTNET'; - -export type PremapDestination = Pick< - Destination, - 'id' | 'name' | 'projectSlug' | 'type' | 'isValid' -> & { - webhookDestination?: Partial | null; - emailDestination?: Partial | null; - telegramDestination?: Partial | null; -}; diff --git a/backend/src/modules/rpcstats/dto.ts b/backend/src/modules/rpcstats/dto.ts index 3573205d0..17f29ca68 100644 --- a/backend/src/modules/rpcstats/dto.ts +++ b/backend/src/modules/rpcstats/dto.ts @@ -2,59 +2,29 @@ // because class-validator was experiencing issues at the time of implementation // and had many unaddressed github issues import * as Joi from 'joi'; -import { DateTime } from 'luxon'; -import { Net } from '../alerts/types'; +import { Api } from '@pc/common/types/api'; -export enum DateTimeResolution { - FIFTEEN_SECONDS = 'FIFTEEN_SECONDS', - ONE_MINUTE = 'ONE_MINUTE', - ONE_HOUR = 'ONE_HOUR', - ONE_DAY = 'ONE_DAY', -} - -export enum MetricGroupBy { - DATE = 'date', - ENDPOINT = 'endpoint', -} -export interface EndpointMetricsDto { - projectSlug: string; - environmentSubId: number; - startDateTime: string; - endDateTime: string; - dateTimeResolution: DateTimeResolution; - skip?: number; - take?: number; - pagingDateTime?: Date; - grouping?: MetricGroupBy[]; -} - -export const EndpointMetricsSchema = Joi.object({ +export const EndpointMetricsSchema = Joi.object< + Api.Query.Input<'/rpcstats/endpointMetrics'>, + true +>({ projectSlug: Joi.string().required(), environmentSubId: Joi.number().required(), startDateTime: Joi.string().required(), endDateTime: Joi.string().required(), - dateTimeResolution: Joi.string().optional(), - grouping: Joi.array().items(Joi.string().required()).optional(), skip: Joi.number().integer().min(0).optional(), take: Joi.number().integer().min(0).max(100).optional(), pagingDateTime: Joi.date().optional(), + filter: Joi.alternatives( + Joi.object({ + type: Joi.string().valid('date'), + dateTimeResolution: Joi.alternatives( + 'FIFTEEN_SECONDS', + 'ONE_MINUTE', + 'ONE_HOUR', + 'ONE_DAY', + ), + }), + Joi.object({ type: Joi.string().valid('endpoint') }), + ), }); - -export interface EndpointMetricsDetailsResponseDto { - apiKeyIdentifier: string; - endpointGroup?: string; - endpointMethod: string; - network: Net; - windowStart?: DateTime; - windowEnd?: DateTime; - successCount: number; - errorCount: number; - minLatency: number; - maxLatency: number; - meanLatency: number; -} - -export interface EndpointMetricsResponseDto { - count: number; - page: EndpointMetricsDetailsResponseDto[]; -} diff --git a/backend/src/modules/rpcstats/rpcstats.controller.ts b/backend/src/modules/rpcstats/rpcstats.controller.ts index e0ef8c613..ea0355981 100644 --- a/backend/src/modules/rpcstats/rpcstats.controller.ts +++ b/backend/src/modules/rpcstats/rpcstats.controller.ts @@ -15,11 +15,8 @@ import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; import { VError } from 'verror'; import { ProjectsService } from '@/src/core/projects/projects.service'; import { RpcStatsService } from './rpcstats.service'; -import { - EndpointMetricsSchema, - EndpointMetricsDto, - EndpointMetricsResponseDto, -} from './dto'; +import { EndpointMetricsSchema } from './dto'; +import { Api } from '@pc/common/types/api'; @Controller('rpcstats') export class RpcStatsController { @@ -41,10 +38,9 @@ export class RpcStatsController { environmentSubId, startDateTime, endDateTime, - dateTimeResolution, - grouping, - }: EndpointMetricsDto, - ): Promise { + filter, + }: Api.Query.Input<'/rpcstats/endpointMetrics'>, + ): Promise> { try { // When there is a project passed in, check that the user has access to the project // await this.projectService.checkUserPermission( @@ -82,8 +78,7 @@ export class RpcStatsController { allApiKeyConsumerNames, DateTime.fromISO(startDateTime), DateTime.fromISO(endDateTime), - dateTimeResolution, - grouping!, + filter, ); } catch (e: any) { throw mapError(e); diff --git a/backend/src/modules/rpcstats/rpcstats.service.ts b/backend/src/modules/rpcstats/rpcstats.service.ts index b0a6f7fa3..8b4398984 100644 --- a/backend/src/modules/rpcstats/rpcstats.service.ts +++ b/backend/src/modules/rpcstats/rpcstats.service.ts @@ -1,12 +1,9 @@ import { DateTime } from 'luxon'; import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; -import { - DateTimeResolution, - EndpointMetricsResponseDto, - MetricGroupBy, -} from './dto'; -import { Net } from '../alerts/types'; +import { Api } from '@pc/common/types/api'; +import { Net } from '@pc/database/clients/core'; +import { RpcStats } from '@pc/common/types/rpcstats'; @Injectable() export class RpcStatsService { @@ -17,9 +14,8 @@ export class RpcStatsService { apiKeyConsumerNames: Array, startDateTime: DateTime, endDateTime: DateTime, - dateTimeResolution: DateTimeResolution, - grouping: MetricGroupBy[], - ): Promise { + filter: Api.Query.Input<'/rpcstats/endpointMetrics'>['filter'], + ): Promise> { const whereClause = this.determineWhereClause( network, apiKeyConsumerNames, @@ -29,20 +25,24 @@ export class RpcStatsService { const groupBy: any = []; const orderBy: any = []; - if (grouping?.includes(MetricGroupBy.ENDPOINT)) { + if (filter.type === RpcStats.MetricGroupBy.ENDPOINT) { groupBy.push('endpointMethod'); orderBy.push({ endpointMethod: 'asc' }); - } - if (grouping?.includes(MetricGroupBy.DATE)) { + } else { groupBy.push('year', 'month', 'day'); orderBy.push({ year: 'asc' }, { month: 'asc' }, { day: 'asc' }); - if (dateTimeResolution === DateTimeResolution.ONE_HOUR) { + if (filter.dateTimeResolution === RpcStats.DateTimeResolution.ONE_HOUR) { groupBy.push('hour24'); orderBy.push({ hour24: 'asc' }); - } else if (dateTimeResolution === DateTimeResolution.ONE_MINUTE) { + } else if ( + filter.dateTimeResolution === RpcStats.DateTimeResolution.ONE_MINUTE + ) { groupBy.push('hour24', 'minute'); orderBy.push({ hour24: 'asc' }, { minute: 'asc' }); - } else if (dateTimeResolution === DateTimeResolution.FIFTEEN_SECONDS) { + } else if ( + filter.dateTimeResolution === + RpcStats.DateTimeResolution.FIFTEEN_SECONDS + ) { groupBy.push('hour24', 'minute', 'quarterMinute'); orderBy.push( { hour24: 'asc' }, @@ -75,7 +75,14 @@ export class RpcStatsService { }); const [metrics] = await Promise.all([listPromise]); - const page = metrics.map((m) => this.toDto(m, dateTimeResolution)); + const page = metrics.map((m) => + this.toDto( + m, + filter.type === RpcStats.MetricGroupBy.DATE + ? filter.dateTimeResolution + : undefined, + ), + ); return { count: metrics.length, // no paging currently but maintaining the paging interface in case it's needed page, @@ -83,7 +90,7 @@ export class RpcStatsService { } private dateTimePartsToResolution( - dateTimeResolution: DateTimeResolution, + dateTimeResolution: RpcStats.DateTimeResolution, dateTimeParts: { year: number; month: number; @@ -102,7 +109,7 @@ export class RpcStatsService { quarterMinute, } = dateTimeParts; switch (dateTimeResolution) { - case DateTimeResolution.FIFTEEN_SECONDS: + case RpcStats.DateTimeResolution.FIFTEEN_SECONDS: const secondsInQuarterMinute = quarterMinute! * 15; return DateTime.fromObject( { @@ -116,16 +123,16 @@ export class RpcStatsService { { zone: 'UTC' }, ); break; - case DateTimeResolution.ONE_MINUTE: + case RpcStats.DateTimeResolution.ONE_MINUTE: return DateTime.fromObject( { year, month, day, hour, minute }, { zone: 'UTC' }, ); break; - case DateTimeResolution.ONE_HOUR: + case RpcStats.DateTimeResolution.ONE_HOUR: return DateTime.fromObject({ year, month, day, hour }, { zone: 'UTC' }); break; - case DateTimeResolution.ONE_DAY: + case RpcStats.DateTimeResolution.ONE_DAY: return DateTime.fromObject({ year, month, day }, { zone: 'UTC' }); break; default: @@ -133,7 +140,10 @@ export class RpcStatsService { } } - private toDto(aggregatedMetricRow, dateTimeResolution: DateTimeResolution) { + private toDto( + aggregatedMetricRow, + dateTimeResolution?: RpcStats.DateTimeResolution, + ) { const { apiKeyIdentifier, endpointMethod, network } = aggregatedMetricRow; const dto = { diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 37ea1e54e..b75942869 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "outDir": "./dist", "baseUrl": "./", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "paths": { "@/*": ["./*"], "@generated/*": ["./generated/*"] diff --git a/common/.prettierrc b/common/.prettierrc new file mode 100644 index 000000000..dcb72794f --- /dev/null +++ b/common/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/common/README.md b/common/README.md new file mode 100644 index 000000000..fb8462c29 --- /dev/null +++ b/common/README.md @@ -0,0 +1,10 @@ +## Imports + +Imports should target `@pc/common/types` + +e.g. + +```ts +import type { Api } from '@pc/common/types/api'; +import type { Alerts } from '@pc/common/types/alerts'; +``` diff --git a/common/package.json b/common/package.json new file mode 100644 index 000000000..e6296486a --- /dev/null +++ b/common/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pc/common", + "version": "1.0.0", + "description": "", + "repository": { + "type": "git", + "url": "git+https://github.com/near/pagoda-console.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/near/pagoda-console/issues" + }, + "homepage": "https://github.com/near/pagoda-console#readme" +} diff --git a/common/types/abi/abi.schema.ts b/common/types/abi/abi.schema.ts new file mode 100644 index 000000000..0748ccfa0 --- /dev/null +++ b/common/types/abi/abi.schema.ts @@ -0,0 +1,34 @@ +import type { AbiRoot } from 'near-abi-client-js'; +import { Abi } from '@pc/database/clients/abi'; + +export namespace Query { + export namespace Inputs { + export type GetContractAbi = { contract: string }; + } + + export namespace Outputs { + export type GetContractAbi = Pick & { + abi: AbiRoot; + }; + } + + export namespace Errors { + export type GetContractAbi = unknown; + } +} + +export namespace Mutation { + export namespace Inputs { + export type AddContractAbi = { contract: string; abi: AbiRoot }; + } + + export namespace Outputs { + export type AddContractAbi = Pick & { + abi: AbiRoot; + }; + } + + export namespace Errors { + export type AddContractAbi = unknown; + } +} diff --git a/common/types/abi/index.ts b/common/types/abi/index.ts new file mode 100644 index 000000000..055e195b0 --- /dev/null +++ b/common/types/abi/index.ts @@ -0,0 +1 @@ +export * as Abi from './abi.schema'; diff --git a/common/types/alerts/alerts.schema.ts b/common/types/alerts/alerts.schema.ts new file mode 100644 index 000000000..435b36487 --- /dev/null +++ b/common/types/alerts/alerts.schema.ts @@ -0,0 +1,268 @@ +import { + Alert as AlertDatabase, + Destination as DestinationDatabase, + DestinationType, + EmailDestination as EmailDestinationDatabase, + TelegramDestination as TelegramDestinationDatabase, + WebhookDestination as WebhookDestinationDatabase, +} from '@pc/database/clients/alerts'; + +export type RuleType = + | 'TX_SUCCESS' + | 'TX_FAILURE' + | 'FN_CALL' + | 'EVENT' + | 'ACCT_BAL_PCT' + | 'ACCT_BAL_NUM'; + +export type TransactionRule = { + type: 'TX_SUCCESS' | 'TX_FAILURE'; + contract: string; +}; +export type FunctionCallRule = { + type: 'FN_CALL'; + contract: string; + function: string; + // params?: object; +}; +export type EventRule = { + type: 'EVENT'; + contract: string; + standard: string; + version: string; + event: string; + // data?: object; +}; +export type AcctBalPctRule = { + type: 'ACCT_BAL_PCT'; + contract: string; + from: string | null; + to: string | null; +}; +export type AcctBalNumRule = { + type: 'ACCT_BAL_NUM'; + contract: string; + from: string | null; + to: string | null; +}; + +export type Rule = + | TransactionRule + | FunctionCallRule + | EventRule + | AcctBalPctRule + | AcctBalNumRule; + +export type CreateAlertBaseInput = { + name?: string; + projectSlug: string; + environmentSubId: number; + destinations?: Array; +}; +type CreateTxAlertInput = CreateAlertBaseInput & { + rule: TransactionRule; +}; +type CreateFnCallAlertInput = CreateAlertBaseInput & { + rule: FunctionCallRule; +}; +type CreateEventAlertInput = CreateAlertBaseInput & { + rule: EventRule; +}; +type CreateAcctBalPctAlertInput = CreateAlertBaseInput & { + rule: AcctBalPctRule; +}; +type CreateAcctBalNumAlertInput = CreateAlertBaseInput & { + rule: AcctBalNumRule; +}; + +export type CreateAlertInput = + | CreateTxAlertInput + | CreateFnCallAlertInput + | CreateEventAlertInput + | CreateAcctBalPctAlertInput + | CreateAcctBalNumAlertInput; + +type CreateBaseDestinationInput = { + name?: string; + type: DestinationType; + projectSlug: string; +}; + +export type CreateWebhookDestinationInput = CreateBaseDestinationInput & { + type: 'WEBHOOK'; + config: { + url: string; + }; +}; +export type CreateEmailDestinationInput = CreateBaseDestinationInput & { + type: 'EMAIL'; + config: { + email: string; + }; +}; +export type CreateTelegramDestinationInput = CreateBaseDestinationInput & { + type: 'TELEGRAM'; + config?: Record; // eslint recommended typing for empty object +}; + +export type CreateDestinationInput = + | CreateWebhookDestinationInput + | CreateEmailDestinationInput + | CreateTelegramDestinationInput; + +type BaseDestination = Pick< + DestinationDatabase, + 'id' | 'name' | 'projectSlug' | 'isValid' +>; + +export type WebhookDestination = BaseDestination & { + type: 'WEBHOOK'; + config: Pick; +}; + +export type EmailDestination = BaseDestination & { + type: 'EMAIL'; + config: Pick; +}; + +export type TelegramDestination = BaseDestination & { + type: 'TELEGRAM'; + config: Pick; +}; + +export type Destination = + | WebhookDestination + | EmailDestination + | TelegramDestination; + +type UpdateDestinationBaseInput = { + id: number; + type: DestinationType; + name?: string; +}; +export type UpdateWebhookDestinationInput = UpdateDestinationBaseInput & { + type: 'WEBHOOK'; + config: { + url: string; + }; +}; +export type UpdateEmailDestinationInput = UpdateDestinationBaseInput & { + type: 'EMAIL'; +}; +export type UpdateTelegramDestinationInput = UpdateDestinationBaseInput & { + type: 'TELEGRAM'; +}; + +export type UpdateDestinationInput = + | UpdateWebhookDestinationInput + | UpdateEmailDestinationInput + | UpdateTelegramDestinationInput; + +type EnabledDestination = Pick & { + config: + | Pick + | Pick + | Pick; +}; + +export type Alert = Pick< + AlertDatabase, + 'id' | 'name' | 'isPaused' | 'projectSlug' | 'environmentSubId' +> & { + rule: Rule; + enabledDestinations: EnabledDestination[]; +}; + +export type TgUpdate = { + update_id: number; + message?: { + chat: TgChat; + text?: string; + }; +}; + +export type TgChat = TgPrivateChat | TgGroupChat; + +type TgPrivateChat = { + id: number; + type: 'private'; + username?: string; +}; + +type TgGroupChat = { + id: number; + type: 'group' | 'supergroup' | 'channel'; + title?: string; +}; + +export type Comparator = 'EQ' | 'LTE' | 'GTE' | 'RANGE'; + +export namespace Query { + export namespace Inputs { + export type ListAlerts = { projectSlug: string; environmentSubId: number }; + export type GetAlertDetails = { id: number }; + export type ListDestinations = { projectSlug: string }; + } + + export namespace Outputs { + export type ListAlerts = Alert[]; + export type GetAlertDetails = Alert; + export type ListDestinations = Destination[]; + } + + export namespace Errors { + export type ListAlerts = unknown; + export type GetAlertDetails = unknown; + export type ListDestinations = unknown; + } +} + +export namespace Mutation { + export namespace Inputs { + export type CreateAlert = CreateAlertInput; + export type UpdateAlert = { id: number; name?: string; isPaused?: boolean }; + export type DeleteAlert = { id: number }; + export type CreateDestination = CreateDestinationInput; + export type DeleteDestination = { id: number }; + export type EnableDestination = { alert: number; destination: number }; + export type DisableDestination = { alert: number; destination: number }; + export type UpdateDestination = UpdateDestinationInput; + export type VerifyEmailDestination = { token: string }; + export type TelegramWebhook = TgUpdate; + export type ResendEmailVerification = { destinationId: number }; + export type UnsubscribeFromEmailAlert = { token: string }; + export type RotateWebhookDestinationSecret = { destinationId: number }; + } + + export namespace Outputs { + export type CreateAlert = Alert; + export type UpdateAlert = Alert; + export type DeleteAlert = void; + export type CreateDestination = Destination; + export type DeleteDestination = void; + export type EnableDestination = void; + export type DisableDestination = void; + export type UpdateDestination = Destination; + export type VerifyEmailDestination = void; + export type TelegramWebhook = void; + export type ResendEmailVerification = void; + export type UnsubscribeFromEmailAlert = void; + export type RotateWebhookDestinationSecret = WebhookDestination; + } + + export namespace Errors { + export type CreateAlert = unknown; + export type UpdateAlert = unknown; + export type DeleteAlert = unknown; + export type CreateDestination = unknown; + export type DeleteDestination = unknown; + export type EnableDestination = unknown; + export type DisableDestination = unknown; + export type UpdateDestination = unknown; + export type VerifyEmailDestination = unknown; + export type TelegramWebhook = unknown; + export type ResendEmailVerification = unknown; + export type UnsubscribeFromEmailAlert = unknown; + export type RotateWebhookDestinationSecret = unknown; + } +} diff --git a/common/types/alerts/index.ts b/common/types/alerts/index.ts new file mode 100644 index 000000000..246fd0b0a --- /dev/null +++ b/common/types/alerts/index.ts @@ -0,0 +1,2 @@ +export * as Alerts from './alerts.schema'; +export * as TriggeredAlerts from './triggered-alerts.schema'; diff --git a/common/types/alerts/triggered-alerts.schema.ts b/common/types/alerts/triggered-alerts.schema.ts new file mode 100644 index 000000000..724f6509c --- /dev/null +++ b/common/types/alerts/triggered-alerts.schema.ts @@ -0,0 +1,42 @@ +import { RuleType } from './alerts.schema'; + +export type TriggeredAlert = { + slug: string; + alertId: number; + name: string; + type: RuleType; + triggeredInBlockHash: string; + triggeredInTransactionHash: string | null; + triggeredInReceiptId: string | null; + triggeredAt: string; + extraData?: Record; +}; + +export type TriggeredAlertList = { + count: number; + page: Array; +}; + +export namespace Query { + export namespace Inputs { + export type ListTriggeredAlerts = { + projectSlug: string; + environmentSubId: number; + skip?: number; + take?: number; + pagingDateTime?: Date; + alertId?: number; + }; + export type GetTriggeredAlertDetails = { slug: string }; + } + + export namespace Outputs { + export type ListTriggeredAlerts = TriggeredAlertList; + export type GetTriggeredAlertDetails = TriggeredAlert; + } + + export namespace Errors { + export type ListTriggeredAlerts = unknown; + export type GetTriggeredAlertDetails = unknown; + } +} diff --git a/common/types/api.ts b/common/types/api.ts new file mode 100644 index 000000000..f6cf084a5 --- /dev/null +++ b/common/types/api.ts @@ -0,0 +1,297 @@ +import type { Abi } from './abi'; +import type { Alerts, TriggeredAlerts } from './alerts'; +import type { Explorer, Projects, Users } from './core'; +import type { RpcStats } from './rpcstats'; + +export namespace Api { + export namespace Query { + type Mapping = { + '/explorer/activity': { + input: Explorer.Query.Inputs.Activity; + output: Explorer.Query.Outputs.Activity; + error: Explorer.Query.Errors.Activity; + }; + '/explorer/balanceChanges': { + input: Explorer.Query.Inputs.BalanceChanges; + output: Explorer.Query.Outputs.BalanceChanges; + error: Explorer.Query.Errors.BalanceChanges; + }; + '/explorer/transaction': { + input: Explorer.Query.Inputs.GetTransaction; + output: Explorer.Query.Outputs.GetTransaction; + error: Explorer.Query.Errors.GetTransaction; + }; + '/explorer/getTransactions': { + input: Explorer.Query.Inputs.GetTransactions; + output: Explorer.Query.Outputs.GetTransactions; + error: Explorer.Query.Errors.GetTransactions; + }; + + '/projects/getDetails': { + input: Projects.Query.Inputs.GetDetails; + output: Projects.Query.Outputs.GetDetails; + error: Projects.Query.Errors.GetDetails; + }; + '/projects/getContracts': { + input: Projects.Query.Inputs.GetContracts; + output: Projects.Query.Outputs.GetContracts; + error: Projects.Query.Errors.GetContracts; + }; + '/projects/getContract': { + input: Projects.Query.Inputs.GetContract; + output: Projects.Query.Outputs.GetContract; + error: Projects.Query.Errors.GetContract; + }; + '/projects/list': { + input: Projects.Query.Inputs.List; + output: Projects.Query.Outputs.List; + error: Projects.Query.Errors.List; + }; + '/projects/getEnvironments': { + input: Projects.Query.Inputs.GetEnvironments; + output: Projects.Query.Outputs.GetEnvironments; + error: Projects.Query.Errors.GetEnvironments; + }; + '/projects/getKeys': { + input: Projects.Query.Inputs.GetKeys; + output: Projects.Query.Outputs.GetKeys; + error: Projects.Query.Errors.GetKeys; + }; + + '/users/getAccountDetails': { + input: Users.Query.Inputs.GetAccountDetails; + // TODO: verify those types, no idea where they are from + output: Users.Query.Outputs.GetAccountDetails; + error: Users.Query.Errors.GetAccountDetails; + }; + '/users/listOrgsWithOnlyAdmin': { + input: Users.Query.Inputs.ListOrgsWithOnlyAdmin; + output: Users.Query.Outputs.ListOrgsWithOnlyAdmin; + error: Users.Query.Errors.ListOrgsWithOnlyAdmin; + }; + '/users/listOrgMembers': { + input: Users.Query.Inputs.ListOrgMembers; + output: Users.Query.Outputs.ListOrgMembers; + error: Users.Query.Errors.ListOrgMembers; + }; + '/users/listOrgs': { + input: Users.Query.Inputs.ListOrgs; + output: Users.Query.Outputs.ListOrgs; + error: Users.Query.Errors.ListOrgs; + }; + + '/abi/getContractAbi': { + input: Abi.Query.Inputs.GetContractAbi; + output: Abi.Query.Outputs.GetContractAbi; + error: Abi.Query.Errors.GetContractAbi; + }; + + '/alerts/listAlerts': { + input: Alerts.Query.Inputs.ListAlerts; + output: Alerts.Query.Outputs.ListAlerts; + error: Alerts.Query.Errors.ListAlerts; + }; + '/alerts/getAlertDetails': { + input: Alerts.Query.Inputs.GetAlertDetails; + output: Alerts.Query.Outputs.GetAlertDetails; + error: Alerts.Query.Errors.GetAlertDetails; + }; + '/alerts/listDestinations': { + input: Alerts.Query.Inputs.ListDestinations; + output: Alerts.Query.Outputs.ListDestinations; + error: Alerts.Query.Errors.ListDestinations; + }; + + '/triggeredAlerts/listTriggeredAlerts': { + input: TriggeredAlerts.Query.Inputs.ListTriggeredAlerts; + output: TriggeredAlerts.Query.Outputs.ListTriggeredAlerts; + error: TriggeredAlerts.Query.Errors.ListTriggeredAlerts; + }; + '/triggeredAlerts/getTriggeredAlertDetails': { + input: TriggeredAlerts.Query.Inputs.GetTriggeredAlertDetails; + output: TriggeredAlerts.Query.Outputs.GetTriggeredAlertDetails; + error: TriggeredAlerts.Query.Errors.GetTriggeredAlertDetails; + }; + + '/rpcstats/endpointMetrics': { + input: RpcStats.Query.Inputs.EndpointMetrics; + output: RpcStats.Query.Outputs.EndpointMetrics; + error: RpcStats.Query.Errors.EndpointMetrics; + }; + }; + + export type Key = keyof Mapping; + export type Output = Mapping[K]['output']; + export type Input = Mapping[K]['input']; + export type Error = Mapping[K]['error']; + } + + export namespace Mutation { + type Mapping = { + '/projects/create': { + input: Projects.Mutation.Inputs.Create; + output: Projects.Mutation.Outputs.Create; + error: Projects.Mutation.Errors.Create; + }; + '/projects/ejectTutorial': { + input: Projects.Mutation.Inputs.EjectTutorial; + output: Projects.Mutation.Outputs.EjectTutorial; + error: Projects.Mutation.Errors.EjectTutorial; + }; + '/projects/delete': { + input: Projects.Mutation.Inputs.Delete; + output: Projects.Mutation.Outputs.Delete; + error: Projects.Mutation.Errors.Delete; + }; + '/projects/addContract': { + input: Projects.Mutation.Inputs.AddContract; + output: Projects.Mutation.Outputs.AddContract; + error: Projects.Mutation.Errors.AddContract; + }; + '/projects/removeContract': { + input: Projects.Mutation.Inputs.RemoveContract; + output: Projects.Mutation.Outputs.RemoveContract; + error: Projects.Mutation.Errors.RemoveContract; + }; + '/projects/rotateKey': { + input: Projects.Mutation.Inputs.RotateKey; + output: Projects.Mutation.Outputs.RotateKey; + error: Projects.Mutation.Errors.RotateKey; + }; + '/projects/generateKey': { + input: Projects.Mutation.Inputs.GenerateKey; + output: Projects.Mutation.Outputs.GenerateKey; + error: Projects.Mutation.Errors.GenerateKey; + }; + '/projects/deleteKey': { + input: Projects.Mutation.Inputs.DeleteKey; + output: Projects.Mutation.Outputs.DeleteKey; + error: Projects.Mutation.Errors.DeleteKey; + }; + + '/users/deleteAccount': { + input: Users.Mutation.Inputs.DeleteAccount; + output: Users.Mutation.Outputs.DeleteAccount; + error: Users.Mutation.Errors.DeleteAccount; + }; + '/users/createOrg': { + input: Users.Mutation.Inputs.CreateOrg; + output: Users.Mutation.Outputs.CreateOrg; + error: Users.Mutation.Errors.CreateOrg; + }; + '/users/inviteToOrg': { + input: Users.Mutation.Inputs.InviteToOrg; + output: Users.Mutation.Outputs.InviteToOrg; + error: Users.Mutation.Errors.InviteToOrg; + }; + '/users/acceptOrgInvite': { + input: Users.Mutation.Inputs.AcceptOrgInvite; + output: Users.Mutation.Outputs.AcceptOrgInvite; + error: Users.Mutation.Errors.AcceptOrgInvite; + }; + '/users/deleteOrg': { + input: Users.Mutation.Inputs.DeleteOrg; + output: Users.Mutation.Outputs.DeleteOrg; + error: Users.Mutation.Errors.DeleteOrg; + }; + '/users/changeOrgRole': { + input: Users.Mutation.Inputs.ChangeOrgRole; + output: Users.Mutation.Outputs.ChangeOrgRole; + error: Users.Mutation.Errors.ChangeOrgRole; + }; + '/users/removeFromOrg': { + input: Users.Mutation.Inputs.RemoveFromOrg; + output: Users.Mutation.Outputs.RemoveFromOrg; + error: Users.Mutation.Errors.RemoveFromOrg; + }; + '/users/removeOrgInvite': { + input: Users.Mutation.Inputs.RemoveOrgInvite; + output: Users.Mutation.Outputs.RemoveOrgInvite; + error: Users.Mutation.Errors.RemoveOrgInvite; + }; + '/users/resetPassword': { + input: Users.Mutation.Inputs.ResetPassword; + output: Users.Mutation.Outputs.ResetPassword; + error: Users.Mutation.Errors.ResetPassword; + }; + + '/abi/addContractAbi': { + input: Abi.Mutation.Inputs.AddContractAbi; + output: Abi.Mutation.Outputs.AddContractAbi; + error: Abi.Mutation.Errors.AddContractAbi; + }; + + '/alerts/createAlert': { + input: Alerts.Mutation.Inputs.CreateAlert; + output: Alerts.Mutation.Outputs.CreateAlert; + error: Alerts.Mutation.Errors.CreateAlert; + }; + '/alerts/updateAlert': { + input: Alerts.Mutation.Inputs.UpdateAlert; + output: Alerts.Mutation.Outputs.UpdateAlert; + error: Alerts.Mutation.Errors.UpdateAlert; + }; + '/alerts/deleteAlert': { + input: Alerts.Mutation.Inputs.DeleteAlert; + output: Alerts.Mutation.Outputs.DeleteAlert; + error: Alerts.Mutation.Errors.DeleteAlert; + }; + '/alerts/createDestination': { + input: Alerts.Mutation.Inputs.CreateDestination; + output: Alerts.Mutation.Outputs.CreateDestination; + error: Alerts.Mutation.Errors.CreateDestination; + }; + '/alerts/deleteDestination': { + input: Alerts.Mutation.Inputs.DeleteDestination; + output: Alerts.Mutation.Outputs.DeleteDestination; + error: Alerts.Mutation.Errors.DeleteDestination; + }; + '/alerts/enableDestination': { + input: Alerts.Mutation.Inputs.EnableDestination; + output: Alerts.Mutation.Outputs.EnableDestination; + error: Alerts.Mutation.Errors.EnableDestination; + }; + '/alerts/disableDestination': { + input: Alerts.Mutation.Inputs.DisableDestination; + output: Alerts.Mutation.Outputs.DisableDestination; + error: Alerts.Mutation.Errors.DisableDestination; + }; + '/alerts/updateDestination': { + input: Alerts.Mutation.Inputs.UpdateDestination; + output: Alerts.Mutation.Outputs.UpdateDestination; + error: Alerts.Mutation.Errors.UpdateDestination; + }; + '/alerts/verifyEmailDestination': { + input: Alerts.Mutation.Inputs.VerifyEmailDestination; + output: Alerts.Mutation.Outputs.VerifyEmailDestination; + error: Alerts.Mutation.Errors.VerifyEmailDestination; + }; + // TODO: should we expose that? + '/alerts/telegramWebhook': { + input: Alerts.Mutation.Inputs.TelegramWebhook; + output: Alerts.Mutation.Outputs.TelegramWebhook; + error: Alerts.Mutation.Errors.TelegramWebhook; + }; + '/alerts/resendEmailVerification': { + input: Alerts.Mutation.Inputs.ResendEmailVerification; + output: Alerts.Mutation.Outputs.ResendEmailVerification; + error: Alerts.Mutation.Errors.ResendEmailVerification; + }; + '/alerts/unsubscribeFromEmailAlert': { + input: Alerts.Mutation.Inputs.UnsubscribeFromEmailAlert; + output: Alerts.Mutation.Outputs.UnsubscribeFromEmailAlert; + error: Alerts.Mutation.Errors.UnsubscribeFromEmailAlert; + }; + '/alerts/rotateWebhookDestinationSecret': { + input: Alerts.Mutation.Inputs.RotateWebhookDestinationSecret; + output: Alerts.Mutation.Outputs.RotateWebhookDestinationSecret; + error: Alerts.Mutation.Errors.RotateWebhookDestinationSecret; + }; + }; + + export type Key = keyof Mapping; + export type Output = Mapping[K]['output']; + export type Input = Mapping[K]['input']; + export type Error = Mapping[K]['error']; + } +} diff --git a/frontend/components/explorer/transaction/types.ts b/common/types/core/explorer-errors.ts similarity index 60% rename from frontend/components/explorer/transaction/types.ts rename to common/types/core/explorer-errors.ts index 65ef55b7b..c84b14055 100644 --- a/frontend/components/explorer/transaction/types.ts +++ b/common/types/core/explorer-errors.ts @@ -1,8 +1,6 @@ -export type TransactionStatus = 'unknown' | 'failure' | 'success'; +export type UnknownError = { type: 'unknown' }; -type UnknownError = { type: 'unknown' }; - -type FunctionCallError = +export type FunctionCallError = | { type: 'compilationError'; error: CompilationError; @@ -16,7 +14,7 @@ type FunctionCallError = | { type: 'executionError'; error: string } | UnknownError; -type NewReceiptValidationError = +export type NewReceiptValidationError = | { type: 'invalidPredecessorId'; accountId: string } | { type: 'invalidReceiverId'; accountId: string } | { type: 'invalidSignerId'; accountId: string } @@ -30,14 +28,14 @@ type NewReceiptValidationError = | { type: 'actionsValidation' } | UnknownError; -type CompilationError = +export type CompilationError = | { type: 'codeDoesNotExist'; accountId: string } | { type: 'prepareError' } | { type: 'wasmerCompileError'; msg: string } | { type: 'unsupportedCompiler'; msg: string } | UnknownError; -type ReceiptActionError = +export type ReceiptActionError = | { type: 'accountAlreadyExists'; accountId: string; @@ -110,7 +108,7 @@ type ReceiptActionError = | { type: 'deleteAccountWithLargeState'; accountId: string } | UnknownError; -type ReceiptTransactionError = +export type ReceiptTransactionError = | { type: 'invalidAccessKeyError' } | { type: 'invalidSignerId'; signerId: string } | { type: 'signerDoesNotExist'; signerId: string } @@ -132,7 +130,7 @@ type ReceiptTransactionError = | { type: 'transactionSizeExceeded'; size: number; limit: number } | UnknownError; -type ReceiptExecutionStatusError = +export type ReceiptExecutionStatusError = | { type: 'action'; error: ReceiptActionError; @@ -142,114 +140,3 @@ type ReceiptExecutionStatusError = error: ReceiptTransactionError; } | UnknownError; - -export type ReceiptExecutionStatus = - | { - type: 'failure'; - error: ReceiptExecutionStatusError; - } - | { - type: 'successValue'; - value: string; - } - | { - type: 'successReceiptId'; - receiptId: string; - } - | { - type: 'unknown'; - }; - -export type Action = - | { - kind: 'createAccount'; - args: Record; - } - | { - kind: 'deployContract'; - args: { - code: string; - }; - } - | { - kind: 'functionCall'; - args: { - methodName: string; - args: string; - gas: number; - deposit: string; - }; - } - | { - kind: 'transfer'; - args: { - deposit: string; - }; - } - | { - kind: 'stake'; - args: { - stake: string; - publicKey: string; - }; - } - | { - kind: 'addKey'; - args: { - publicKey: string; - accessKey: { - nonce: number; - permission: - | { - type: 'fullAccess'; - } - | { - type: 'functionCall'; - contractId: string; - methodNames: string[]; - }; - }; - }; - } - | { - kind: 'deleteKey'; - args: { - publicKey: string; - }; - } - | { - kind: 'deleteAccount'; - args: { - beneficiaryId: string; - }; - }; - -export type NestedReceiptWithOutcome = { - id: string; - predecessorId: string; - receiverId: string; - actions: Action[]; - outcome: { - block: { - hash: string; - height: number; - timestamp: number; - }; - tokensBurnt: string; - gasBurnt: number; - status: ReceiptExecutionStatus; - logs: string[]; - nestedReceipts: NestedReceiptWithOutcome[]; - }; -}; - -export type Transaction = { - hash: string; - timestamp: number; - signerId: string; - receiverId: string; - fee: string; - amount: string; - status: TransactionStatus; - receipt: NestedReceiptWithOutcome; -}; diff --git a/common/types/core/explorer.schema.ts b/common/types/core/explorer.schema.ts new file mode 100644 index 000000000..29820bcb7 --- /dev/null +++ b/common/types/core/explorer.schema.ts @@ -0,0 +1,218 @@ +import { Net } from '@pc/database/clients/core'; +import * as RPC from '../rpc'; + +export namespace Old { + export type Action = { + kind: K extends string ? K : keyof K; + args: ActionArgs; + }; + + export type ActionArgs = + K extends string ? {} : K[keyof K]; + + export type Transaction = { + hash: string; + signerId: string; + receiverId: string; + blockHash: string; + blockTimestamp: number; + transactionIndex: number; + actions: Action[]; + }; +} + +import { ReceiptExecutionStatusError } from './explorer-errors'; +export * as Errors from './explorer-errors'; + +export type ReceiptExecutionStatus = + | { + type: 'failure'; + error: ReceiptExecutionStatusError; + } + | { + type: 'successValue'; + value: string; + } + | { + type: 'successReceiptId'; + receiptId: string; + } + | { + type: 'unknown'; + }; + +export type Action = + | { + kind: 'createAccount'; + args: Record; + } + | { + kind: 'deployContract'; + args: { + code: string; + }; + } + | { + kind: 'functionCall'; + args: { + methodName: string; + args: string; + gas: number; + deposit: string; + }; + } + | { + kind: 'transfer'; + args: { + deposit: string; + }; + } + | { + kind: 'stake'; + args: { + stake: string; + publicKey: string; + }; + } + | { + kind: 'addKey'; + args: { + publicKey: string; + accessKey: { + nonce: number; + permission: + | { + type: 'fullAccess'; + } + | { + type: 'functionCall'; + contractId: string; + methodNames: string[]; + }; + }; + }; + } + | { + kind: 'deleteKey'; + args: { + publicKey: string; + }; + } + | { + kind: 'deleteAccount'; + args: { + beneficiaryId: string; + }; + }; + +export type ActivityConnection = { + transactionHash: string; + receiptId?: string; + sender: string; + receiver: string; +}; + +type AccountBatchAction = { + kind: 'batch'; + actions: AccountActivityAction[]; +}; + +type AccountValidatorRewardAction = { + kind: 'validatorReward'; + blockHash: string; +}; + +export type AccountActivityAction = + | Action + | AccountValidatorRewardAction + | AccountBatchAction; + +export type AccountActivityWithConnection = AccountActivityAction & + ActivityConnection; + +export type ActivityConnectionActions = { + parentAction?: AccountActivityWithConnection; + childrenActions?: AccountActivityWithConnection[]; +}; + +export type ActivityActionItemAction = AccountActivityAction & + ActivityConnectionActions & + Omit; + +export type ActivityActionItem = { + involvedAccountId: string | null; + timestamp: number; + direction: 'inbound' | 'outbound'; + deltaAmount: string; + action: ActivityActionItemAction; +}; + +export type AccountActivity = { + items: ActivityActionItem[]; + cursor?: { + blockTimestamp: string; + shardId: number; + indexInChunk: number; + }; +}; + +export type TransactionStatus = 'unknown' | 'failure' | 'success'; + +export type NestedReceiptWithOutcome = { + id: string; + predecessorId: string; + receiverId: string; + actions: Action[]; + outcome: { + block: { + hash: string; + height: number; + timestamp: number; + }; + tokensBurnt: string; + gasBurnt: number; + status: ReceiptExecutionStatus; + logs: string[]; + nestedReceipts: NestedReceiptWithOutcome[]; + }; +}; + +export type Transaction = { + hash: string; + timestamp: number; + signerId: string; + receiverId: string; + fee: string; + amount: string; + status: TransactionStatus; + receipt: NestedReceiptWithOutcome; +}; + +export namespace Query { + export namespace Inputs { + export type Activity = { contractId: string; net: Net }; + export type BalanceChanges = { + net: Net; + receiptId: string; + accountIds: string[]; + }; + export type GetTransaction = { net: Net; hash: string }; + export type GetTransactions = { net: Net; contracts: string[] }; + } + + export namespace Outputs { + export type Activity = { items: ActivityActionItem[] }; + export type BalanceChanges = (string | null)[]; + export type GetTransaction = Transaction; + export type GetTransactions = (Old.Transaction & { + sourceContract: string; + })[]; + } + + export namespace Errors { + export type Activity = unknown; + export type BalanceChanges = unknown; + export type GetTransaction = unknown; + export type GetTransactions = unknown; + } +} diff --git a/common/types/core/index.ts b/common/types/core/index.ts new file mode 100644 index 000000000..8d379652b --- /dev/null +++ b/common/types/core/index.ts @@ -0,0 +1,3 @@ +export * as Projects from './projects.schema'; +export * as Users from './users.schema'; +export * as Explorer from './explorer.schema'; diff --git a/common/types/core/projects.schema.ts b/common/types/core/projects.schema.ts new file mode 100644 index 000000000..9db559d3b --- /dev/null +++ b/common/types/core/projects.schema.ts @@ -0,0 +1,98 @@ +import { + Project, + Org, + Contract, + ProjectTutorial, + ApiKey, + Environment, +} from '@pc/database/clients/core'; + +export namespace Query { + export namespace Inputs { + export type GetDetails = { slug: string }; + export type GetContracts = { project: string; environment: number }; + export type GetContract = { slug: string }; + export type List = void; + export type GetEnvironments = { project: string }; + export type GetKeys = { project: string }; + } + + export namespace Outputs { + export type GetDetails = Pick & { + org: Pick; + }; + export type GetContracts = Pick[]; + export type GetContract = Pick; + export type List = (Pick< + Project, + 'id' | 'name' | 'slug' | 'tutorial' | 'active' + > & { + org: Pick & { + isPersonal: boolean; + }; + })[]; + export type GetEnvironments = Pick[]; + export type GetKeys = (Pick & { + keySlug: ApiKey['slug']; + key: string; + })[]; + } + + export namespace Errors { + export type GetDetails = unknown; + export type GetContracts = unknown; + export type GetContract = unknown; + export type List = unknown; + export type GetEnvironments = unknown; + export type GetKeys = unknown; + } +} + +export namespace Mutation { + export namespace Inputs { + export type Create = { + org?: string; + name: string; + tutorial?: ProjectTutorial; + }; + export type EjectTutorial = { slug: string }; + export type Delete = { slug: string }; + export type AddContract = { + project: string; + environment: number; + address: string; + }; + export type RemoveContract = { slug: string }; + export type RotateKey = { slug: string }; + export type GenerateKey = { project: string; description: string }; + export type DeleteKey = { slug: string }; + } + + export namespace Outputs { + export type Create = Pick; + export type EjectTutorial = void; + export type Delete = void; + export type AddContract = Pick; + export type RemoveContract = void; + export type RotateKey = Pick & { + keySlug: ApiKey['slug']; + key: string; + }; + export type GenerateKey = Pick & { + keySlug: ApiKey['slug']; + key: string; + }; + export type DeleteKey = void; + } + + export namespace Errors { + export type Create = unknown; + export type EjectTutorial = unknown; + export type Delete = unknown; + export type AddContract = unknown; + export type RemoveContract = unknown; + export type RotateKey = unknown; + export type GenerateKey = unknown; + export type DeleteKey = unknown; + } +} diff --git a/common/types/core/users.schema.ts b/common/types/core/users.schema.ts new file mode 100644 index 000000000..7037d9661 --- /dev/null +++ b/common/types/core/users.schema.ts @@ -0,0 +1,106 @@ +import { + Org, + OrgRole, + User, + OrgMember, + OrgInvite, +} from '@pc/database/clients/core'; + +export namespace Query { + export namespace Inputs { + export type GetAccountDetails = void; + export type ListOrgsWithOnlyAdmin = void; + export type ListOrgMembers = { org: Org['slug'] }; + export type ListOrgs = void; + } + + export namespace Outputs { + export type GetAccountDetails = { + uid?: string; + email?: string; + name?: string; + photoUrl?: string; + }; + export type ListOrgsWithOnlyAdmin = Pick[]; + export type ListOrgMembers = (Pick & + ( + | { + isInvite: true; + user: { + uid: null; + email: OrgInvite['email']; + }; + } + | { + isInvite: false; + user: Pick; + } + ))[]; + export type ListOrgs = (Pick & { + isPersonal: boolean; + })[]; + } + + export namespace Errors { + export type GetAccountDetails = unknown; + export type ListOrgsWithOnlyAdmin = unknown; + export type ListOrgMembers = unknown; + export type ListOrgs = unknown; + } +} + +export namespace Mutation { + export namespace Inputs { + export type DeleteAccount = void; + export type CreateOrg = { name: string }; + export type InviteToOrg = { + org: Org['slug']; + email: string; + role: OrgRole; + }; + export type AcceptOrgInvite = { token: string }; + export type DeleteOrg = { org: Org['slug'] }; + export type ChangeOrgRole = { + org: Org['slug']; + role: OrgRole; + user: User['uid']; + }; + export type RemoveFromOrg = { + org: Org['slug']; + user: User['uid']; + }; + export type RemoveOrgInvite = { + org: Org['slug']; + email: string; + }; + export type ResetPassword = { email: string }; + } + + export namespace Outputs { + export type DeleteAccount = void; + export type CreateOrg = Pick & { + isPersonal: false; + }; + export type InviteToOrg = void; + export type AcceptOrgInvite = Pick; + export type DeleteOrg = void; + export type ChangeOrgRole = Pick & { + user: Pick; + }; + export type RemoveFromOrg = void; + export type RemoveOrgInvite = void; + export type ResetPassword = void; + } + + export namespace Errors { + export type DeleteAccount = unknown; + export type CreateOrg = unknown; + export type InviteToOrg = unknown; + export type AcceptOrgInvite = unknown; + export type DeleteOrg = unknown; + export type ChangeOrgRole = unknown; + export type RemoveFromOrg = unknown; + export type RemoveOrgInvite = unknown; + export type ResetPassword = unknown; + } +} diff --git a/backend/src/core/near-rpc/types.ts b/common/types/rpc.ts similarity index 100% rename from backend/src/core/near-rpc/types.ts rename to common/types/rpc.ts diff --git a/common/types/rpcstats/index.ts b/common/types/rpcstats/index.ts new file mode 100644 index 000000000..7b5eb63a6 --- /dev/null +++ b/common/types/rpcstats/index.ts @@ -0,0 +1 @@ +export * as RpcStats from './rpcstats.schema'; diff --git a/common/types/rpcstats/rpcstats.schema.ts b/common/types/rpcstats/rpcstats.schema.ts new file mode 100644 index 000000000..4b3bc2c18 --- /dev/null +++ b/common/types/rpcstats/rpcstats.schema.ts @@ -0,0 +1,76 @@ +import { Net } from '@pc/database/clients/core'; + +export enum DateTimeResolution { + FIFTEEN_SECONDS = 'FIFTEEN_SECONDS', + ONE_MINUTE = 'ONE_MINUTE', + ONE_HOUR = 'ONE_HOUR', + ONE_DAY = 'ONE_DAY', +} + +export type TimeRangeValue = + | '15_MINS' + | '1_HRS' + | '24_HRS' + | '7_DAYS' + | '30_DAYS'; + +export enum MetricGroupBy { + DATE = 'date', + ENDPOINT = 'endpoint', +} + +export type BaseEndpointMetric = { + endpointMethod: string; + successCount: number; + errorCount: number; + totalCount: number; +}; + +export type Metrics = { + apiKeyIdentifier: string; + endpointGroup?: string; + endpointMethod: string; + network: Net; + windowStart?: string; + windowEnd?: string; + successCount: number; + errorCount: number; + minLatency: number; + maxLatency: number; + meanLatency: number; +}; + +export type MetricsPage = { + count: number; + page: Metrics[]; +}; + +export namespace Query { + export namespace Inputs { + export type EndpointMetrics = { + projectSlug: string; + environmentSubId: number; + startDateTime: string; + endDateTime: string; + skip?: number; + take?: number; + pagingDateTime?: Date; + filter: + | { + type: MetricGroupBy.DATE; + dateTimeResolution: DateTimeResolution; + } + | { + type: MetricGroupBy.ENDPOINT; + }; + }; + } + + export namespace Outputs { + export type EndpointMetrics = MetricsPage; + } + + export namespace Errors { + export type EndpointMetrics = unknown; + } +} diff --git a/frontend/components/explorer/activity/AccountActivityBadge.tsx b/frontend/components/explorer/activity/AccountActivityBadge.tsx index 81635e3d3..791d25a92 100644 --- a/frontend/components/explorer/activity/AccountActivityBadge.tsx +++ b/frontend/components/explorer/activity/AccountActivityBadge.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import * as React from 'react'; import { Tooltip } from '@/components/lib/Tooltip'; @@ -5,10 +6,9 @@ import { styled } from '@/styles/stitches'; import { truncateMiddle } from '@/utils/truncate-middle'; import TransactionLink from '../utils/TransactionLink'; -import type { AccountActivityAction, ActivityConnectionActions } from './types'; type Props = { - action: AccountActivityAction | NonNullable; + action: Explorer.AccountActivityAction | Explorer.AccountActivityWithConnection; }; const Wrapper = styled('div', { @@ -22,7 +22,7 @@ const Wrapper = styled('div', { fontSize: 12, }); -const ACTION_NAMES: Record = { +const ACTION_NAMES: Record = { transfer: 'Transfer', stake: 'Restake', deployContract: 'Contract Deployed', diff --git a/frontend/components/explorer/activity/AccountActivityView.tsx b/frontend/components/explorer/activity/AccountActivityView.tsx index 78f7a1fda..5f45c6fba 100644 --- a/frontend/components/explorer/activity/AccountActivityView.tsx +++ b/frontend/components/explorer/activity/AccountActivityView.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import JSBI from 'jsbi'; import { DateTime } from 'luxon'; import * as React from 'react'; @@ -27,10 +28,9 @@ import { TableRow, TableWrapper, } from './styles'; -import type { AccountActivityAction, AccountActivityElement, ActivityConnectionActions } from './types'; type RowProps = { - item: AccountActivityElement; + item: Explorer.ActivityActionItem; }; const ActivityItemActionWrapper = styled('div', { @@ -48,7 +48,7 @@ const ActivityItemTitle = styled('span', { }); const ActivityItemAction: React.FC<{ - action: NonNullable | AccountActivityAction; + action: Explorer.AccountActivityWithConnection | Explorer.AccountActivityAction; }> = ({ action }) => { const badge = ( <> @@ -209,9 +209,11 @@ type Props = { const AccountActivityView: React.FC = ({ accountId }) => { const net = useNet(); - const query = useSWR<{ items: AccountActivityElement[] }>( - accountId ? ['explorer/activity', accountId, net] : null, - () => unauthenticatedPost(`/explorer/activity/`, { contractId: accountId, net }), + const query = useSWR(accountId ? ['explorer/activity', accountId, net] : null, () => + unauthenticatedPost('/explorer/activity', { + contractId: accountId, + net, + }), ); if (!accountId) { diff --git a/frontend/components/explorer/activity/types.ts b/frontend/components/explorer/activity/types.ts deleted file mode 100644 index beacee0e8..000000000 --- a/frontend/components/explorer/activity/types.ts +++ /dev/null @@ -1,100 +0,0 @@ -type Action = - | { - kind: 'createAccount'; - args: Record; - } - | { - kind: 'deployContract'; - args: { - code: string; - }; - } - | { - kind: 'functionCall'; - args: { - methodName: string; - args: string; - gas: number; - deposit: string; - }; - } - | { - kind: 'transfer'; - args: { - deposit: string; - }; - } - | { - kind: 'stake'; - args: { - stake: string; - publicKey: string; - }; - } - | { - kind: 'addKey'; - args: { - publicKey: string; - accessKey: { - nonce: number; - permission: - | { - type: 'fullAccess'; - } - | { - type: 'functionCall'; - contractId: string; - methodNames: string[]; - }; - }; - }; - } - | { - kind: 'deleteKey'; - args: { - publicKey: string; - }; - } - | { - kind: 'deleteAccount'; - args: { - beneficiaryId: string; - }; - }; - -export type ActivityConnectionActions = { - parentAction?: AccountActivityAction & ActivityConnection; - childrenActions?: (AccountActivityAction & ActivityConnection)[]; -}; - -export type ActivityConnection = { - transactionHash: string; - receiptId?: string; - sender: string; - receiver: string; -}; - -type AccountBatchAction = { - kind: 'batch'; - actions: AccountActivityAction[]; -}; - -type AccountValidatorRewardAction = { - kind: 'validatorReward'; - blockHash: string; -}; - -export type AccountActivityAction = Action | AccountValidatorRewardAction | AccountBatchAction; - -export type AccountActivityElement = { - involvedAccountId: string | null; - cursor: { - blockTimestamp: string; - shardId: number; - indexInChunk: number; - }; - timestamp: number; - direction: 'inbound' | 'outbound'; - deltaAmount: string; - action: AccountActivityAction & ActivityConnectionActions & Omit; -}; diff --git a/frontend/components/explorer/transaction/InspectReceipt.tsx b/frontend/components/explorer/transaction/InspectReceipt.tsx index 0389f559a..a73a2d6ad 100644 --- a/frontend/components/explorer/transaction/InspectReceipt.tsx +++ b/frontend/components/explorer/transaction/InspectReceipt.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import JSBI from 'jsbi'; import * as React from 'react'; import useSWR from 'swr'; @@ -11,10 +12,9 @@ import AccountLink from '../utils/AccountLink'; import BlockLink from '../utils/BlockLink'; import Gas from '../utils/Gas'; import { NearAmount } from '../utils/NearAmount'; -import type { Action, NestedReceiptWithOutcome } from './types'; type Props = { - receipt: NestedReceiptWithOutcome; + receipt: Explorer.NestedReceiptWithOutcome; }; const Table = styled('table', { @@ -36,12 +36,12 @@ const BalanceAmount = styled('div', { color: 'var(--color-cta-neutral-highlight_', }); -const getDeposit = (actions: Action[]): JSBI => { +const getDeposit = (actions: Explorer.Action[]): JSBI => { return actions .map((action) => ('deposit' in action.args ? JSBI.BigInt(action.args.deposit) : BI.zero)) .reduce((accumulator, deposit) => JSBI.add(accumulator, deposit), BI.zero); }; -const getGasAttached = (actions: Action[]): JSBI => { +const getGasAttached = (actions: Explorer.Action[]): JSBI => { const gasAttached = actions .map((action) => action.args) .filter( @@ -62,14 +62,12 @@ const getGasAttached = (actions: Action[]): JSBI => { const InspectReceipt: React.FC = React.memo(({ receipt: { id, ...receipt } }) => { const net = useNet(); - const query = useSWR<(string | null)[]>( - ['explorer/balanceChanges', net, receipt.predecessorId, receipt.receiverId], - () => - unauthenticatedPost(`/explorer/balanceChanges/`, { - net, - receiptId: id, - accountIds: [receipt.predecessorId, receipt.receiverId], - }), + const query = useSWR(['explorer/balanceChanges', net, receipt.predecessorId, receipt.receiverId], () => + unauthenticatedPost(`/explorer/balanceChanges`, { + net, + receiptId: id, + accountIds: [receipt.predecessorId, receipt.receiverId], + }), ); const predecessorBalance = query.data?.[0]; const receiverBalance = query.data?.[0]; diff --git a/frontend/components/explorer/transaction/ReceiptDetails.tsx b/frontend/components/explorer/transaction/ReceiptDetails.tsx index a1d17fd6a..c7e98c2c3 100644 --- a/frontend/components/explorer/transaction/ReceiptDetails.tsx +++ b/frontend/components/explorer/transaction/ReceiptDetails.tsx @@ -1,13 +1,12 @@ +import type { Explorer } from '@pc/common/types/core'; import * as React from 'react'; import { CodeBlock } from '@/components/lib/CodeBlock'; import { H4 } from '@/components/lib/Heading'; import { styled } from '@/styles/stitches'; -import type { NestedReceiptWithOutcome } from './types'; - type Props = { - receipt: NestedReceiptWithOutcome; + receipt: Explorer.NestedReceiptWithOutcome; }; const DetailsWrapper = styled('div', { diff --git a/frontend/components/explorer/transaction/ReceiptInfo.tsx b/frontend/components/explorer/transaction/ReceiptInfo.tsx index e8d107c6b..e4040371f 100644 --- a/frontend/components/explorer/transaction/ReceiptInfo.tsx +++ b/frontend/components/explorer/transaction/ReceiptInfo.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import * as React from 'react'; import * as Tabs from '@/components/lib/Tabs'; @@ -5,10 +6,9 @@ import { StableId } from '@/utils/stable-ids'; import InspectReceipt from './InspectReceipt'; import ReceiptDetails from './ReceiptDetails'; -import type { NestedReceiptWithOutcome } from './types'; type Props = { - receipt: NestedReceiptWithOutcome; + receipt: Explorer.NestedReceiptWithOutcome; }; const ReceiptInfo: React.FC = React.memo(({ receipt }) => { diff --git a/frontend/components/explorer/transaction/ReceiptKind.tsx b/frontend/components/explorer/transaction/ReceiptKind.tsx index dfc6f0e3a..748854b58 100644 --- a/frontend/components/explorer/transaction/ReceiptKind.tsx +++ b/frontend/components/explorer/transaction/ReceiptKind.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import * as React from 'react'; import { styled } from '@/styles/stitches'; @@ -6,10 +7,9 @@ import { StableId } from '@/utils/stable-ids'; import { Button } from '../../lib/Button'; import CodeArgs from '../utils/CodeArgs'; import { NearAmount } from '../utils/NearAmount'; -import type { Action } from './types'; interface Props { - action: Action; + action: Explorer.Action; onClick: React.MouseEventHandler; isTxTypeActive: boolean; } diff --git a/frontend/components/explorer/transaction/TransactionActions.tsx b/frontend/components/explorer/transaction/TransactionActions.tsx index 363ff5cd2..6d230a82a 100644 --- a/frontend/components/explorer/transaction/TransactionActions.tsx +++ b/frontend/components/explorer/transaction/TransactionActions.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import type { DurationLikeObject } from 'luxon'; import { Duration, Interval } from 'luxon'; import * as React from 'react'; @@ -16,7 +17,6 @@ import { StableId } from '@/utils/stable-ids'; import { Button } from '../../lib/Button'; import TransactionReceipt from './TransactionReceipt'; -import type { Transaction } from './types'; type Props = { transactionHash: string | null; @@ -49,7 +49,7 @@ const TitleWrapper = styled('div', { function customErrorRetry( err: any, __: string, - config: Readonly>>, + config: Readonly>>, revalidate: Revalidator, opts: Required, ): void { @@ -76,9 +76,13 @@ function customErrorRetry( const TransactionActions: React.FC = React.memo(({ transactionHash }) => { const net = useNet(); - const query = useSWR( + const query = useSWR( transactionHash ? ['explorer/transaction', transactionHash, net] : null, - () => unauthenticatedPost(`/explorer/transaction/`, { hash: transactionHash, net }), + () => + unauthenticatedPost('/explorer/transaction', { + hash: transactionHash!, + net, + }), { onErrorRetry: customErrorRetry, // TODO currently this is a quick hack to load TXs that may have pending receipts that are scheduled to execute in the next block. We could stop refreshing once we get the last receipt's execution outcome timestamp. @@ -103,7 +107,7 @@ const TransactionActions: React.FC = React.memo(({ transactionHash }) => TransactionActions.displayName = 'TransactionActions'; type ListProps = { - transaction: Transaction; + transaction: Explorer.Transaction; }; // see https://github.com/moment/luxon/issues/1134 @@ -137,7 +141,7 @@ export const TransactionReceiptContext = React.createContext = React.memo(({ transaction }) => { const preCollectedReceipts = React.useMemo(() => { const receipts = [] as ToogleReceipt[]; - const collectReceiptHashes: any = (receipt: Transaction['receipt']) => { + const collectReceiptHashes: any = (receipt: Explorer.Transaction['receipt']) => { const id = receipt.id; receipts.push({ id, active: false }); return receipt.outcome.nestedReceipts diff --git a/frontend/components/explorer/transaction/TransactionReceipt.tsx b/frontend/components/explorer/transaction/TransactionReceipt.tsx index 8fa3316e1..bb560c2fd 100644 --- a/frontend/components/explorer/transaction/TransactionReceipt.tsx +++ b/frontend/components/explorer/transaction/TransactionReceipt.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import * as React from 'react'; import { styled } from '@/styles/stitches'; @@ -6,12 +7,11 @@ import AccountLink from '../utils/AccountLink'; import ReceiptInfo from './ReceiptInfo'; import ReceiptKind from './ReceiptKind'; import { TransactionReceiptContext } from './TransactionActions'; -import type { NestedReceiptWithOutcome } from './types'; type Props = { - receipt: NestedReceiptWithOutcome; + receipt: Explorer.NestedReceiptWithOutcome; convertionReceipt: boolean; - fellowOutgoingReceipts: NestedReceiptWithOutcome[]; + fellowOutgoingReceipts: Explorer.NestedReceiptWithOutcome[]; className: string; customCss?: React.CSSProperties; }; diff --git a/frontend/components/explorer/transactions/ActionGroup.tsx b/frontend/components/explorer/transactions/ActionGroup.tsx index 383ab718d..d1d333e6c 100644 --- a/frontend/components/explorer/transactions/ActionGroup.tsx +++ b/frontend/components/explorer/transactions/ActionGroup.tsx @@ -1,17 +1,16 @@ +import type { Explorer } from '@pc/common/types/core'; import JSBI from 'jsbi'; +import type { FinalityStatus } from '@/modules/contracts/hooks/recent-transactions'; import BatchTransactionIcon from '@/public/static/images/icon-m-batch.svg'; -import type { FinalityStatus } from '@/utils/types'; import ActionRow from './ActionRow'; import type { ViewMode } from './ActionRowBlock'; import ActionRowBlock from './ActionRowBlock'; import ActionsList from './ActionsList'; -import type { TransactionInfo } from './types'; -import type { Receipt } from './types'; interface Props { - actionGroup: Receipt | TransactionInfo; + actionGroup: Explorer.Old.Transaction; detailsLink?: React.ReactNode; status?: React.ReactNode; viewMode?: ViewMode; diff --git a/frontend/components/explorer/transactions/ActionMessage.tsx b/frontend/components/explorer/transactions/ActionMessage.tsx index 371948fd0..575d5076b 100644 --- a/frontend/components/explorer/transactions/ActionMessage.tsx +++ b/frontend/components/explorer/transactions/ActionMessage.tsx @@ -1,44 +1,36 @@ +import type { Explorer } from '@pc/common/types/core'; +import type * as RPC from '@pc/common/types/rpc'; + import AccountLink from '../utils/AccountLink'; import Balance from '../utils/Balance'; import CodeArgs from '../utils/CodeArgs'; -import type * as T from './types'; -export interface Props { - actionKind: keyof TransactionMessageRenderers; - actionArgs: A; +export interface Props { + actionKind: Explorer.Old.Action['kind']; + actionArgs: Explorer.Old.ActionArgs; receiverId: string; showDetails?: boolean; } -type AnyAction = - | T.CreateAccount - | T.DeleteAccount - | T.DeployContract - | T.FunctionCall - | T.Transfer - | T.Stake - | T.AddKey - | T.DeleteKey; - interface TransactionMessageRenderers { - CreateAccount: React.FC>; - DeleteAccount: React.FC>; - DeployContract: React.FC>; - FunctionCall: React.FC>; - Transfer: React.FC>; - Stake: React.FC>; - AddKey: React.FC>; - DeleteKey: React.FC>; + CreateAccount: React.FC>; + DeleteAccount: React.FC>; + DeployContract: React.FC>; + FunctionCall: React.FC>; + Transfer: React.FC>; + Stake: React.FC>; + AddKey: React.FC>; + DeleteKey: React.FC>; } const transactionMessageRenderers: TransactionMessageRenderers = { - CreateAccount: ({ receiverId }: Props) => ( + CreateAccount: ({ receiverId }: Props<'CreateAccount'>) => ( <> <>New account created: ), - DeleteAccount: ({ receiverId, actionArgs }: Props) => ( + DeleteAccount: ({ receiverId, actionArgs }: Props) => ( <> <>Delete account @@ -46,13 +38,13 @@ const transactionMessageRenderers: TransactionMessageRenderers = { ), - DeployContract: ({ receiverId }: Props) => ( + DeployContract: ({ receiverId }: Props) => ( <> <>Contract deployed: ), - FunctionCall: ({ receiverId, actionArgs, showDetails }: Props) => { + FunctionCall: ({ receiverId, actionArgs, showDetails }: Props) => { let args; if (showDetails) { if (typeof actionArgs.args === 'undefined') { @@ -78,7 +70,7 @@ const transactionMessageRenderers: TransactionMessageRenderers = { ); }, - Transfer: ({ receiverId, actionArgs: { deposit } }: Props) => ( + Transfer: ({ receiverId, actionArgs: { deposit } }: Props) => ( <> <>Transferred @@ -86,38 +78,27 @@ const transactionMessageRenderers: TransactionMessageRenderers = { ), - Stake: ({ actionArgs: { stake, public_key } }: Props) => ( + Stake: ({ actionArgs: { stake, public_key } }: Props) => ( <> <>Staked: <>with ${public_key.substring(0, 15)}... ), - AddKey: ({ receiverId, actionArgs }: Props) => ( + AddKey: ({ receiverId, actionArgs }: Props) => ( <> {typeof actionArgs.access_key.permission === 'object' ? ( - actionArgs.access_key.permission.permission_kind ? ( - <> - <>New key added for - - {`: ${actionArgs.public_key.substring(0, 15)}...`} -

- <>{`with permission ${actionArgs.access_key.permission.permission_kind}`} -

- - ) : ( - <> - <>Access key added for contract - - {`: ${actionArgs.public_key.substring(0, 15)}...`} -

- {`with permission to call ${ - actionArgs.access_key.permission.FunctionCall.method_names.length > 0 - ? `(${actionArgs.access_key.permission.FunctionCall.method_names.join(', ')})` - : 'any' - } methods`} -

- - ) + <> + <>Access key added for contract + + {`: ${actionArgs.public_key.substring(0, 15)}...`} +

+ {`with permission to call ${ + actionArgs.access_key.permission.FunctionCall.method_names.length > 0 + ? `(${actionArgs.access_key.permission.FunctionCall.method_names.join(', ')})` + : 'any' + } methods`} +

+ ) : ( <> <>New key added for @@ -130,12 +111,12 @@ const transactionMessageRenderers: TransactionMessageRenderers = { )} ), - DeleteKey: ({ actionArgs: { public_key } }: Props) => ( + DeleteKey: ({ actionArgs: { public_key } }: Props) => ( <>{`Key deleted: ${public_key.substring(0, 15)}...`} ), }; -const ActionMessage = (props: Props) => { +const ActionMessage = (props: Props) => { const MessageRenderer = transactionMessageRenderers[props.actionKind]; if (MessageRenderer === undefined) { return ( diff --git a/frontend/components/explorer/transactions/ActionRow.tsx b/frontend/components/explorer/transactions/ActionRow.tsx index d9f724cac..22e6269b9 100644 --- a/frontend/components/explorer/transactions/ActionRow.tsx +++ b/frontend/components/explorer/transactions/ActionRow.tsx @@ -1,3 +1,4 @@ +import type { Explorer } from '@pc/common/types/core'; import { PureComponent } from 'react'; import actionIcons from './ActionIcons'; @@ -5,10 +6,9 @@ import ActionMessage from './ActionMessage'; import type { DetalizationMode, ViewMode } from './ActionRowBlock'; import ActionRowBlock from './ActionRowBlock'; // import * as T from "../../libraries/explorer-wamp/transactions"; -import type * as T from './types'; export interface Props { - action: T.Action; + action: Explorer.Old.Action; blockTimestamp?: number; className: string; detailsLink?: React.ReactNode; diff --git a/frontend/components/explorer/transactions/ActionsList.tsx b/frontend/components/explorer/transactions/ActionsList.tsx index 29de75ce5..c9c9ade83 100644 --- a/frontend/components/explorer/transactions/ActionsList.tsx +++ b/frontend/components/explorer/transactions/ActionsList.tsx @@ -1,12 +1,12 @@ +import type { Explorer } from '@pc/common/types/core'; import { PureComponent } from 'react'; import ActionRow from './ActionRow'; import type { DetalizationMode, ViewMode } from './ActionRowBlock'; // import * as T from "../../libraries/explorer-wamp/transactions"; -import type * as T from './types'; export interface Props { - actions: T.Action[]; + actions: Explorer.Old.Action[]; blockTimestamp: number; detailsLink?: React.ReactNode; detalizationMode?: DetalizationMode; diff --git a/frontend/components/explorer/transactions/TransactionAction.tsx b/frontend/components/explorer/transactions/TransactionAction.tsx index 80dc3069d..c01f03967 100644 --- a/frontend/components/explorer/transactions/TransactionAction.tsx +++ b/frontend/components/explorer/transactions/TransactionAction.tsx @@ -1,26 +1,28 @@ // import { Translate } from "react-localize-redux"; +import type { Explorer } from '@pc/common/types/core'; +import type * as RPC from '@pc/common/types/rpc'; +import type { Net } from '@pc/database/clients/core'; import { DateTime } from 'luxon'; import { PureComponent } from 'react'; +import type { FinalityStatus } from '@/modules/contracts/hooks/recent-transactions'; import config from '@/utils/config'; -import type { NetOption } from '@/utils/types'; import TransactionLink from '../utils/TransactionLink'; import ActionGroup from './ActionGroup'; import type { ViewMode } from './ActionRowBlock'; import TransactionExecutionStatus from './TransactionExecutionStatus'; // import TransactionsApi, * as T from "../../libraries/explorer-wamp/transactions"; // TODO -import type * as T from './types'; export interface Props { - transaction: T.Transaction; + transaction: Explorer.Old.Transaction; viewMode?: ViewMode; - net: NetOption; + net: Net; finalityStatus?: FinalityStatus; } interface State { - status?: T.ExecutionStatus; + status?: keyof RPC.FinalExecutionStatus; } class TransactionAction extends PureComponent { @@ -48,7 +50,7 @@ class TransactionAction extends PureComponent { return ( <> @@ -122,13 +124,12 @@ class TransactionAction extends PureComponent { // NOTE: this is a custom implementation for console. Explorer requests this status through // its backend from dedicated archival nodes -import type { FinalityStatus } from '@/utils/types'; import { NetContext } from '../utils/NetContext'; -import type { ExecutionStatus, TransactionInfo } from './types'; + async function getTransactionStatus( - transaction: TransactionInfo, - net: NetOption, + transaction: Explorer.Old.Transaction, + net: Net, forceArchival?: boolean, ): Promise { let rpcUrl; @@ -174,7 +175,7 @@ async function getTransactionStatus( throw new Error('Failed to fetch transaction status'); } const transactionExtraInfo = res.result; - const status = Object.keys(transactionExtraInfo.status)[0] as ExecutionStatus; + const status = Object.keys(transactionExtraInfo.status)[0] as keyof RPC.FinalExecutionStatus; return status; } diff --git a/frontend/components/explorer/transactions/TransactionExecutionStatus.tsx b/frontend/components/explorer/transactions/TransactionExecutionStatus.tsx index 40be15477..e1e6bc740 100644 --- a/frontend/components/explorer/transactions/TransactionExecutionStatus.tsx +++ b/frontend/components/explorer/transactions/TransactionExecutionStatus.tsx @@ -1,7 +1,7 @@ -import type { ExecutionStatus } from './types'; +import type * as RPC from '@pc/common/types/rpc'; export interface Props { - status: ExecutionStatus; + status: keyof RPC.FinalExecutionStatus; } const TransactionExecutionStatusComponent = ({ status }: Props) => { let statusText; diff --git a/frontend/components/explorer/transactions/types.ts b/frontend/components/explorer/transactions/types.ts deleted file mode 100644 index 5f580bc41..000000000 --- a/frontend/components/explorer/transactions/types.ts +++ /dev/null @@ -1,145 +0,0 @@ -export type ExecutionStatus = 'NotStarted' | 'Started' | 'Failure' | 'SuccessValue'; - -export interface TransactionBaseInfo { - hash: string; - signerId: string; - receiverId: string; - blockHash: string; - blockTimestamp: number; - transactionIndex: number; - actions: Action[]; -} -export type TransactionInfo = TransactionBaseInfo & { - status?: ExecutionStatus; -}; - -export type CreateAccount = Record; - -export interface DeleteAccount { - beneficiary_id: string; -} - -export type DeployContract = Record; - -export interface FunctionCall { - args: string; - deposit: string; - gas: number; - method_name: string; -} - -export interface Transfer { - deposit: string; -} - -export interface Stake { - stake: string; - public_key: string; -} - -export interface AddKey { - access_key: any; - public_key: string; -} - -export interface DeleteKey { - public_key: string; -} - -export interface RpcAction { - CreateAccount: CreateAccount; - DeleteAccount: DeleteAccount; - DeployContract: DeployContract; - FunctionCall: FunctionCall; - Transfer: Transfer; - Stake: Stake; - AddKey: AddKey; - DeleteKey: DeleteKey; -} - -export interface Action { - kind: keyof RpcAction; - args: RpcAction[keyof RpcAction] | Record; -} - -export interface ReceiptSuccessValue { - SuccessValue: string | null; -} - -export interface ReceiptFailure { - Failure: any; -} - -export interface ReceiptSuccessId { - SuccessReceiptId: string; -} - -export type ReceiptStatus = ReceiptSuccessValue | ReceiptFailure | ReceiptSuccessId | string; - -export interface Outcome { - tokens_burnt: string; - logs: string[]; - receipt_ids: string[]; - status: ReceiptStatus; - gas_burnt: number; -} - -export interface ReceiptOutcome { - id: string; - outcome: Outcome; - block_hash: string; -} - -export interface ReceiptsOutcomeWrapper { - receiptsOutcome?: ReceiptOutcome[]; -} - -export interface NestedReceiptWithOutcome { - actions?: Action[]; - block_hash: string; - outcome: ReceiptExecutionOutcome; - predecessor_id: string; - receipt_id: string; - receiver_id: string; -} - -export interface ReceiptExecutionOutcome { - tokens_burnt: string; - logs: string[]; - outgoing_receipts?: NestedReceiptWithOutcome[]; - status: ReceiptStatus; - gas_burnt: number; -} - -export interface TransactionOutcome { - id: string; - outcome: Outcome; - block_hash: string; -} - -export interface TransactionOutcomeWrapper { - transactionOutcome?: TransactionOutcome; -} - -export type Transaction = TransactionInfo & - ReceiptsOutcomeWrapper & - TransactionOutcomeWrapper & { receipt?: NestedReceiptWithOutcome } & { sourceContract?: string }; // custom for console - -export interface TxPagination { - endTimestamp: number; - transactionIndex: number; -} - -export interface Receipt { - actions: Action[]; - blockTimestamp: number; - receiptId: string; - gasBurnt: number; - receiverId: string; - signerId: string; - status?: ReceiptExecutionStatus; - originatedFromTransactionHash?: string | null; - tokensBurnt: string; -} - -export type ReceiptExecutionStatus = 'Unknown' | 'Failure' | 'SuccessValue' | 'SuccessReceiptId'; diff --git a/frontend/components/explorer/utils/NetContext.tsx b/frontend/components/explorer/utils/NetContext.tsx index 6bab5cc70..9e19de6fe 100644 --- a/frontend/components/explorer/utils/NetContext.tsx +++ b/frontend/components/explorer/utils/NetContext.tsx @@ -1,5 +1,4 @@ +import type { Net } from '@pc/database/clients/core'; import React from 'react'; -import type { NetOption } from '@/utils/types'; - -export const NetContext = React.createContext(null); +export const NetContext = React.createContext(null); diff --git a/frontend/components/layouts/DashboardLayout/DashboardLayout.tsx b/frontend/components/layouts/DashboardLayout/DashboardLayout.tsx index 88c4e2d78..ef1ff7f3e 100644 --- a/frontend/components/layouts/DashboardLayout/DashboardLayout.tsx +++ b/frontend/components/layouts/DashboardLayout/DashboardLayout.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import type { ReactNode } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; import { NftInfoCard } from '@/modules/core/components/NftInfoCard'; @@ -7,11 +7,10 @@ import { Footer } from '../Footer'; import { Header } from './Header'; import { Sidebar } from './Sidebar'; import * as S from './styles'; -import type { Redirect } from './types'; interface Props { children: ReactNode; - redirect?: Redirect; + redirect?: ComponentProps['redirect']; } export function DashboardLayout({ children, redirect }: Props) { diff --git a/frontend/components/layouts/DashboardLayout/EnvironmentSelector/EnvironmentSelector.tsx b/frontend/components/layouts/DashboardLayout/EnvironmentSelector/EnvironmentSelector.tsx index dddc58d0b..ce9af7eea 100644 --- a/frontend/components/layouts/DashboardLayout/EnvironmentSelector/EnvironmentSelector.tsx +++ b/frontend/components/layouts/DashboardLayout/EnvironmentSelector/EnvironmentSelector.tsx @@ -1,9 +1,12 @@ +import type { Api } from '@pc/common/types/api'; + import * as DropdownMenu from '@/components/lib/DropdownMenu'; import { SubnetIcon } from '@/components/lib/SubnetIcon'; import { useProjectSelector, useSelectedProject } from '@/hooks/selected-project'; import analytics from '@/utils/analytics'; import { StableId } from '@/utils/stable-ids'; -import type { Environment } from '@/utils/types'; + +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; interface Props { onBeforeChange?: (change: () => void) => void; diff --git a/frontend/components/layouts/DashboardLayout/Header/Header.tsx b/frontend/components/layouts/DashboardLayout/Header/Header.tsx index 3002b64ad..9c30b4bf1 100644 --- a/frontend/components/layouts/DashboardLayout/Header/Header.tsx +++ b/frontend/components/layouts/DashboardLayout/Header/Header.tsx @@ -9,11 +9,14 @@ import { UserFullDropdown } from '@/components/lib/UserFullDropdown'; import { ConfirmModal } from '@/components/modals/ConfirmModal'; import { EnvironmentSelector } from '../EnvironmentSelector'; -import type { Redirect } from '../types'; import * as S from './styles'; type Props = ComponentProps & { - redirect?: Redirect; + redirect?: { + environmentChange?: boolean; + projectChange?: boolean; + url: string; + }; }; export function Header({ redirect, ...props }: Props) { diff --git a/frontend/components/layouts/DashboardLayout/ProjectSelector/ProjectSelector.tsx b/frontend/components/layouts/DashboardLayout/ProjectSelector/ProjectSelector.tsx index c57202937..7054aff63 100644 --- a/frontend/components/layouts/DashboardLayout/ProjectSelector/ProjectSelector.tsx +++ b/frontend/components/layouts/DashboardLayout/ProjectSelector/ProjectSelector.tsx @@ -10,7 +10,6 @@ import { useProjectGroups } from '@/hooks/projects'; import { useProjectSelector, useSelectedProject } from '@/hooks/selected-project'; import analytics from '@/utils/analytics'; import { StableId } from '@/utils/stable-ids'; -import type { Project } from '@/utils/types'; interface Props { onBeforeChange?: (change: () => void) => void; @@ -22,7 +21,7 @@ export function ProjectSelector(props: Props) { const { projectGroups } = useProjectGroups(); const router = useRouter(); - function onSelectProject(project: Project) { + function onSelectProject(project: NonNullable[number][1][number]) { if (props.onBeforeChange) { props.onBeforeChange(() => { selectProject(project.slug); diff --git a/frontend/components/layouts/DashboardLayout/types.ts b/frontend/components/layouts/DashboardLayout/types.ts deleted file mode 100644 index ee78febb0..000000000 --- a/frontend/components/layouts/DashboardLayout/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Redirect { - environmentChange?: boolean; - projectChange?: boolean; - url: string; -} diff --git a/frontend/components/lib/SubnetIcon/SubnetIcon.tsx b/frontend/components/lib/SubnetIcon/SubnetIcon.tsx index 27bf7c791..d31795865 100644 --- a/frontend/components/lib/SubnetIcon/SubnetIcon.tsx +++ b/frontend/components/lib/SubnetIcon/SubnetIcon.tsx @@ -1,9 +1,10 @@ +import type { Net } from '@pc/database/clients/core'; + import { assertUnreachable } from '@/utils/helpers'; -import type { NetOption } from '@/utils/types'; import { FeatherIcon } from '../FeatherIcon'; -export function SubnetIcon({ net }: { net?: NetOption }) { +export function SubnetIcon({ net }: { net?: Net }) { if (!net) return null; switch (net) { diff --git a/frontend/hooks/analytics.ts b/frontend/hooks/analytics.ts index 11a5724d8..a9df8ced5 100644 --- a/frontend/hooks/analytics.ts +++ b/frontend/hooks/analytics.ts @@ -35,7 +35,7 @@ export function useAnalytics() { if (user) { // https://segment.com/docs/connections/spec/best-practices-identify/ - analytics.identify(user.uid, { + analytics.identify(user.uid!, { email: user.email, displayName: user.name, userId: user.uid, diff --git a/frontend/hooks/api-keys.ts b/frontend/hooks/api-keys.ts index acd7e645e..ff5fc26e4 100644 --- a/frontend/hooks/api-keys.ts +++ b/frontend/hooks/api-keys.ts @@ -1,21 +1,24 @@ +import type { Api } from '@pc/common/types/api'; import useSWR from 'swr'; -import type { SWRConfiguration } from 'swr/dist/types'; +import type { Fetcher, SWRConfiguration } from 'swr/dist/types'; import { useIdentity } from '@/hooks/user'; import { authenticatedPost } from '@/utils/http'; -import type { NetOption } from '@/utils/types'; -type ApiKeys = Partial>; +type ApiKeys = Api.Query.Output<'/projects/getKeys'>; -export function useApiKeys(project: string | undefined, swrOptions?: SWRConfiguration) { +export function useApiKeys( + project: string | undefined, + swrOptions?: SWRConfiguration, Fetcher>, +) { const identity = useIdentity(); const { data: keys, error, mutate, - } = useSWR( + } = useSWR( identity && project ? ['/projects/getKeys', project, identity.uid] : null, - (key, project) => { + (key: '/projects/getKeys', project: string) => { return authenticatedPost(key, { project }); }, swrOptions, diff --git a/frontend/hooks/contracts.ts b/frontend/hooks/contracts.ts index 6736c4184..82c24f4db 100644 --- a/frontend/hooks/contracts.ts +++ b/frontend/hooks/contracts.ts @@ -1,11 +1,14 @@ +import type { Api } from '@pc/common/types/api'; +import type * as RPC from '@pc/common/types/rpc'; +import type { Net } from '@pc/database/clients/core'; import useSWR from 'swr'; import { useIdentity } from '@/hooks/user'; import analytics from '@/utils/analytics'; import config from '@/utils/config'; import { authenticatedPost } from '@/utils/http'; -import type { NetOption, ViewAccount } from '@/utils/types'; -import type { Contract } from '@/utils/types'; + +type Contract = Api.Query.Output<'/projects/getContracts'>[number]; export async function deleteContract(contract: Contract) { try { @@ -35,10 +38,13 @@ export function useContracts(project: string | undefined, environment: number | data: contracts, error, mutate, - } = useSWR( - identity && project && environment ? ['/projects/getContracts', project, environment, identity.uid] : null, + } = useSWR( + identity && project && environment ? ['/projects/getContracts' as const, project, environment, identity.uid] : null, (key, project, environment) => { - return authenticatedPost(key, { project, environment }); + return authenticatedPost(key, { + project, + environment, + }); }, ); @@ -52,17 +58,17 @@ export function useContract(slug: string | undefined) { data: contract, error, mutate, - } = useSWR(identity && slug ? ['/projects/getContract', slug, identity.uid] : null, (key, slug) => { + } = useSWR(identity && slug ? ['/projects/getContract' as const, slug, identity.uid] : null, (key, slug) => { return authenticatedPost(key, { slug }); }); return { contract, error, mutate }; } -export function useContractMetrics(address: string | undefined, net: NetOption | undefined) { - const { data, error, mutate } = useSWR( +export function useContractMetrics(address: string | undefined, net: Net | undefined) { + const { data, error, mutate } = useSWR( address && net ? [address, net] : null, - async (address: string, net: NetOption) => { + async (address: string, net: Net) => { const res = await fetch(config.url.rpc.default[net], { method: 'POST', headers: { @@ -78,7 +84,7 @@ export function useContractMetrics(address: string | undefined, net: NetOption | account_id: address, }, }), - }).then((res) => res.json()); + }).then((res) => res.json() as Promise<{ result: RPC.RpcQueryResponseNarrowed<'view_account'>; error?: any }>); if (res.error) { throw new Error(res.error.name); // TODO decide whether to retry error } diff --git a/frontend/hooks/environments.ts b/frontend/hooks/environments.ts index 0321be0fb..a56b705bb 100644 --- a/frontend/hooks/environments.ts +++ b/frontend/hooks/environments.ts @@ -2,24 +2,6 @@ import useSWR from 'swr'; import { useIdentity } from '@/hooks/user'; import { authenticatedPost } from '@/utils/http'; -import type { Environment } from '@/utils/types'; - -export function useEnvironment(environmentId: number | undefined) { - const identity = useIdentity(); - - const { - data: environment, - error, - mutate, - } = useSWR( - identity && environmentId ? ['/projects/getEnvironmentDetails', environmentId, identity.uid] : null, - (key, environmentId) => { - return authenticatedPost(key, { environmentId }); - }, - ); - - return { environment, error, mutate }; -} export function useEnvironments(project: string | undefined) { const identity = useIdentity(); @@ -28,12 +10,11 @@ export function useEnvironments(project: string | undefined) { data: environments, error, mutate, - } = useSWR( - identity && project && ['/projects/getEnvironments', project, identity.uid], - (key, project) => { - return authenticatedPost(key, { project }); - }, - ); + } = useSWR(identity && project && ['/projects/getEnvironments' as const, project, identity.uid], (key, project) => { + return authenticatedPost(key, { + project, + }); + }); return { environments, error, mutate }; } diff --git a/frontend/hooks/new-api-keys.ts b/frontend/hooks/new-api-keys.ts index 6dc358221..3e8821a5d 100644 --- a/frontend/hooks/new-api-keys.ts +++ b/frontend/hooks/new-api-keys.ts @@ -1,23 +1,21 @@ +import type { Api } from '@pc/common/types/api'; +import type { KeyedMutator } from 'swr'; import useSWR from 'swr'; -import type { SWRConfiguration } from 'swr/dist/types'; import { useIdentity } from '@/hooks/user'; import { authenticatedPost } from '@/utils/http'; -import type { ApiKey } from '@/utils/types'; -export function useApiKeys(project: string | undefined, swrOptions?: SWRConfiguration) { +type Keys = Api.Query.Output<'/projects/getKeys'>; + +export function useApiKeys(project: string | undefined) { const identity = useIdentity(); const { data: keys, error, mutate, - } = useSWR( - identity && project ? ['/projects/getKeys', project, identity.uid] : null, - (key, project) => { - return authenticatedPost(key, { project }); - }, - swrOptions, - ); + } = useSWR(identity && project ? ['/projects/getKeys' as const, project, identity.uid] : null, (key, project) => { + return authenticatedPost(key, { project }); + }); - return { keys, error, mutate }; + return { keys, error, mutate: mutate as KeyedMutator }; } diff --git a/frontend/hooks/organizations.ts b/frontend/hooks/organizations.ts index ec7e42bdd..4bef724c5 100644 --- a/frontend/hooks/organizations.ts +++ b/frontend/hooks/organizations.ts @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { noop } from 'lodash-es'; import { useMemo } from 'react'; import type { MutatorCallback, MutatorOptions } from 'swr'; @@ -8,7 +9,8 @@ import type { MutationOptions } from '@/hooks/mutation'; import { useMutation } from '@/hooks/mutation'; import { useAccount, useIdentity } from '@/hooks/user'; import { authenticatedPost } from '@/utils/http'; -import type { Organization, OrganizationMember, OrganizationRole, User } from '@/utils/types'; + +type User = Api.Query.Output<'/users/getAccountDetails'>; enum UserError { SERVER_ERROR = 'SERVER_ERROR', @@ -51,7 +53,7 @@ const isUserError = (code: string): code is UserError => { const DEFAULT_ERROR_MESSAGE = 'Unknown server error'; -type ParsedError = { +export type ParsedError = { code: UserError; message: string; }; @@ -62,8 +64,10 @@ const parseError = (e: any, messageParser: (code: UserError) => string | undefin }; const openSuccessToast = (message: string) => openToast({ type: 'success', title: 'Success', description: message }); -const openUserErrorToast = ({ code, message }: ParsedError) => - openToast({ type: 'error', title: getErrorTitle(code), description: message }); +const openUserErrorToast = (error: Api.Mutation.Error<'/users/acceptOrgInvite'>) => { + const castedError = error as ParsedError; + return openToast({ type: 'error', title: getErrorTitle(castedError.code), description: castedError.message }); +}; const getErrorTitle = (error: UserError) => { switch (error) { @@ -100,16 +104,20 @@ const getErrorTitle = (error: UserError) => { } }; -type OrgsMutationOptions = MutationOptions; +type OrgsMutationOptions = MutationOptions< + Api.Mutation.Input, + Api.Mutation.Output, + M, + Api.Mutation.Error +>; -const getOrgMembersKey = (orgSlug: string | undefined) => ['/users/listOrgMembers', orgSlug]; -const getOrgsKey = () => ['/users/listOrgs']; +const getOrgMembersKey = (orgSlug: string | undefined) => ['/users/listOrgMembers', orgSlug] as const; +const getOrgsKey = () => ['/users/listOrgs'] as const; export const useOrgMembers = (slug: string) => { const identity = useIdentity(); - const { data, error, mutate, isValidating } = useSWR( - identity ? getOrgMembersKey(slug) : null, - (path) => authenticatedPost(path, { org: slug }), + const { data, error, mutate, isValidating } = useSWR(identity ? getOrgMembersKey(slug) : null, (path) => + authenticatedPost(path, { org: slug }), ); return { members: data, error, mutate, isValidating }; @@ -117,7 +125,7 @@ export const useOrgMembers = (slug: string) => { export const useOrganizations = (filterPersonal: boolean) => { const identity = useIdentity(); - const { data, error, mutate, isValidating } = useSWR(identity ? getOrgsKey() : null, (path) => + const { data, error, mutate, isValidating } = useSWR(identity ? getOrgsKey() : null, (path) => authenticatedPost(path), ); const filteredOrgs = useMemo( @@ -130,20 +138,24 @@ export const useOrganizations = (filterPersonal: boolean) => { type MutationData = T | Promise | MutatorCallback; -const mutateOrganizations = (data?: MutationData, opts?: boolean | MutatorOptions) => - mutate(getOrgsKey(), data, opts); +type Orgs = Api.Query.Output<'/users/listOrgs'>; + +const mutateOrganizations = (data?: MutationData, opts?: boolean | MutatorOptions) => + mutate(getOrgsKey(), data, opts); + +type OrgMembers = Api.Query.Output<'/users/listOrgMembers'>; const mutateOrganizationMembers = ( orgSlug: string, - data?: MutationData, - opts?: boolean | MutatorOptions, -) => mutate(getOrgMembersKey(orgSlug), data, opts); + data?: MutationData, + opts?: boolean | MutatorOptions, +) => mutate(getOrgMembersKey(orgSlug), data, opts); export const useOrgsWithOnlyAdmin = () => { const identity = useIdentity(); - const { data, error, mutate, isValidating } = useSWR[]>( - identity ? ['/users/listOrgsWithOnlyAdmin', identity.uid] : null, - (key) => authenticatedPost(key), + const { data, error, mutate, isValidating } = useSWR( + identity ? ['/users/listOrgsWithOnlyAdmin' as const, identity.uid] : null, + (key) => authenticatedPost<'/users/listOrgsWithOnlyAdmin'>(key), ); return { organizations: data, error, mutate, isValidating }; @@ -158,16 +170,17 @@ const getCreateOrgMessage = (code: UserError) => { } }; -const createOrgMutationOptions = (user?: User): OrgsMutationOptions<{ name: string }, Organization> => ({ +const createOrgMutationOptions = (user?: User): OrgsMutationOptions<'/users/createOrg'> => ({ eventName: 'Create organization', mutate: ({ name }) => - authenticatedPost('/users/createOrg', { name }).catch((e) => parseError(e, getCreateOrgMessage)), - onSuccess: (createdOrg: Organization) => { - if (user) { + authenticatedPost('/users/createOrg', { name }).catch((e) => parseError(e, getCreateOrgMessage)), + onSuccess: (createdOrg: Api.Mutation.Output<'/users/createOrg'>) => { + if (user && user.uid && user.email) { mutateOrganizationMembers(createdOrg.slug, [ { role: 'ADMIN', orgSlug: createdOrg.slug, + isInvite: false, user: { uid: user.uid, email: user.email, @@ -188,9 +201,9 @@ export const useCreateOrg = () => { return useMutation(useMemo(() => createOrgMutationOptions(account.user), [account.user])); }; -const updateOrgMembersCache = (orgSlug: string, uid: string, role: OrganizationRole) => { - let prevRole: OrganizationRole | undefined; - mutate( +const updateOrgMembersCache = (orgSlug: string, uid: string, role: OrgMembers[number]['role']) => { + let prevRole: OrgMembers[number]['role'] | undefined; + mutate( getOrgMembersKey(orgSlug), (members) => { if (!members) { @@ -227,17 +240,17 @@ const getUserRoleChangeMessage = (code: UserError) => { const createChangeUserRoleMutationOptions = ( orgSlug: string, -): OrgsMutationOptions<{ uid: string; role: OrganizationRole }, void, OrganizationRole | undefined> => ({ +): OrgsMutationOptions<'/users/changeOrgRole', OrgMembers[number]['role'] | undefined> => ({ eventName: 'Change user role in organization', - mutate: ({ uid, role }) => - authenticatedPost('/users/changeOrgRole', { org: orgSlug, user: uid, role }).catch((e) => + mutate: ({ user, role }) => + authenticatedPost('/users/changeOrgRole', { org: orgSlug, user, role }).catch((e) => parseError(e, getUserRoleChangeMessage), ), onSuccess: (_result, { role }) => openSuccessToast(`Role changed to ${role}`), - onMutate: ({ uid, role }) => updateOrgMembersCache(orgSlug, uid, role), - onError: (error, { uid }, prevRole) => { + onMutate: ({ user, role }) => updateOrgMembersCache(orgSlug, user, role), + onError: (error, { user }, prevRole) => { if (prevRole) { - updateOrgMembersCache(orgSlug, uid, prevRole); + updateOrgMembersCache(orgSlug, user, prevRole); } openUserErrorToast(error); }, @@ -257,11 +270,14 @@ const getRemoveFromOrgMessage = (code: UserError) => { } }; -const createLeaveOrgMutationOptions = (orgSlug: string, selfUid?: string): OrgsMutationOptions => ({ +const createLeaveOrgMutationOptions = ( + orgSlug: string, + selfUid?: string, +): OrgsMutationOptions<'/users/removeFromOrg'> => ({ eventName: 'Leave from organization', mutate: selfUid ? () => - authenticatedPost('/users/removeFromOrg', { org: orgSlug, user: selfUid }).catch((e) => + authenticatedPost('/users/removeFromOrg', { org: orgSlug, user: selfUid }).catch((e) => parseError(e, getRemoveFromOrgMessage), ) : noop, @@ -299,12 +315,10 @@ const getInviteMemberMessage = (code: UserError) => { } }; -const createInviteUserMutationOptions = ( - orgSlug: string, -): OrgsMutationOptions<{ email: string; role: OrganizationRole }> => ({ +const createInviteUserMutationOptions = (orgSlug: string): OrgsMutationOptions<'/users/inviteToOrg'> => ({ eventName: 'Invite user to organization', mutate: ({ email, role }) => - authenticatedPost('/users/inviteToOrg', { org: orgSlug, email, role }).catch((e) => + authenticatedPost('/users/inviteToOrg', { org: orgSlug, email, role }).catch((e) => parseError(e, getInviteMemberMessage), ), onSuccess: (_result, { email, role }) => { @@ -344,10 +358,10 @@ const getRemoveInviteMessage = (code: UserError) => { } }; -const createRekoveInviteMutationOptions = (orgSlug: string): OrgsMutationOptions<{ email: string }> => ({ +const createRekoveInviteMutationOptions = (orgSlug: string): OrgsMutationOptions<'/users/removeOrgInvite'> => ({ eventName: 'Revoke organization invite', mutate: ({ email }) => - authenticatedPost('/users/removeOrgInvite', { org: orgSlug, email }).catch((e) => + authenticatedPost('/users/removeOrgInvite', { org: orgSlug, email }).catch((e) => parseError(e, getRemoveInviteMessage), ), onSuccess: (_result, { email }) => { @@ -364,14 +378,14 @@ const createRekoveInviteMutationOptions = (orgSlug: string): OrgsMutationOptions export const useRemoveOrgInvite = (orgSlug: string) => useMutation(useMemo(() => createRekoveInviteMutationOptions(orgSlug), [orgSlug])); -const createRemoveUserMutationOptions = (orgSlug: string): OrgsMutationOptions<{ uid: string }> => ({ +const createRemoveUserMutationOptions = (orgSlug: string): OrgsMutationOptions<'/users/removeFromOrg'> => ({ eventName: 'Remove user from organization', - mutate: ({ uid }) => - authenticatedPost('/users/removeFromOrg', { org: orgSlug, user: uid }).catch((e) => + mutate: ({ user }) => + authenticatedPost('/users/removeFromOrg', { org: orgSlug, user }).catch((e) => parseError(e, getRemoveInviteMessage), ), - onSuccess: (_result, { uid }) => { - mutateOrganizationMembers(orgSlug, (members) => members && members.filter((member) => member.user.uid !== uid), { + onSuccess: (_result, { user }) => { + mutateOrganizationMembers(orgSlug, (members) => members && members.filter((member) => member.user.uid !== user), { revalidate: false, }); openSuccessToast('User is removed from organization'); @@ -391,16 +405,16 @@ const getDeleteOrgMessage = (code: UserError) => { } }; -const createDeleteOrgMutationOptions = (orgSlug: string): OrgsMutationOptions<{ name: string }> => ({ +const createDeleteOrgMutationOptions = (orgSlug: string): OrgsMutationOptions<'/users/deleteOrg'> => ({ eventName: 'Delete organization', mutate: () => - authenticatedPost('/users/deleteOrg', { org: orgSlug }).catch((e) => parseError(e, getDeleteOrgMessage)), - onSuccess: (_result, { name }) => { + authenticatedPost('/users/deleteOrg', { org: orgSlug }).catch((e) => parseError(e, getDeleteOrgMessage)), + onSuccess: (_result, { org }) => { mutateOrganizationMembers(orgSlug); mutateOrganizations((orgs) => orgs && orgs.filter((org) => org.slug !== orgSlug), { revalidate: false, }); - openSuccessToast(`Organization "${name}" deleted`); + openSuccessToast(`Organization "${org}" deleted`); }, onError: openUserErrorToast, }); @@ -423,13 +437,12 @@ const getInviteErrorMessage = (code: UserError) => { } }; -const acceptOrgInviteOptions: MutationOptions<{ token: string }, void, unknown, { code: UserError; message: string }> = - { - eventName: 'Accept organization invite', - mutate: ({ token }) => - authenticatedPost('/users/acceptOrgInvite', { token }).catch((e) => parseError(e, getInviteErrorMessage)), - onSuccess: () => mutateOrganizations(), - }; +const acceptOrgInviteOptions: OrgsMutationOptions<'/users/acceptOrgInvite'> = { + eventName: 'Accept organization invite', + mutate: ({ token }) => + authenticatedPost('/users/acceptOrgInvite', { token }).catch((e) => parseError(e, getInviteErrorMessage)), + onSuccess: () => mutateOrganizations(), +}; export const useAcceptOrgInvite = () => useMutation(acceptOrgInviteOptions); diff --git a/frontend/hooks/projects.ts b/frontend/hooks/projects.ts index 6fdfcab34..bec97c3e8 100644 --- a/frontend/hooks/projects.ts +++ b/frontend/hooks/projects.ts @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useRouter } from 'next/router'; import { useEffect } from 'react'; import useSWR from 'swr'; @@ -6,7 +7,6 @@ import { mutate } from 'swr'; import { useIdentity } from '@/hooks/user'; import analytics from '@/utils/analytics'; import { authenticatedPost } from '@/utils/http'; -import type { Project } from '@/utils/types'; import { useProjectSelector } from './selected-project'; @@ -30,6 +30,8 @@ export async function ejectTutorial(slug: string, name: string) { return false; } +type Projects = Api.Query.Output<'/projects/list'>; + export async function deleteProject(userId: string | undefined, slug: string, name: string) { try { await authenticatedPost('/projects/delete', { slug }); @@ -38,7 +40,7 @@ export async function deleteProject(userId: string | undefined, slug: string, na name, }); // Update the SWR cache before a refetch for better UX. - mutate(userId ? ['/projects/list', userId] : null, async (projects) => { + mutate(userId ? ['/projects/list', userId] : null, async (projects) => { return projects?.filter((p) => p.slug !== slug); }); return true; @@ -59,8 +61,8 @@ export function useProject(projectSlug: string | undefined) { const identity = useIdentity(); const { selectProject } = useProjectSelector(); - const { data: project, error } = useSWR( - identity && projectSlug ? ['/projects/getDetails', projectSlug, identity.uid] : null, + const { data: project, error } = useSWR( + identity && projectSlug ? ['/projects/getDetails' as const, projectSlug, identity.uid] : null, (key, projectSlug) => { return authenticatedPost(key, { slug: projectSlug }); }, @@ -90,7 +92,7 @@ export function useProjects() { error, mutate, isValidating, - } = useSWR(identity ? ['/projects/list', identity.uid] : null, (key) => { + } = useSWR(identity ? ['/projects/list' as const, identity.uid] : null, (key) => { return authenticatedPost(key); }); @@ -102,8 +104,8 @@ export function useProjectGroups() { return { projectGroups: projects ? Object.entries( - projects.reduce>((acc, project) => { - const orgName = project.org.isPersonal ? 'Personal' : project.org.name || 'unknown'; + projects.reduce>((acc, project) => { + const orgName = project.org.isPersonal ? 'Personal' : project.org.name; if (!acc[orgName]) { acc[orgName] = []; } diff --git a/frontend/hooks/selected-project.ts b/frontend/hooks/selected-project.ts index 8d78d7d90..816723627 100644 --- a/frontend/hooks/selected-project.ts +++ b/frontend/hooks/selected-project.ts @@ -1,9 +1,9 @@ +import type { Api } from '@pc/common/types/api'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useState } from 'react'; import { useSettingsStore } from '@/stores/settings'; import config from '@/utils/config'; -import type { Environment, Project } from '@/utils/types'; import { useEnvironments } from './environments'; import { usePreviousValue } from './previous-value'; @@ -11,6 +11,8 @@ import { useProject } from './projects'; import { useRouteParam } from './route'; import { useIdentity } from './user'; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; + interface Options { enforceSelectedProject?: boolean; } @@ -115,7 +117,7 @@ export function useOnSelectedProjectChange(onChange: () => void) { export function useSelectedProjectSync( selectedEnvironmentSubId: Environment['subId'] | undefined, - selectedProjectSlug: Project['slug'] | undefined, + selectedProjectSlug: string | undefined, ) { const settings = useSettingsStore((store) => store.currentUser); const { environment, project } = useSelectedProject(); diff --git a/frontend/hooks/user.ts b/frontend/hooks/user.ts index 1db007e21..1fbc61a99 100644 --- a/frontend/hooks/user.ts +++ b/frontend/hooks/user.ts @@ -5,7 +5,6 @@ import useSWR from 'swr'; import analytics from '@/utils/analytics'; import { authenticatedPost, unauthenticatedPost } from '@/utils/http'; -import type { User } from '@/utils/types'; export function useAccount() { const identity = useIdentity(); @@ -13,7 +12,7 @@ export function useAccount() { data: user, error, mutate, - } = useSWR(identity ? ['/users/getAccountDetails', identity.uid] : null, (key) => { + } = useSWR(identity ? ['/users/getAccountDetails' as const, identity.uid] : null, (key) => { return authenticatedPost(key); }); diff --git a/frontend/modules/alerts/components/AlertTableRow.tsx b/frontend/modules/alerts/components/AlertTableRow.tsx index 55ab8f5ab..055afff9d 100644 --- a/frontend/modules/alerts/components/AlertTableRow.tsx +++ b/frontend/modules/alerts/components/AlertTableRow.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Badge } from '@/components/lib/Badge'; @@ -10,11 +11,12 @@ import { Tooltip } from '@/components/lib/Tooltip'; import { StableId } from '@/utils/stable-ids'; import { alertTypes } from '../utils/constants'; -import type { Alert } from '../utils/types'; import { DeleteAlertModal } from './DeleteAlertModal'; +type Alert = Api.Query.Output<'/alerts/listAlerts'>[number]; + export function AlertTableRow({ alert, onDelete }: { alert: Alert; onDelete: () => void }) { - const alertType = alertTypes[alert.type]; + const alertType = alertTypes[alert.rule.type]; const url = `/alerts/edit-alert/${alert.id}`; const [showDeleteModal, setShowDeleteModal] = useState(false); diff --git a/frontend/modules/alerts/components/Alerts.tsx b/frontend/modules/alerts/components/Alerts.tsx index da199ae5e..8f79e28bc 100644 --- a/frontend/modules/alerts/components/Alerts.tsx +++ b/frontend/modules/alerts/components/Alerts.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import Link from 'next/link'; import { ButtonLink } from '@/components/lib/Button'; @@ -9,11 +10,13 @@ import * as Table from '@/components/lib/Table'; import { Text } from '@/components/lib/Text'; import { openToast } from '@/components/lib/Toast'; import { StableId } from '@/utils/stable-ids'; -import type { Environment, Project } from '@/utils/types'; import { useAlerts } from '../hooks/alerts'; import { AlertTableRow } from './AlertTableRow'; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; +type Project = Api.Query.Output<'/projects/getDetails'>; + export function Alerts({ environment, project }: { environment?: Environment; project?: Project }) { const { alerts, mutate } = useAlerts(project?.slug, environment?.subId); diff --git a/frontend/modules/alerts/components/DeleteAlertModal.tsx b/frontend/modules/alerts/components/DeleteAlertModal.tsx index ca325707d..c92fe147a 100644 --- a/frontend/modules/alerts/components/DeleteAlertModal.tsx +++ b/frontend/modules/alerts/components/DeleteAlertModal.tsx @@ -1,10 +1,11 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Text } from '@/components/lib/Text'; import { ConfirmModal } from '@/components/modals/ConfirmModal'; import { deleteAlert } from '@/modules/alerts/hooks/alerts'; -import type { Alert } from '../utils/types'; +type Alert = Api.Query.Output<'/alerts/listAlerts'>[number]; interface Props { alert: Alert; diff --git a/frontend/modules/alerts/components/DeleteDestinationModal.tsx b/frontend/modules/alerts/components/DeleteDestinationModal.tsx index b05f422c5..8eca40b30 100644 --- a/frontend/modules/alerts/components/DeleteDestinationModal.tsx +++ b/frontend/modules/alerts/components/DeleteDestinationModal.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useEffect, useState } from 'react'; import { Card } from '@/components/lib/Card'; @@ -11,7 +12,9 @@ import { useSelectedProject } from '@/hooks/selected-project'; import { useAlerts } from '../hooks/alerts'; import { deleteDestination } from '../hooks/destinations'; -import type { Alert, Destination } from '../utils/types'; + +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; +type Alerts = Api.Query.Output<'/alerts/listAlerts'>; interface Props { destination: Destination; @@ -25,7 +28,7 @@ export function DeleteDestinationModal({ destination, show, setShow, onDelete }: const [isDeleting, setIsDeleting] = useState(false); const { environment, project } = useSelectedProject(); const { alerts } = useAlerts(project?.slug, environment?.subId); - const [enabledAlerts, setEnabledAlerts] = useState([]); + const [enabledAlerts, setEnabledAlerts] = useState([]); useEffect(() => { const result = alerts?.filter((alert) => { diff --git a/frontend/modules/alerts/components/Destinations.tsx b/frontend/modules/alerts/components/Destinations.tsx index 69dfe93ce..8690d64dd 100644 --- a/frontend/modules/alerts/components/Destinations.tsx +++ b/frontend/modules/alerts/components/Destinations.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Button } from '@/components/lib/Button'; @@ -11,12 +12,13 @@ import { openToast } from '@/components/lib/Toast'; import { EditDestinationModal } from '@/modules/alerts/components/EditDestinationModal'; import { NewDestinationModal } from '@/modules/alerts/components/NewDestinationModal'; import { useDestinations } from '@/modules/alerts/hooks/destinations'; -import type { Destination } from '@/modules/alerts/utils/types'; import { StableId } from '@/utils/stable-ids'; -import type { Project } from '@/utils/types'; import { DestinationTableRow } from './DestinationsTableRow'; +type Project = Api.Query.Output<'/projects/getDetails'>; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; + export function Destinations({ project }: { project?: Project }) { const { destinations, mutate } = useDestinations(project?.slug); const [showNewDestinationModal, setShowNewDestinationModal] = useState(false); @@ -73,7 +75,7 @@ export function Destinations({ project }: { project?: Project }) { openToast({ type: 'success', title: 'Destination Deleted', - description: name, + description: name ?? undefined, }); mutate(() => { diff --git a/frontend/modules/alerts/components/DestinationsSelector.tsx b/frontend/modules/alerts/components/DestinationsSelector.tsx index c64827861..6b99cb911 100644 --- a/frontend/modules/alerts/components/DestinationsSelector.tsx +++ b/frontend/modules/alerts/components/DestinationsSelector.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import type { Dispatch, SetStateAction } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -14,10 +15,11 @@ import { useDestinations } from '@/modules/alerts/hooks/destinations'; import { destinationTypes } from '@/modules/alerts/utils/constants'; import { StableId } from '@/utils/stable-ids'; -import type { Destination } from '../utils/types'; import { EditDestinationModal } from './EditDestinationModal'; import { NewDestinationModal } from './NewDestinationModal'; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; + export interface OnDestinationSelectionChangeEvent { destination: Destination; isSelected: boolean; diff --git a/frontend/modules/alerts/components/DestinationsTableRow.tsx b/frontend/modules/alerts/components/DestinationsTableRow.tsx index 821e080aa..a8bea63f8 100644 --- a/frontend/modules/alerts/components/DestinationsTableRow.tsx +++ b/frontend/modules/alerts/components/DestinationsTableRow.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Badge } from '@/components/lib/Badge'; @@ -11,9 +12,10 @@ import { Tooltip } from '@/components/lib/Tooltip'; import { StableId } from '@/utils/stable-ids'; import { destinationTypes } from '../utils/constants'; -import type { Destination } from '../utils/types'; import { DeleteDestinationModal } from './DeleteDestinationModal'; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; + export function DestinationTableRow({ destination, onClick, diff --git a/frontend/modules/alerts/components/EditDestinationModal.tsx b/frontend/modules/alerts/components/EditDestinationModal.tsx index 6d419a892..6169be22a 100644 --- a/frontend/modules/alerts/components/EditDestinationModal.tsx +++ b/frontend/modules/alerts/components/EditDestinationModal.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -17,12 +18,13 @@ import { StableId } from '@/utils/stable-ids'; import { updateDestination, useDestinations } from '../hooks/destinations'; import { useVerifyDestinationInterval } from '../hooks/verify-destination-interval'; import { destinationTypes } from '../utils/constants'; -import type { Destination } from '../utils/types'; import { DeleteDestinationModal } from './DeleteDestinationModal'; import { EmailDestinationVerification } from './EmailDestinationVerification'; import { TelegramDestinationVerification } from './TelegramDestinationVerification'; import { WebhookDestinationSecret } from './WebhookDestinationSecret'; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; + interface Props { destination: Destination; show: boolean; @@ -162,14 +164,16 @@ function TelegramDestinationForm({ destination, onUpdate, setShow }: FormProps) const { formState, setValue, register, handleSubmit } = useForm(); useEffect(() => { - setValue('name', destination.name); + if (destination.name) { + setValue('name', destination.name); + } }, [setValue, destination]); async function submitForm(data: TelegramFormData) { try { const updated = await updateDestination({ id: destination.id, - type: destination.type, + type: 'TELEGRAM', name: data.name, }); @@ -244,7 +248,9 @@ function WebhookDestinationForm({ destination, onUpdate, setShow, onSecretRotate const { formState, setValue, register, handleSubmit } = useForm(); useEffect(() => { - setValue('name', destination.name); + if (destination.name) { + setValue('name', destination.name); + } setValue('url', destination.config.url); }, [setValue, destination]); @@ -336,14 +342,16 @@ function EmailDestinationForm({ destination, onUpdate, setShow }: FormProps) { const { formState, setValue, register, handleSubmit } = useForm(); useEffect(() => { - setValue('name', destination.name); + if (destination.name) { + setValue('name', destination.name); + } }, [setValue, destination]); async function submitForm(data: EmailFormData) { try { const updated = await updateDestination({ id: destination.id, - type: destination.type, + type: 'EMAIL', name: data.name, }); diff --git a/frontend/modules/alerts/components/EmailDestinationVerification.tsx b/frontend/modules/alerts/components/EmailDestinationVerification.tsx index 909faa20b..22f106380 100644 --- a/frontend/modules/alerts/components/EmailDestinationVerification.tsx +++ b/frontend/modules/alerts/components/EmailDestinationVerification.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Button } from '@/components/lib/Button'; @@ -6,7 +7,8 @@ import { Text } from '@/components/lib/Text'; import { StableId } from '@/utils/stable-ids'; import { resendEmailVerification } from '../hooks/destinations'; -import type { Destination } from '../utils/types'; + +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; interface Props { destination: Destination; diff --git a/frontend/modules/alerts/components/NewDestinationModal.tsx b/frontend/modules/alerts/components/NewDestinationModal.tsx index 1b1d49310..b1f6f96b2 100644 --- a/frontend/modules/alerts/components/NewDestinationModal.tsx +++ b/frontend/modules/alerts/components/NewDestinationModal.tsx @@ -1,3 +1,5 @@ +import type { Api } from '@pc/common/types/api'; +import type { DestinationType } from '@pc/database/clients/alerts'; import { useState } from 'react'; import type { FieldValues } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -19,11 +21,12 @@ import { StableId } from '@/utils/stable-ids'; import { createDestination, useDestinations } from '../hooks/destinations'; import { useVerifyDestinationInterval } from '../hooks/verify-destination-interval'; import { destinationTypeOptions } from '../utils/constants'; -import type { Destination, DestinationType, NewDestination } from '../utils/types'; import { EmailDestinationVerification } from './EmailDestinationVerification'; import { TelegramDestinationVerification } from './TelegramDestinationVerification'; import { WebhookDestinationSecret } from './WebhookDestinationSecret'; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; + interface Props { onCreate?: (destination: Destination) => void; onVerify?: (destination: Destination) => void; @@ -43,7 +46,7 @@ function useNewDestinationForm(props: FormProps) { useVerifyDestinationInterval(destination, mutate, props.setShow, props.onVerify); - async function create(data: NewDestination) { + async function create(data: Api.Mutation.Input<'/alerts/createDestination'>) { try { const destination = await createDestination(data); diff --git a/frontend/modules/alerts/components/TelegramDestinationVerification.tsx b/frontend/modules/alerts/components/TelegramDestinationVerification.tsx index e4ef64b8d..dfb35f957 100644 --- a/frontend/modules/alerts/components/TelegramDestinationVerification.tsx +++ b/frontend/modules/alerts/components/TelegramDestinationVerification.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { QRCodeSVG } from 'qrcode.react'; import { Box } from '@/components/lib/Box'; @@ -9,7 +10,7 @@ import { Text } from '@/components/lib/Text'; import config from '@/utils/config'; import { StableId } from '@/utils/stable-ids'; -import type { Destination } from '../utils/types'; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; interface Props { destination: Destination; diff --git a/frontend/modules/alerts/components/TriggeredAlerts.tsx b/frontend/modules/alerts/components/TriggeredAlerts.tsx index 707d85544..8570e6360 100644 --- a/frontend/modules/alerts/components/TriggeredAlerts.tsx +++ b/frontend/modules/alerts/components/TriggeredAlerts.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { DateTime } from 'luxon'; import Link from 'next/link'; import { useEffect, useState } from 'react'; @@ -20,12 +21,14 @@ import { useRouteParam } from '@/hooks/route'; import { useOnSelectedProjectChange } from '@/hooks/selected-project'; import { StableId } from '@/utils/stable-ids'; import { truncateMiddle } from '@/utils/truncate-middle'; -import type { Environment, Project } from '@/utils/types'; import { useAlerts } from '../hooks/alerts'; import { useTriggeredAlerts } from '../hooks/triggered-alerts'; import { alertTypes } from '../utils/constants'; -import type { TriggeredAlert } from '../utils/types'; + +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; +type Project = Api.Query.Output<'/projects/getDetails'>; +type TriggeredAlert = Api.Query.Output<'/triggeredAlerts/listTriggeredAlerts'>['page'][number]; export function TriggeredAlerts({ environment, project }: { environment?: Environment; project?: Project }) { const queryParamAlertFilter = useRouteParam('alertId'); @@ -121,7 +124,7 @@ export function TriggeredAlerts({ environment, project }: { environment?: Enviro onValueChange={onSelectAlertFilter} > {alerts?.map((a) => { - const alertTypeOption = alertTypes[a.type]; + const alertTypeOption = alertTypes[a.rule.type]; return ( - {truncateMiddle(row.triggeredInTransactionHash)} + {row.triggeredInTransactionHash ? truncateMiddle(row.triggeredInTransactionHash) : null} diff --git a/frontend/modules/alerts/components/WebhookDestinationSecret.tsx b/frontend/modules/alerts/components/WebhookDestinationSecret.tsx index 9e8f720ee..40b18e43b 100644 --- a/frontend/modules/alerts/components/WebhookDestinationSecret.tsx +++ b/frontend/modules/alerts/components/WebhookDestinationSecret.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Button } from '@/components/lib/Button'; @@ -10,7 +11,8 @@ import { ConfirmModal } from '@/components/modals/ConfirmModal'; import { StableId } from '@/utils/stable-ids'; import { rotateWebhookDestinationSecret } from '../hooks/destinations'; -import type { Destination } from '../utils/types'; + +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; interface Props { destination: Destination; @@ -36,13 +38,7 @@ export function WebhookDestinationSecret({ destination, onRotate }: Props) { setIsSending(true); const res = await rotateWebhookDestinationSecret(destination.id); if (res.type !== 'WEBHOOK') return; - - const updated = { - ...destination, - }; - updated.config.secret = res.config.secret; - - onRotate(updated); + onRotate(res); } catch (e) { console.error('Failed to rotate webhook secret.', e); openToast({ diff --git a/frontend/modules/alerts/hooks/alerts.ts b/frontend/modules/alerts/hooks/alerts.ts index bea16581f..0be820fd6 100644 --- a/frontend/modules/alerts/hooks/alerts.ts +++ b/frontend/modules/alerts/hooks/alerts.ts @@ -1,13 +1,12 @@ +import type { Api } from '@pc/common/types/api'; import useSWR from 'swr'; import { useIdentity } from '@/hooks/user'; import analytics from '@/utils/analytics'; import { authenticatedPost } from '@/utils/http'; -import type { Alert, NewAlert, UpdateAlert } from '../utils/types'; - -export async function createAlert(data: NewAlert) { - const alert: Alert = await authenticatedPost('/alerts/createAlert', { +export async function createAlert(data: Api.Mutation.Input<'/alerts/createAlert'>) { + const alert = await authenticatedPost('/alerts/createAlert', { ...data, }); @@ -20,7 +19,7 @@ export async function createAlert(data: NewAlert) { return alert; } -export async function deleteAlert(alert: Alert) { +export async function deleteAlert(alert: Api.Mutation.Input<'/alerts/deleteAlert'>) { try { await authenticatedPost('/alerts/deleteAlert', { id: alert.id }); analytics.track('DC Remove Alert', { @@ -66,8 +65,8 @@ export async function enableDestinationForAlert(alertId: number, destinationId: }); } -export async function updateAlert(data: UpdateAlert) { - const alert: Alert = await authenticatedPost('/alerts/updateAlert', { +export async function updateAlert(data: Api.Mutation.Input<'/alerts/updateAlert'>) { + const alert = await authenticatedPost('/alerts/updateAlert', { ...data, }); @@ -87,8 +86,8 @@ export function useAlert(alertId: number | undefined) { data: alert, error, mutate, - } = useSWR( - identity && alertId ? ['/alerts/getAlertDetails', alertId, identity.uid] : null, + } = useSWR( + identity && alertId ? ['/alerts/getAlertDetails' as const, alertId, identity.uid] : null, async (key, alertId) => { return authenticatedPost(key, { id: alertId }); }, @@ -104,12 +103,12 @@ export function useAlerts(projectSlug: string | undefined, environmentSubId: num error, mutate, isValidating, - } = useSWR( + } = useSWR( identity && projectSlug && environmentSubId - ? ['/alerts/listAlerts', projectSlug, environmentSubId, identity.uid] + ? ['/alerts/listAlerts' as const, projectSlug, environmentSubId, identity.uid] : null, (key) => { - return authenticatedPost(key, { environmentSubId, projectSlug }); + return authenticatedPost(key, { environmentSubId: environmentSubId!, projectSlug: projectSlug! }); }, ); diff --git a/frontend/modules/alerts/hooks/destinations.ts b/frontend/modules/alerts/hooks/destinations.ts index bd705bb93..1a151ca64 100644 --- a/frontend/modules/alerts/hooks/destinations.ts +++ b/frontend/modules/alerts/hooks/destinations.ts @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import useSWR from 'swr'; import { openToast } from '@/components/lib/Toast'; @@ -5,10 +6,8 @@ import { useIdentity } from '@/hooks/user'; import analytics from '@/utils/analytics'; import { authenticatedPost } from '@/utils/http'; -import type { Destination, NewDestination, UpdateDestination } from '../utils/types'; - -export async function createDestination(data: NewDestination) { - const destination: Destination = await authenticatedPost('/alerts/createDestination', { +export async function createDestination(data: Api.Mutation.Input<'/alerts/createDestination'>) { + const destination = await authenticatedPost('/alerts/createDestination', { ...data, }); @@ -21,7 +20,7 @@ export async function createDestination(data: NewDestination) { return destination; } -export async function deleteDestination(destination: Destination) { +export async function deleteDestination(destination: Api.Mutation.Input<'/alerts/deleteDestination'>) { try { await authenticatedPost('/alerts/deleteDestination', { id: destination.id }); analytics.track('DC Remove Destination', { @@ -41,8 +40,8 @@ export async function deleteDestination(destination: Destination) { return false; } -export async function updateDestination(data: UpdateDestination) { - const destination: Destination = await authenticatedPost('/alerts/updateDestination', { +export async function updateDestination(data: Api.Mutation.Input<'/alerts/updateDestination'>) { + const destination = await authenticatedPost('/alerts/updateDestination', { ...data, }); @@ -63,10 +62,10 @@ export function useDestinations(projectSlug: string | undefined) { error, mutate, isValidating, - } = useSWR( - identity && projectSlug ? ['/alerts/listDestinations', projectSlug, identity.uid] : null, + } = useSWR( + identity && projectSlug ? ['/alerts/listDestinations' as const, projectSlug, identity.uid] : null, (key) => { - return authenticatedPost(key, { projectSlug }); + return authenticatedPost(key, { projectSlug: projectSlug! }); }, ); @@ -106,7 +105,7 @@ export async function resendEmailVerification(destinationId: number) { } export async function rotateWebhookDestinationSecret(destinationId: number) { - const destination: Destination = await authenticatedPost('/alerts/rotateWebhookDestinationSecret', { + const destination = await authenticatedPost('/alerts/rotateWebhookDestinationSecret', { destinationId, }); diff --git a/frontend/modules/alerts/hooks/triggered-alerts.ts b/frontend/modules/alerts/hooks/triggered-alerts.ts index a1765edca..3aa243311 100644 --- a/frontend/modules/alerts/hooks/triggered-alerts.ts +++ b/frontend/modules/alerts/hooks/triggered-alerts.ts @@ -6,8 +6,6 @@ import { useIdentity } from '@/hooks/user'; import config from '@/utils/config'; import { authenticatedPost } from '@/utils/http'; -import type { TriggeredAlert, TriggeredAlertsPagingResponse } from '../utils/types'; - interface TriggeredAlertFilters { alertId?: number; } @@ -26,10 +24,10 @@ export function useTriggeredAlerts( const take = pagination.state.pageSize; const skip = (pagination.state.currentPage - 1) * pagination.state.pageSize; - const { data, error } = useSWR( + const { data, error } = useSWR( identity && projectSlug && environmentSubId ? [ - '/triggeredAlerts/listTriggeredAlerts', + '/triggeredAlerts/listTriggeredAlerts' as const, projectSlug, environmentSubId, identity.uid, @@ -41,8 +39,8 @@ export function useTriggeredAlerts( : null, (key) => { return authenticatedPost(key, { - environmentSubId, - projectSlug, + environmentSubId: environmentSubId!, + projectSlug: projectSlug!, take, skip, pagingDateTime: pagination.state.pagingDateTime, @@ -67,8 +65,8 @@ export function useTriggeredAlerts( export function useTriggeredAlertDetails(slug: string) { const identity = useIdentity(); - const { data, error } = useSWR( - identity ? ['/triggeredAlerts/getTriggeredAlertDetails', slug] : null, + const { data, error } = useSWR( + identity ? ['/triggeredAlerts/getTriggeredAlertDetails' as const, slug] : null, (key) => { return authenticatedPost(key, { slug, diff --git a/frontend/modules/alerts/hooks/verify-destination-interval.ts b/frontend/modules/alerts/hooks/verify-destination-interval.ts index a137b0b3e..688089150 100644 --- a/frontend/modules/alerts/hooks/verify-destination-interval.ts +++ b/frontend/modules/alerts/hooks/verify-destination-interval.ts @@ -1,9 +1,10 @@ +import type { Api } from '@pc/common/types/api'; import { useCallback, useEffect } from 'react'; import type { KeyedMutator } from 'swr'; import { openToast } from '@/components/lib/Toast'; -import type { Destination } from '../utils/types'; +type Destination = Api.Query.Output<'/alerts/listDestinations'>[number]; export function useVerifyDestinationInterval( destination: Destination | undefined, diff --git a/frontend/modules/alerts/utils/constants.ts b/frontend/modules/alerts/utils/constants.ts index a86301712..000a9a47d 100644 --- a/frontend/modules/alerts/utils/constants.ts +++ b/frontend/modules/alerts/utils/constants.ts @@ -1,13 +1,14 @@ -import type { AlertType, AmountComparator, DestinationType } from './types'; +import type { Alerts } from '@pc/common/types/alerts'; +import type { DestinationType } from '@pc/database/clients/alerts'; interface AlertTypeOption { description: string; icon: string; name: string; - value: AlertType; + value: Alerts.RuleType; } -export const alertTypes: Record = { +export const alertTypes: Record = { TX_SUCCESS: { description: 'Triggers whenever a successful action occurs.', icon: 'check-circle', @@ -58,10 +59,10 @@ export const alertTypeOptions = [ interface AmountComparatorOption { icon: string; name: string; - value: AmountComparator; + value: Alerts.Comparator; } -export const amountComparators: Record = { +export const amountComparators: Record = { EQ: { icon: '==', name: 'Equal To', diff --git a/frontend/modules/alerts/utils/types.ts b/frontend/modules/alerts/utils/types.ts deleted file mode 100644 index 1ec3ff7f9..000000000 --- a/frontend/modules/alerts/utils/types.ts +++ /dev/null @@ -1,219 +0,0 @@ -// Alerts: - -export type Alert = AlertAcctBalPct | AlertAcctBalNum | AlertEvent | AlertFnCall | AlertTxFailure | AlertTxSuccess; - -export type AlertType = 'ACCT_BAL_NUM' | 'ACCT_BAL_PCT' | 'EVENT' | 'FN_CALL' | 'TX_FAILURE' | 'TX_SUCCESS'; - -interface BaseAlert { - environmentSubId: number; - projectSlug: string; - type: AlertType; -} - -interface ExtendedAlert extends BaseAlert { - enabledDestinations: Destination[]; - id: number; - isPaused: boolean; - name: string; -} - -interface AlertAcctBalNum extends ExtendedAlert { - rule: RuleAcctBalNum; - type: 'ACCT_BAL_NUM'; -} - -interface AlertAcctBalPct extends ExtendedAlert { - rule: RuleAcctBalPct; - type: 'ACCT_BAL_PCT'; -} - -interface AlertEvent extends ExtendedAlert { - rule: RuleEvent; - type: 'EVENT'; -} - -interface AlertFnCall extends ExtendedAlert { - rule: RuleFnCall; - type: 'FN_CALL'; -} - -interface AlertTxFailure extends ExtendedAlert { - rule: RuleTxFailure; - type: 'TX_FAILURE'; -} - -interface AlertTxSuccess extends ExtendedAlert { - rule: RuleTxSuccess; - type: 'TX_SUCCESS'; -} - -export type NewAlert = - | NewAlertAcctBalNum - | NewAlertAcctBalPct - | NewAlertEvent - | NewAlertFnCall - | NewAlertTxFailure - | NewAlertTxSuccess; - -interface ExtendedNewAlert extends BaseAlert { - destinations?: number[]; -} - -interface NewAlertAcctBalNum extends ExtendedNewAlert { - rule: RuleAcctBalNum; - type: 'ACCT_BAL_NUM'; -} - -interface NewAlertAcctBalPct extends ExtendedNewAlert { - rule: RuleAcctBalPct; - type: 'ACCT_BAL_PCT'; -} - -interface NewAlertEvent extends ExtendedNewAlert { - rule: RuleEvent; - type: 'EVENT'; -} - -interface NewAlertFnCall extends ExtendedNewAlert { - rule: RuleFnCall; - type: 'FN_CALL'; -} - -interface NewAlertTxFailure extends ExtendedNewAlert { - rule: RuleTxFailure; - type: 'TX_FAILURE'; -} - -interface NewAlertTxSuccess extends ExtendedNewAlert { - rule: RuleTxSuccess; - type: 'TX_SUCCESS'; -} - -export interface UpdateAlert { - id: number; - isPaused?: boolean; - name?: string; -} - -interface RuleAcctBalNum { - contract: string; - from: string | null; - to: string | null; -} - -interface RuleAcctBalPct { - contract: string; - from: string | null; - to: string | null; -} - -interface RuleEvent { - contract: string; - standard: string; - version: string; - event: string; -} - -interface RuleFnCall { - contract: string; - function: string; -} - -interface RuleTxFailure { - contract: string; -} - -interface RuleTxSuccess { - contract: string; -} - -export type AmountComparator = 'EQ' | 'LTE' | 'GTE' | 'RANGE'; - -// Destinations: - -export type Destination = EmailDestination | TelegramDestination | WebhookDestination; - -export type DestinationType = 'WEBHOOK' | 'EMAIL' | 'TELEGRAM'; - -interface BaseDestination { - projectSlug: string; - type: DestinationType; -} - -interface ExtendedDestination extends BaseDestination { - id: number; - isValid: boolean; - name: string; -} - -interface EmailDestination extends ExtendedDestination { - config: ConfigEmail; - type: 'EMAIL'; -} - -interface TelegramDestination extends ExtendedDestination { - config: ConfigTelegram; - type: 'TELEGRAM'; -} - -interface WebhookDestination extends ExtendedDestination { - config: ConfigWebhook & { - secret: string; - }; - type: 'WEBHOOK'; -} - -export type NewDestination = NewEmailDestination | NewTelegramDestination | NewWebhookDestination; - -export type UpdateDestination = { - id: number; - type: DestinationType; - config?: ConfigEmail | ConfigWebhook; - name: string; -}; - -type NewDestinationExtended = BaseDestination; - -interface NewEmailDestination extends NewDestinationExtended { - config: ConfigEmail; - type: 'EMAIL'; -} - -interface NewTelegramDestination extends NewDestinationExtended { - config: Record; - type: 'TELEGRAM'; -} - -interface NewWebhookDestination extends NewDestinationExtended { - config: ConfigWebhook; - type: 'WEBHOOK'; -} - -interface ConfigEmail { - email: string; -} - -interface ConfigTelegram { - chatTitle: string | null; - startToken: string; -} - -interface ConfigWebhook { - url: string; -} - -export interface TriggeredAlertsPagingResponse { - count: number; - page: TriggeredAlert[]; -} -export interface TriggeredAlert { - slug: string; - name: string; - alertId: number; - type: AlertType; - triggeredInBlockHash: string; - triggeredInTransactionHash: string; - triggeredInReceiptId: string; - triggeredAt: string; - extraData?: Record; -} diff --git a/frontend/modules/apis/components/ApiKeys.tsx b/frontend/modules/apis/components/ApiKeys.tsx index b531b89e4..e3c823d05 100644 --- a/frontend/modules/apis/components/ApiKeys.tsx +++ b/frontend/modules/apis/components/ApiKeys.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { Root as VisuallyHidden } from '@radix-ui/react-visually-hidden'; import { useRef, useState } from 'react'; @@ -17,7 +18,9 @@ import StarterGuide from '@/modules/core/components/StarterGuide'; import analytics from '@/utils/analytics'; import { authenticatedPost } from '@/utils/http'; import { StableId } from '@/utils/stable-ids'; -import type { ApiKey, Project } from '@/utils/types'; + +type Project = Api.Query.Output<'/projects/getDetails'>; +type ApiKey = Api.Query.Output<'/projects/getKeys'>[number]; const ROTATION_WARNING = 'Are you sure you would like to rotate this API key? The current key will be invalidated and future calls made with it will be rejected.'; @@ -31,7 +34,11 @@ export function ApiKeys({ project }: Props) { const [showRotationModal, setShowRotationModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false); - const [keyToRotate, setKeyToRotate] = useState({ keySlug: '', description: '', key: '' }); + const [keyToRotate, setKeyToRotate] = useState({ + keySlug: '', + description: '', + key: '', + }); async function rotateKey(keySlug: string) { showRotationModal && setShowRotationModal(false); @@ -46,10 +53,10 @@ export function ApiKeys({ project }: Props) { }); }); await mutateKeys(async (cachedKeys) => { - const { keySlug: newKeySlug, key: newKey }: { keySlug: string; key: string } = await authenticatedPost( - '/projects/rotateKey', - { slug: keySlug }, - ); + const { keySlug: newKeySlug, key: newKey } = await authenticatedPost('/projects/rotateKey' as const, { + slug: keySlug, + }); + analytics.track('DC Rotate API Key', { status: 'success', description: keyToRotate.description, diff --git a/frontend/modules/apis/components/ApiStats.tsx b/frontend/modules/apis/components/ApiStats.tsx index 784e1bb80..6e44e6292 100644 --- a/frontend/modules/apis/components/ApiStats.tsx +++ b/frontend/modules/apis/components/ApiStats.tsx @@ -1,4 +1,6 @@ // import { useApiKeys } from '@/hooks/api-keys'; +import type { Api } from '@pc/common/types/api'; +import type { RpcStats } from '@pc/common/types/rpcstats'; import { DateTime } from 'luxon'; import { useEffect, useState } from 'react'; import * as Charts from 'recharts'; @@ -17,11 +19,13 @@ import { Tooltip } from '@/components/lib/Tooltip'; import config from '@/utils/config'; import { formatNumber } from '@/utils/format-number'; import { StableId } from '@/utils/stable-ids'; -import type { Environment, Project } from '@/utils/types'; import { useApiStats } from '../hooks/api-stats'; import { timeRanges } from '../utils/constants'; -import type { ApiStatsData, TimeRangeValue } from '../utils/types'; + +type Project = Api.Query.Output<'/projects/getDetails'>; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; +type ApiStatsData = ReturnType; interface Props { environment?: Environment; @@ -30,7 +34,7 @@ interface Props { export function ApiStats({ environment, project }: Props) { // const { keys } = useApiKeys(project?.slug); // for filtering - const [selectedTimeRangeValue, setSelectedTimeRangeValue] = useState('30_DAYS'); + const [selectedTimeRangeValue, setSelectedTimeRangeValue] = useState('30_DAYS'); const [liveRefreshEnabled, setLiveRefreshEnabled] = useState(true); const selectedTimeRange = timeRanges.find((t) => t.value === selectedTimeRangeValue); const [rangeEndTime, setRangeEndTime] = useState(DateTime.now()); @@ -89,7 +93,7 @@ export function ApiStats({ environment, project }: Props) { setSelectedTimeRangeValue(value as TimeRangeValue)} + onValueChange={(value) => setSelectedTimeRangeValue(value as RpcStats.TimeRangeValue)} > {timeRanges?.map((range) => { return ( diff --git a/frontend/modules/apis/components/CreateApiKeyForm.tsx b/frontend/modules/apis/components/CreateApiKeyForm.tsx index dee1368be..c97a8a74a 100644 --- a/frontend/modules/apis/components/CreateApiKeyForm.tsx +++ b/frontend/modules/apis/components/CreateApiKeyForm.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useForm } from 'react-hook-form'; import { Button } from '@/components/lib/Button'; @@ -9,10 +10,11 @@ import { styled } from '@/styles/stitches'; import analytics from '@/utils/analytics'; import { authenticatedPost } from '@/utils/http'; import { StableId } from '@/utils/stable-ids'; -import type { ApiKey, Project } from '@/utils/types'; + +type Project = Api.Query.Output<'/projects/getDetails'>; interface NewKeyFormData { - description: ApiKey['description']; + description: string; } interface Props { @@ -30,12 +32,15 @@ export const CreateApiKeyForm = ({ show, setShow, project }: Props) => { const { register, handleSubmit, formState } = useForm(); async function createKey(description: string) { + if (!project) { + return; + } show && setShow(false); try { await mutateKeys(async (cachedKeys) => { - const newKey = await authenticatedPost('/projects/generateKey', { + const newKey = await authenticatedPost('/projects/generateKey', { description, - project: project?.slug, + project: project.slug, }); analytics.track('DC Create API Key', { status: 'success', diff --git a/frontend/modules/apis/hooks/api-stats.ts b/frontend/modules/apis/hooks/api-stats.ts index b2e6ccf3f..c4524793d 100644 --- a/frontend/modules/apis/hooks/api-stats.ts +++ b/frontend/modules/apis/hooks/api-stats.ts @@ -1,20 +1,16 @@ +import type { Api } from '@pc/common/types/api'; +import { RpcStats } from '@pc/common/types/rpcstats'; import type { DateTime, DateTimeUnit } from 'luxon'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { useIdentity } from '@/hooks/user'; import { authenticatedPost } from '@/utils/http'; -import type { Environment, Project } from '@/utils/types'; -import type { - ApiStatsData, - EndpointMetric, - EndpointMetricsDetailsResponseDto, - RpcStatsPagingResponse, - TimeRangeValue, -} from '../utils/types'; +type Project = Api.Query.Output<'/projects/getDetails'>; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; -function timeRangeToDates(timeRangeValue: TimeRangeValue, endTime: DateTime): [DateTime, DateTime] { +function timeRangeToDates(timeRangeValue: RpcStats.TimeRangeValue, endTime: DateTime): [DateTime, DateTime] { switch (timeRangeValue) { case '30_DAYS': return [endTime.minus({ days: 30 }), endTime]; @@ -29,7 +25,7 @@ function timeRangeToDates(timeRangeValue: TimeRangeValue, endTime: DateTime): [D } } -function totalMetrics(endpointMetrics: EndpointMetricsDetailsResponseDto[]): { +function totalMetrics(endpointMetrics: RpcStats.Metrics[]): { successCount: number; errorCount: number; weightedTotalLatency: number; @@ -46,7 +42,9 @@ function totalMetrics(endpointMetrics: EndpointMetricsDetailsResponseDto[]): { return { successCount, errorCount, weightedTotalLatency }; } -function endpointTotals(endpointMetrics: EndpointMetricsDetailsResponseDto[]): EndpointMetric[] { +function endpointTotals( + endpointMetrics: RpcStats.Metrics[], +): (Pick & { totalCount: number })[] { return endpointMetrics.map((endpointMetric) => { return { endpointMethod: endpointMetric.endpointMethod, @@ -64,7 +62,7 @@ export enum DateTimeResolution { ONE_DAY = 'ONE_DAY', } -function resolutionForTimeRange(timeRangeValue: TimeRangeValue): DateTimeResolution { +function resolutionForTimeRange(timeRangeValue: RpcStats.TimeRangeValue): RpcStats.DateTimeResolution { switch (timeRangeValue) { case '30_DAYS': return DateTimeResolution.ONE_HOUR; @@ -79,11 +77,6 @@ function resolutionForTimeRange(timeRangeValue: TimeRangeValue): DateTimeResolut } } -export enum GroupBy { - date = 'date', - endpoint = 'endpoint', -} - function toLuxonDateTimeResolution(dateTimeResolution: DateTimeResolution) { switch (dateTimeResolution) { case DateTimeResolution.FIFTEEN_SECONDS: @@ -98,13 +91,12 @@ function toLuxonDateTimeResolution(dateTimeResolution: DateTimeResolution) { } function fillEmptyDateValues( - dateValues: Array, + dateValues: RpcStats.Metrics[], startDateTime: DateTime, endDateTime: DateTime, dateTimeResolution: DateTimeResolution, ) { - const filledDateValues: Omit[] = - []; + const filledDateValues: Omit[] = []; const luxonDateTimeResolution = toLuxonDateTimeResolution(dateTimeResolution); // set currentDateTime to start of next dateTimeResolution let currentDateTime = startDateTime @@ -132,22 +124,24 @@ function fillEmptyDateValues( return filledDateValues; } +type EndpointMetrics = Api.Query.Output<'/rpcstats/endpointMetrics'>; + export function useApiStats( environment: Environment | undefined, project: Project | undefined, - timeRangeValue: TimeRangeValue, + timeRangeValue: RpcStats.TimeRangeValue, rangeEndTime: DateTime, -): ApiStatsData | undefined { +) { const identity = useIdentity(); const [startDateTime, endDateTime] = timeRangeToDates(timeRangeValue, rangeEndTime); // convert timeRangeValue to params for use in the API call const dateTimeResolution = resolutionForTimeRange(timeRangeValue); - const [dataByDate, setDataByDate] = useState(); - const [dataByEndpoint, setDataByEndpoint] = useState(); + const [dataByDate, setDataByDate] = useState(); + const [dataByEndpoint, setDataByEndpoint] = useState(); - const { data: dataByDateResponse } = useSWR( + const { data: dataByDateResponse } = useSWR( identity && environment && project && startDateTime && endDateTime ? [ - '/rpcstats/endpointMetrics', + '/rpcstats/endpointMetrics' as const, 'date', environment.subId, identity.uid, @@ -157,20 +151,22 @@ export function useApiStats( : null, (key) => { return authenticatedPost(key, { - environmentSubId: environment?.subId, - projectSlug: project?.slug, - startDateTime, - endDateTime, - dateTimeResolution, - grouping: [GroupBy.date], + environmentSubId: environment!.subId, + projectSlug: project!.slug, + startDateTime: startDateTime.toString(), + endDateTime: endDateTime.toString(), + filter: { + type: RpcStats.MetricGroupBy.DATE, + dateTimeResolution, + }, }); }, ); - const { data: dataByEndpointResponse } = useSWR( + const { data: dataByEndpointResponse } = useSWR( identity && environment && project && startDateTime && endDateTime ? [ - '/rpcstats/endpointMetrics', + '/rpcstats/endpointMetrics' as const, 'endpoint', environment.subId, identity.uid, @@ -180,11 +176,11 @@ export function useApiStats( : null, (key) => { return authenticatedPost(key, { - environmentSubId: environment?.subId, - projectSlug: project?.slug, - startDateTime, - endDateTime, - grouping: [GroupBy.endpoint], + environmentSubId: environment!.subId, + projectSlug: project!.slug, + startDateTime: startDateTime.toString(), + endDateTime: endDateTime.toString(), + filter: { type: RpcStats.MetricGroupBy.ENDPOINT }, }); }, ); diff --git a/frontend/modules/apis/utils/constants.ts b/frontend/modules/apis/utils/constants.ts index 51659ae5d..1883ffb06 100644 --- a/frontend/modules/apis/utils/constants.ts +++ b/frontend/modules/apis/utils/constants.ts @@ -1,6 +1,6 @@ -import type { TimeRange } from './types'; +import type { RpcStats } from '@pc/common/types/rpcstats'; -export const timeRanges: TimeRange[] = [ +export const timeRanges: { value: RpcStats.TimeRangeValue; label: string }[] = [ { label: '15 Minutes', value: '15_MINS', diff --git a/frontend/modules/apis/utils/types.ts b/frontend/modules/apis/utils/types.ts deleted file mode 100644 index 6f984d4f9..000000000 --- a/frontend/modules/apis/utils/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -export type TimeRangeValue = '15_MINS' | '1_HRS' | '24_HRS' | '7_DAYS' | '30_DAYS'; - -export interface TimeRange { - label: string; - value: TimeRangeValue; -} - -export interface ApiDataPoint { - label: string; - value: number; -} - -export interface EndpointMetric { - errorCount: number; - invalidCount?: number; - endpointMethod: string; - successCount: number; - totalCount?: number; -} - -export interface ApiStatsData { - requestLatencyMs: number; - requestSuccessRatePercentage: number; - totalInvalidRequests: number; - totalRequestVolume: number; - requestStatusPerMethod: EndpointMetricsDetailsResponseDto[]; - - charts: { - totalRequestVolume: Omit[]; - totalRequestsPerMethod: EndpointMetric[]; - totalRequestsPerStatus: ApiDataPoint[]; - }; -} - -export interface Net { - MAINNET: 'MAINNET'; - TESTNET: 'TESTNET'; -} - -export interface EndpointMetricsDetailsResponseDto { - apiKeyIdentifier: string; - endpointGroup?: string; - endpointMethod: string; - network: Net; - windowStart?: string; - windowEnd?: string; - successCount: number; - errorCount: number; - minLatency: number; - maxLatency: number; - meanLatency: number; -} - -export interface RpcStatsPagingResponse { - count: number; - page: Array; -} diff --git a/frontend/modules/contracts/components/AddContractForm.tsx b/frontend/modules/contracts/components/AddContractForm.tsx index 9c529be4f..049414129 100644 --- a/frontend/modules/contracts/components/AddContractForm.tsx +++ b/frontend/modules/contracts/components/AddContractForm.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -19,7 +20,10 @@ import { formRegex } from '@/utils/constants'; import { deployContractTemplate } from '@/utils/deploy-contract-template'; import { authenticatedPost } from '@/utils/http'; import { StableId } from '@/utils/stable-ids'; -import type { Contract, Environment, Project } from '@/utils/types'; + +type Project = Api.Query.Output<'/projects/getDetails'>; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; +type Contract = Api.Query.Output<'/projects/getContracts'>[number]; interface Props { project: Project; @@ -83,7 +87,7 @@ export function AddContractForm(props: Props) { const submitForm: SubmitHandler = async ({ contractAddress }) => { const contractAddressValue = contractAddress.trim(); try { - const contract: Contract = await authenticatedPost('/projects/addContract', { + const contract = await authenticatedPost('/projects/addContract', { project: props.project.slug, environment: props.environment.subId, address: contractAddressValue, diff --git a/frontend/modules/contracts/components/ContractAbi.tsx b/frontend/modules/contracts/components/ContractAbi.tsx index c57d08780..f66f35b4a 100644 --- a/frontend/modules/contracts/components/ContractAbi.tsx +++ b/frontend/modules/contracts/components/ContractAbi.tsx @@ -1,9 +1,12 @@ +import type { Api } from '@pc/common/types/api'; + import { CodeBlock } from '@/components/lib/CodeBlock'; import { Flex } from '@/components/lib/Flex'; import { Message } from '@/components/lib/Message'; import { Spinner } from '@/components/lib/Spinner'; import { useAnyAbi } from '@/modules/contracts/hooks/abi'; -import type { Contract } from '@/utils/types'; + +type Contract = Api.Query.Output<'/projects/getContracts'>[number]; interface Props { contract?: Contract; diff --git a/frontend/modules/contracts/components/ContractDetails.tsx b/frontend/modules/contracts/components/ContractDetails.tsx index 2fee81417..e9f5de429 100644 --- a/frontend/modules/contracts/components/ContractDetails.tsx +++ b/frontend/modules/contracts/components/ContractDetails.tsx @@ -1,3 +1,6 @@ +import type { Api } from '@pc/common/types/api'; +import type { Net } from '@pc/database/clients/core'; + import TransactionAction from '@/components/explorer/transactions/TransactionAction'; import { NetContext } from '@/components/explorer/utils/NetContext'; import { Box } from '@/components/lib/Box'; @@ -10,10 +13,12 @@ import { Text } from '@/components/lib/Text'; import { useContractMetrics } from '@/hooks/contracts'; import { convertYoctoToNear } from '@/utils/convert-near'; import { formatBytes } from '@/utils/format-bytes'; -import type { Environment, NetOption } from '@/utils/types'; -import type { Contract } from '@/utils/types'; import { useFinalityStatus, useRecentTransactions } from '../hooks/recent-transactions'; + +type Contract = Api.Query.Output<'/projects/getContract'>; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; + interface Props { contract?: Contract; environment?: Environment; @@ -61,7 +66,7 @@ export function ContractDetails({ contract, environment }: Props) { ); } -function RecentTransactionList({ contract, net }: { contract?: Contract; net?: NetOption }) { +function RecentTransactionList({ contract, net }: { contract?: Contract; net?: Net }) { // NOTE: This component and following code is legacy and will soon be replaced by new explorer components. const { finalityStatus } = useFinalityStatus(net); diff --git a/frontend/modules/contracts/components/ContractInteract.tsx b/frontend/modules/contracts/components/ContractInteract.tsx index e8dcd53fb..56e696b92 100644 --- a/frontend/modules/contracts/components/ContractInteract.tsx +++ b/frontend/modules/contracts/components/ContractInteract.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useEffect, useState } from 'react'; import { Container } from '@/components/lib/Container'; @@ -7,7 +8,8 @@ import { openToast } from '@/components/lib/Toast'; import { ContractTransaction } from '@/modules/contracts/components/ContractTransaction'; import { UploadContractAbi } from '@/modules/contracts/components/UploadContractAbi'; import { useAnyAbi } from '@/modules/contracts/hooks/abi'; -import type { Contract } from '@/utils/types'; + +type Contract = Api.Query.Output<'/projects/getContract'>; interface Props { contract?: Contract; diff --git a/frontend/modules/contracts/components/ContractTransaction.tsx b/frontend/modules/contracts/components/ContractTransaction.tsx index d0761fde7..8f7dfbb7b 100644 --- a/frontend/modules/contracts/components/ContractTransaction.tsx +++ b/frontend/modules/contracts/components/ContractTransaction.tsx @@ -1,4 +1,5 @@ import type { WalletSelector } from '@near-wallet-selector/core'; +import type { Api } from '@pc/common/types/api'; import JSBI from 'jsbi'; import type { AbiParameter, AbiRoot, AnyContract as AbiContract } from 'near-abi-client-js'; import { useRouter } from 'next/router'; @@ -37,9 +38,10 @@ import { convertNearToYocto } from '@/utils/convert-near'; import { numberInputHandler } from '@/utils/input-handlers'; import { sanitizeNumber } from '@/utils/sanitize-number'; import { StableId } from '@/utils/stable-ids'; -import type { Contract } from '@/utils/types'; import { validateInteger, validateMaxNearU128, validateMaxYoctoU128 } from '@/utils/validations'; +type Contract = Api.Query.Output<'/projects/getContract'>; + const TextItalic = styled(Text, { fontStyle: 'italic', }); diff --git a/frontend/modules/contracts/components/DeleteContractModal.tsx b/frontend/modules/contracts/components/DeleteContractModal.tsx index 9fe277a1f..f17273e5d 100644 --- a/frontend/modules/contracts/components/DeleteContractModal.tsx +++ b/frontend/modules/contracts/components/DeleteContractModal.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useState } from 'react'; import { Flex } from '@/components/lib/Flex'; @@ -6,7 +7,8 @@ import { Text } from '@/components/lib/Text'; import { openToast } from '@/components/lib/Toast'; import { ConfirmModal } from '@/components/modals/ConfirmModal'; import { deleteContract } from '@/hooks/contracts'; -import type { Contract } from '@/utils/types'; + +type Contract = Api.Query.Output<'/projects/getContracts'>[number]; interface Props { contract: Contract; diff --git a/frontend/modules/contracts/hooks/abi.ts b/frontend/modules/contracts/hooks/abi.ts index d6d5b3a22..d5f7ad2ba 100644 --- a/frontend/modules/contracts/hooks/abi.ts +++ b/frontend/modules/contracts/hooks/abi.ts @@ -1,3 +1,5 @@ +import type { Api } from '@pc/common/types/api'; +import type { Net } from '@pc/database/clients/core'; import type { AbiRoot, AnyContract } from 'near-abi-client-js'; import { Contract as NearContract } from 'near-abi-client-js'; import { connect, keyStores } from 'near-api-js'; @@ -7,16 +9,12 @@ import { useIdentity } from '@/hooks/user'; import analytics from '@/utils/analytics'; import config from '@/utils/config'; import { authenticatedPost } from '@/utils/http'; -import type { Contract, NetOption } from '@/utils/types'; import { inspectContract } from '../utils/embedded-abi'; const RPC_API_ENDPOINT = config.url.rpc.default.TESTNET; -interface AbiResponse { - contractSlug: string; - abi: AbiRoot; -} +type Contract = Api.Query.Output<'/projects/getContract'>; // Prefers an embedded ABI in the wasm, if there is one, else returns any manually uploaded ABI. export const useAnyAbi = (contract: Contract | undefined) => { @@ -37,12 +35,12 @@ export const useAnyAbi = (contract: Contract | undefined) => { return { contractAbi, error }; }; -export const useEmbeddedAbi = (net: NetOption | undefined, address: string | undefined) => { +export const useEmbeddedAbi = (net: Net | undefined, address: string | undefined) => { const { data: embeddedAbi, error, mutate, - } = useSWR(net && address ? [net, address] : null, (net, address) => { + } = useSWR(net && address ? [net, address] : null, (net, address) => { return inspectContract(net, address); }); return { embeddedAbi, error, mutate }; @@ -54,8 +52,8 @@ export const useContractAbi = (contract: string | undefined) => { data: contractAbi, error, mutate, - } = useSWR( - identity && contract ? ['/abi/getContractAbi', contract, identity.uid] : null, + } = useSWR( + identity && contract ? ['/abi/getContractAbi' as const, contract, identity.uid] : null, (key, contract) => { return authenticatedPost(key, { contract }); }, diff --git a/frontend/modules/contracts/hooks/recent-transactions.ts b/frontend/modules/contracts/hooks/recent-transactions.ts index 53b769177..19673f27a 100644 --- a/frontend/modules/contracts/hooks/recent-transactions.ts +++ b/frontend/modules/contracts/hooks/recent-transactions.ts @@ -1,20 +1,20 @@ +import type * as RPC from '@pc/common/types/rpc'; +import type { Net } from '@pc/database/clients/core'; import JSBI from 'jsbi'; import useSWR from 'swr'; -import type { Transaction } from '@/components/explorer/transactions/types'; import { useIdentity } from '@/hooks/user'; import config from '@/utils/config'; import { authenticatedPost } from '@/utils/http'; -import type { FinalityStatus, NetOption } from '@/utils/types'; -export function useRecentTransactions(contract: string | undefined, net: NetOption | undefined) { +export function useRecentTransactions(contract: string | undefined, net: Net | undefined) { const identity = useIdentity(); // TODO (P2+) look into whether using contracts as part of the SWR key will cause a large // amount of unnecessary caching, since every modification to the contract set will be a // separate key - const { data: transactions, error } = useSWR( - identity && contract && net ? ['/projects/getTransactions', contract, net, identity.uid] : null, + const { data: transactions, error } = useSWR( + identity && contract && net ? ['/explorer/getTransactions' as const, contract, net, identity.uid] : null, (key, contracts, net) => { return authenticatedPost(key, { contracts: contracts.split(','), @@ -26,8 +26,13 @@ export function useRecentTransactions(contract: string | undefined, net: NetOpti return { transactions, error }; } -export function useFinalityStatus(net: NetOption | undefined) { - const { data, error } = useSWR(net ? [config.url.rpc.default[net]] : null, async (key) => { +export interface FinalityStatus { + finalBlockHeight: number; + finalBlockTimestampNanosecond: JSBI; +} + +export function useFinalityStatus(net: Net | undefined) { + const { data, error } = useSWR(net ? [config.url.rpc.default[net]] : null, async (key) => { return await fetch(key, { method: 'POST', headers: { @@ -41,7 +46,7 @@ export function useFinalityStatus(net: NetOption | undefined) { finality: 'final', }, }), - }).then((res) => res.json()); + }).then((res) => res.json() as Promise<{ result: RPC.ResponseMapping['block'] }>); }); const finalBlock = data?.result; diff --git a/frontend/modules/contracts/hooks/wallet-selector.ts b/frontend/modules/contracts/hooks/wallet-selector.ts index 21d0ff8b8..d34a5af96 100644 --- a/frontend/modules/contracts/hooks/wallet-selector.ts +++ b/frontend/modules/contracts/hooks/wallet-selector.ts @@ -8,12 +8,12 @@ import { setupNearWallet } from '@near-wallet-selector/near-wallet'; import nearWalletIconUrl from '@near-wallet-selector/near-wallet/assets/near-wallet-icon.png'; import { setupSender } from '@near-wallet-selector/sender'; import senderIconUrl from '@near-wallet-selector/sender/assets/sender-icon.png'; +import type { Net } from '@pc/database/clients/core'; import { useCallback, useEffect, useState } from 'react'; import { distinctUntilChanged, map } from 'rxjs'; import { openToast } from '@/components/lib/Toast'; import { useSelectedProject } from '@/hooks/selected-project'; -import type { NetOption } from '@/utils/types'; // Cache in module to ensure we don't re-init let selector: WalletSelector | null = null; @@ -25,7 +25,7 @@ let modal: WalletSelectorModal | null = null; export const useWalletSelector = (contractId: string | undefined) => { const [accounts, setAccounts] = useState>([]); const { environment } = useSelectedProject(); - const [prevNet, setPrevNet] = useState(); + const [prevNet, setPrevNet] = useState(); const init = useCallback(async () => { if (!contractId || !environment) return null; diff --git a/frontend/modules/contracts/utils/embedded-abi.ts b/frontend/modules/contracts/utils/embedded-abi.ts index a7e4bd7bc..b3d287140 100644 --- a/frontend/modules/contracts/utils/embedded-abi.ts +++ b/frontend/modules/contracts/utils/embedded-abi.ts @@ -1,15 +1,15 @@ +import type { Net } from '@pc/database/clients/core'; import * as fzstd from 'fzstd'; import type { AbiRoot } from 'near-abi-client-js'; import * as nearApi from 'near-api-js'; import config from '@/utils/config'; import { assertUnreachable } from '@/utils/helpers'; -import type { NetOption } from '@/utils/types'; // This version of NAJ needs a keyStore. We are only making view calls so we shouldn't need this. If we upgrade to v1.0.0, the keyStore should be removed. const myKeyStore = new nearApi.keyStores.InMemoryKeyStore(); -export async function inspectContract(net: NetOption, contract: string) { +export async function inspectContract(net: Net, contract: string) { let nodeUrl; switch (net) { case 'MAINNET': diff --git a/frontend/modules/core/components/StarterGuide.tsx b/frontend/modules/core/components/StarterGuide.tsx index 65d1b4a6c..f78dac31c 100644 --- a/frontend/modules/core/components/StarterGuide.tsx +++ b/frontend/modules/core/components/StarterGuide.tsx @@ -1,3 +1,4 @@ +import type { Net } from '@pc/database/clients/core'; import { useEffect, useState } from 'react'; import * as Accordion from '@/components/lib/Accordion'; @@ -11,7 +12,6 @@ import { TextLink } from '@/components/lib/TextLink'; import { useSelectedProject } from '@/hooks/selected-project'; import config from '@/utils/config'; import { StableId } from '@/utils/stable-ids'; -import type { NetOption } from '@/utils/types'; const NAJ_STARTER_TEMPLATE = `const { connect, keyStores } = require("near-api-js"); @@ -77,7 +77,7 @@ export default function StarterGuide() { // TODO (P2+) determine net by other means than subId useEffect(() => { - const net: NetOption = environment?.subId === 2 ? 'MAINNET' : 'TESTNET'; + const net: Net = environment?.subId === 2 ? 'MAINNET' : 'TESTNET'; if (!environment?.subId) { return; diff --git a/frontend/modules/core/components/tutorials/SetApiKey.tsx b/frontend/modules/core/components/tutorials/SetApiKey.tsx index 326a198b2..17c1f430f 100644 --- a/frontend/modules/core/components/tutorials/SetApiKey.tsx +++ b/frontend/modules/core/components/tutorials/SetApiKey.tsx @@ -5,7 +5,7 @@ import { useSelectedProject } from '@/hooks/selected-project'; export default function ApiKey() { const { project } = useSelectedProject(); const { keys } = useApiKeys(project?.slug); - const key = keys?.TESTNET; + const key = keys?.[0].key; return ( <> {`near set-api-key $NEAR_CLI_TESTNET_RPC_SERVER_URL ${key}`} diff --git a/frontend/package.json b/frontend/package.json index cf5eb6f02..0c59a0330 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@near-wallet-selector/my-near-wallet": "^7.0.3", "@near-wallet-selector/near-wallet": "^7.0.3", "@near-wallet-selector/sender": "^7.0.3", + "@pc/common": "*", "@pc/database": "*", "@radix-ui/react-accordion": "^1.0.0", "@radix-ui/react-dialog": "^1.0.0", diff --git a/frontend/pages/alerts/edit-alert/[alertId].tsx b/frontend/pages/alerts/edit-alert/[alertId].tsx index e9ca76c82..f2f5c0f61 100644 --- a/frontend/pages/alerts/edit-alert/[alertId].tsx +++ b/frontend/pages/alerts/edit-alert/[alertId].tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import Link from 'next/link'; import { useRouter } from 'next/router'; import type { ReactNode } from 'react'; @@ -29,7 +30,6 @@ import { useAlert, } from '@/modules/alerts/hooks/alerts'; import { alertTypes, amountComparators } from '@/modules/alerts/utils/constants'; -import type { Alert } from '@/modules/alerts/utils/types'; import { convertYoctoToNear } from '@/utils/convert-near'; import { formatNumber } from '@/utils/format-number'; import { StableId } from '@/utils/stable-ids'; @@ -39,7 +39,7 @@ interface NameFormData { name: string; } -async function update(alert: Alert, data: { isPaused?: boolean; name?: string }) { +async function update(alert: Api.Mutation.Input<'/alerts/updateAlert'>, data: { isPaused?: boolean; name?: string }) { try { await updateAlert({ id: alert.id, @@ -333,10 +333,10 @@ const EditAlert: NextPageWithLayout = () => {

Condition

- + -
{alertTypes[alert.type].name}
- {alertTypes[alert.type].description} +
{alertTypes[alert.rule.type].name}
+ {alertTypes[alert.rule.type].description}
@@ -363,6 +363,8 @@ const EditAlert: NextPageWithLayout = () => { ); }; +type Alert = Api.Query.Output<'/alerts/getAlertDetails'>; + function AlertSettings({ alert }: { alert: Alert }) { function Wrapper({ children }: { children: ReactNode }) { return ( @@ -375,7 +377,7 @@ function AlertSettings({ alert }: { alert: Alert }) { ); } - if (alert.type === 'ACCT_BAL_NUM') { + if (alert.rule.type === 'ACCT_BAL_NUM') { const comparator = returnAmountComparator(alert.rule.from, alert.rule.to); return ( @@ -412,7 +414,7 @@ function AlertSettings({ alert }: { alert: Alert }) { ); } - if (alert.type === 'ACCT_BAL_PCT') { + if (alert.rule.type === 'ACCT_BAL_PCT') { const comparator = returnAmountComparator(alert.rule.from, alert.rule.to); return ( @@ -447,7 +449,7 @@ function AlertSettings({ alert }: { alert: Alert }) { ); } - if (alert.type === 'EVENT') { + if (alert.rule.type === 'EVENT') { return ( @@ -472,7 +474,7 @@ function AlertSettings({ alert }: { alert: Alert }) { ); } - if (alert.type === 'FN_CALL') { + if (alert.rule.type === 'FN_CALL') { return ( diff --git a/frontend/pages/alerts/new-alert.tsx b/frontend/pages/alerts/new-alert.tsx index 7b77e4a5e..a7f6c132b 100644 --- a/frontend/pages/alerts/new-alert.tsx +++ b/frontend/pages/alerts/new-alert.tsx @@ -1,3 +1,5 @@ +import type { Alerts } from '@pc/common/types/alerts'; +import type { Api } from '@pc/common/types/api'; import { useCombobox } from 'downshift'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -26,8 +28,6 @@ import { useSelectedProject } from '@/hooks/selected-project'; import { DestinationsSelector } from '@/modules/alerts/components/DestinationsSelector'; import { createAlert, useAlerts } from '@/modules/alerts/hooks/alerts'; import { alertTypeOptions, amountComparatorOptions } from '@/modules/alerts/utils/constants'; -import type { AlertType, AmountComparator } from '@/modules/alerts/utils/types'; -import { NewAlert } from '@/modules/alerts/utils/types'; import { formRegex } from '@/utils/constants'; import { convertNearToYocto } from '@/utils/convert-near'; import { assertUnreachable } from '@/utils/helpers'; @@ -35,14 +35,14 @@ import { numberInputHandler } from '@/utils/input-handlers'; import { mergeInputProps } from '@/utils/merge-input-props'; import { sanitizeNumber } from '@/utils/sanitize-number'; import { StableId } from '@/utils/stable-ids'; -import type { Contract, Environment, NextPageWithLayout, Project } from '@/utils/types'; +import type { NextPageWithLayout } from '@/utils/types'; import { validateMaxNearDecimalLength, validateMaxNearU128 } from '@/utils/validations'; interface FormData { contract: string; - type: AlertType; + type: Alerts.RuleType; acctBalRule?: { - comparator: AmountComparator; + comparator: Alerts.Comparator; }; acctBalNumRule?: { from: string; @@ -62,6 +62,10 @@ interface FormData { }; } +type Contract = Api.Query.Output<'/projects/getContracts'>[number]; +type Project = Api.Query.Output<'/projects/getDetails'>; +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; + const NewAlert: NextPageWithLayout = () => { const router = useRouter(); const form = useForm(); @@ -569,7 +573,7 @@ function returnNewAlertBody( destinations: number[], project?: Project, environment?: Environment, -): NewAlert { +): Api.Mutation.Input<'/alerts/createAlert'> { if (!project || !environment) throw new Error('No project or environment selected.'); const base = { @@ -584,8 +588,8 @@ function returnNewAlertBody( return { ...base, - type: 'ACCT_BAL_NUM', rule: { + type: 'ACCT_BAL_NUM', contract: data.contract, ...returnAcctBalNumBody(data.acctBalRule.comparator, data.acctBalNumRule), }, @@ -595,8 +599,8 @@ function returnNewAlertBody( return { ...base, - type: 'ACCT_BAL_PCT', rule: { + type: 'ACCT_BAL_PCT', contract: data.contract, ...returnAcctBalBody(data.acctBalRule.comparator, data.acctBalPctRule), }, @@ -604,8 +608,8 @@ function returnNewAlertBody( case 'EVENT': return { ...base, - type: 'EVENT', rule: { + type: 'EVENT', contract: data.contract, ...data.eventRule!, }, @@ -613,8 +617,8 @@ function returnNewAlertBody( case 'FN_CALL': return { ...base, - type: 'FN_CALL', rule: { + type: 'FN_CALL', contract: data.contract, ...data.fnCallRule!, }, @@ -622,16 +626,16 @@ function returnNewAlertBody( case 'TX_FAILURE': return { ...base, - type: 'TX_FAILURE', rule: { + type: 'TX_FAILURE', contract: data.contract, }, }; case 'TX_SUCCESS': return { ...base, - type: 'TX_SUCCESS', rule: { + type: 'TX_SUCCESS', contract: data.contract, }, }; @@ -640,13 +644,13 @@ function returnNewAlertBody( } } -function returnAcctBalNumBody(comparator: AmountComparator, { from, to }: { from: string; to: string }) { +function returnAcctBalNumBody(comparator: Alerts.Comparator, { from, to }: { from: string; to: string }) { from = convertNearToYocto(from); to = convertNearToYocto(to); return returnAcctBalBody(comparator, { from, to }); } -function returnAcctBalBody(comparator: AmountComparator, { from, to }: { from: string; to: string }) { +function returnAcctBalBody(comparator: Alerts.Comparator, { from, to }: { from: string; to: string }) { switch (comparator) { case 'EQ': return { diff --git a/frontend/pages/alerts/triggered-alert/[triggeredAlertId].tsx b/frontend/pages/alerts/triggered-alert/[triggeredAlertId].tsx index f6dba4ccf..709b4d58d 100644 --- a/frontend/pages/alerts/triggered-alert/[triggeredAlertId].tsx +++ b/frontend/pages/alerts/triggered-alert/[triggeredAlertId].tsx @@ -1,3 +1,4 @@ +import type { Alerts, TriggeredAlerts } from '@pc/common/types/alerts'; import { DateTime } from 'luxon'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -21,7 +22,6 @@ import { useSelectedProject, useSelectedProjectSync } from '@/hooks/selected-pro import { useAlert } from '@/modules/alerts/hooks/alerts'; import { useTriggeredAlertDetails } from '@/modules/alerts/hooks/triggered-alerts'; import { alertTypes } from '@/modules/alerts/utils/constants'; -import type { Alert, TriggeredAlert } from '@/modules/alerts/utils/types'; import config from '@/utils/config'; import { StableId } from '@/utils/stable-ids'; import type { NextPageWithLayout } from '@/utils/types'; @@ -72,12 +72,12 @@ const ViewTriggeredAlert: NextPageWithLayout = () => { useSelectedProjectSync(alert?.environmentSubId, alert?.projectSlug); - function alertType(triggeredAlert: TriggeredAlert) { + function alertType(triggeredAlert: TriggeredAlerts.TriggeredAlert) { if (!triggeredAlert) return alertTypes.EVENT; return alertTypes[triggeredAlert.type]; } - function alertContract(alert: Alert) { + function alertContract(alert: Alerts.Alert) { return alert?.rule?.contract; } @@ -145,21 +145,25 @@ const ViewTriggeredAlert: NextPageWithLayout = () => { - - - + {triggeredAlert.triggeredInTransactionHash ? ( + + ) : null} + + {triggeredAlert.triggeredInReceiptId ? ( + + ) : null} ; const ViewContract: NextPageWithLayout = () => { const router = useRouter(); diff --git a/frontend/pages/contracts/index.tsx b/frontend/pages/contracts/index.tsx index 8529cee46..71e843ed0 100644 --- a/frontend/pages/contracts/index.tsx +++ b/frontend/pages/contracts/index.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { useRouter } from 'next/router'; import type { Dispatch, ReactNode, SetStateAction } from 'react'; import { useState } from 'react'; @@ -23,9 +24,10 @@ import { useAnyAbi } from '@/modules/contracts/hooks/abi'; import { convertYoctoToNear } from '@/utils/convert-near'; import { formatBytes } from '@/utils/format-bytes'; import { StableId } from '@/utils/stable-ids'; -import type { Contract } from '@/utils/types'; import type { NextPageWithLayout } from '@/utils/types'; +type Contract = Api.Query.Output<'/projects/getContracts'>[number]; + const ListContracts: NextPageWithLayout = () => { const { project, environment } = useSelectedProject(); const { contracts, mutate } = useContracts(project?.slug, environment?.subId); diff --git a/frontend/pages/new-nft-tutorial.tsx b/frontend/pages/new-nft-tutorial.tsx index e7cda051f..9392fa1da 100644 --- a/frontend/pages/new-nft-tutorial.tsx +++ b/frontend/pages/new-nft-tutorial.tsx @@ -16,7 +16,6 @@ import analytics from '@/utils/analytics'; import { formValidations } from '@/utils/constants'; import { authenticatedPost } from '@/utils/http'; import { StableId } from '@/utils/stable-ids'; -import type { Project } from '@/utils/types'; import type { NextPageWithLayout } from '@/utils/types'; interface NewProjectFormData { @@ -35,7 +34,7 @@ const NewNftTutorial: NextPageWithLayout = () => { try { router.prefetch(path); - const project: Project = await authenticatedPost('/projects/create', { + const project = await authenticatedPost('/projects/create', { name, tutorial, }); diff --git a/frontend/pages/new-project.tsx b/frontend/pages/new-project.tsx index 61bc4a940..b3705fbdb 100644 --- a/frontend/pages/new-project.tsx +++ b/frontend/pages/new-project.tsx @@ -20,7 +20,6 @@ import analytics from '@/utils/analytics'; import { formValidations } from '@/utils/constants'; import { authenticatedPost } from '@/utils/http'; import { StableId } from '@/utils/stable-ids'; -import type { Project } from '@/utils/types'; import type { NextPageWithLayout } from '@/utils/types'; const PERSONAL_ORGANIZATION_NAME = 'Personal organization'; @@ -47,7 +46,7 @@ const NewProject: NextPageWithLayout = () => { const createProject: SubmitHandler = async ({ projectName, projectOrg }) => { try { router.prefetch('/apis?tab=keys'); - const project = await authenticatedPost('/projects/create', { name: projectName, org: projectOrg }); + const project = await authenticatedPost('/projects/create', { name: projectName, org: projectOrg }); analytics.track('DC Create New Project', { status: 'success', name: projectName, diff --git a/frontend/pages/organizations/[slug].tsx b/frontend/pages/organizations/[slug].tsx index 87ff44508..17af2ccdd 100644 --- a/frontend/pages/organizations/[slug].tsx +++ b/frontend/pages/organizations/[slug].tsx @@ -1,3 +1,5 @@ +import type { Api } from '@pc/common/types/api'; +import type { OrgRole } from '@pc/database/clients/core'; import { useRouter } from 'next/router'; import React, { useCallback, useEffect, useState } from 'react'; import type { UseFormReturn } from 'react-hook-form'; @@ -34,11 +36,11 @@ import { useIdentity } from '@/hooks/user'; import { styled } from '@/styles/stitches'; import { formValidations } from '@/utils/constants'; import { StableId } from '@/utils/stable-ids'; -import type { NextPageWithLayout, Organization, OrganizationMember, OrganizationRole } from '@/utils/types'; +import type { NextPageWithLayout } from '@/utils/types'; -const ROLES: OrganizationRole[] = ['COLLABORATOR', 'ADMIN']; +const ROLES: OrgRole[] = ['COLLABORATOR', 'ADMIN']; -const ROLE_NAMES: Record = { +const ROLE_NAMES: Record = { COLLABORATOR: 'Collaborator', ADMIN: 'Admin', }; @@ -91,11 +93,11 @@ const RemoveUserDialog = ({ return; } if ('uid' in userData) { - removeUserMutation.mutate({ uid: userData.uid }); + removeUserMutation.mutate({ user: userData.uid, org: orgSlug }); } else { - removeInviteMutation.mutate({ email: userData.email }); + removeInviteMutation.mutate({ email: userData.email, org: orgSlug }); } - }, [removeInviteMutation, removeUserMutation, userData]); + }, [removeInviteMutation, removeUserMutation, userData, orgSlug]); const resetError = useCallback(() => { removeInviteMutation.reset(); removeUserMutation.reset(); @@ -124,6 +126,9 @@ const RemoveUserDialog = ({ ); }; +type Organization = Api.Query.Output<'/users/listOrgs'>[number]; +type OrgMember = Api.Query.Output<'/users/listOrgMembers'>[number]; + const OrganizationMemberView = ({ organization, member, @@ -131,8 +136,8 @@ const OrganizationMemberView = ({ singleAdmin, }: { organization: Organization; - member: OrganizationMember; - self: OrganizationMember; + member: OrgMember; + self: OrgMember; singleAdmin: boolean; }) => { const [leavingModalOpen, setLeavingModalOpen] = useState(false); @@ -174,7 +179,7 @@ const OrganizationMemberView = ({ - changeRoleMutation.mutate({ uid: member.user.uid!, role: value as OrganizationRole }) + changeRoleMutation.mutate({ user: member.user.uid!, role: value as OrgRole, org: organization.slug }) } > {ROLES.map((role) => ( @@ -207,7 +212,7 @@ const OrganizationMemberView = ({ confirmText="Remove" errorText={(leaveMutation.error as any)?.description} isProcessing={leaveMutation.loading} - onConfirm={leaveMutation.mutate} + onConfirm={() => leaveMutation.mutate({ org: organization.slug, user: self.user.uid! })} setErrorText={leaveMutation.reset} setShow={setLeavingModalOpen} show={leavingModalOpen} @@ -219,7 +224,7 @@ const OrganizationMemberView = ({ ); }; -type InviteForm = { email: string; role: OrganizationRole }; +type InviteForm = { email: string; role: OrgRole }; const InviteFormEmailInput = ({ form }: { form: UseFormReturn }) => { return ( @@ -249,10 +254,7 @@ const InviteFormRoleDropdown = ({ form }: { form: UseFormReturn }) = - form.setValue('role', value as OrganizationRole)} - > + form.setValue('role', value as OrgRole)}> {ROLES.map((role) => ( {ROLE_NAMES[role]} @@ -299,7 +301,7 @@ const InviteUserDialog = ({ @@ -368,7 +370,7 @@ const OrganizationView: NextPageWithLayout = () => { if (!selectedOrganization) { return; } - deleteMutation.mutate({ name: selectedOrganization.name }); + deleteMutation.mutate({ org: selectedOrganization.name }); }, [deleteMutation, selectedOrganization]); return ( diff --git a/frontend/pages/organizations/accept-invite.tsx b/frontend/pages/organizations/accept-invite.tsx index a60d7eeaa..922816f9f 100644 --- a/frontend/pages/organizations/accept-invite.tsx +++ b/frontend/pages/organizations/accept-invite.tsx @@ -6,6 +6,7 @@ import { Flex } from '@/components/lib/Flex'; import { Message } from '@/components/lib/Message'; import { Spinner } from '@/components/lib/Spinner'; import { useOrganizationsLayout } from '@/hooks/layouts'; +import type { ParsedError } from '@/hooks/organizations'; import { useAcceptOrgInvite } from '@/hooks/organizations'; import { useIdentity } from '@/hooks/user'; import { StableId } from '@/utils/stable-ids'; @@ -41,7 +42,7 @@ const AcceptOrgInvite: NextPageWithLayout = () => { return ( {acceptMutation.status === 'error' ? ( - + ) : ( )} diff --git a/frontend/pages/pick-project-template/[templateSlug].tsx b/frontend/pages/pick-project-template/[templateSlug].tsx index 355bcaf89..cfc1f0ec2 100644 --- a/frontend/pages/pick-project-template/[templateSlug].tsx +++ b/frontend/pages/pick-project-template/[templateSlug].tsx @@ -18,7 +18,6 @@ import analytics from '@/utils/analytics'; import { deployContractTemplate } from '@/utils/deploy-contract-template'; import { authenticatedPost } from '@/utils/http'; import { StableId } from '@/utils/stable-ids'; -import type { Project } from '@/utils/types'; import type { NextPageWithLayout } from '@/utils/types'; const ViewProjectTemplate: NextPageWithLayout = () => { @@ -44,7 +43,7 @@ const ViewProjectTemplate: NextPageWithLayout = () => { try { setIsDeploying(true); - const project = await authenticatedPost('/projects/create', { + const project = await authenticatedPost('/projects/create', { name: projectName, }); diff --git a/frontend/pages/project-analytics.tsx b/frontend/pages/project-analytics.tsx index 30ed96418..9d5664b47 100644 --- a/frontend/pages/project-analytics.tsx +++ b/frontend/pages/project-analytics.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import { iframeResizer } from 'iframe-resizer'; import Link from 'next/link'; import { useEffect, useRef } from 'react'; @@ -17,7 +18,7 @@ import { useSelectedProject } from '@/hooks/selected-project'; import { useTheme } from '@/hooks/theme'; import config from '@/utils/config'; import { StableId } from '@/utils/stable-ids'; -import type { Contract, Environment, NextPageWithLayout } from '@/utils/types'; +import type { NextPageWithLayout } from '@/utils/types'; const ProjectAnalytics: NextPageWithLayout = () => { const { environment, project } = useSelectedProject(); @@ -38,7 +39,10 @@ const ProjectAnalytics: NextPageWithLayout = () => { ); }; -function AnalyticsIframe({ environment, contracts }: { environment: Environment; contracts: Contract[] }) { +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; +type Project = Api.Query.Output<'/projects/getContracts'>; + +function AnalyticsIframe({ environment, contracts }: { environment: Environment; contracts: Project }) { const { activeTheme } = useTheme(); const iframeId = 'analytics-iframe'; const initialized = useRef(false); diff --git a/frontend/pages/projects.tsx b/frontend/pages/projects.tsx index fc2ac6aba..f615ec118 100644 --- a/frontend/pages/projects.tsx +++ b/frontend/pages/projects.tsx @@ -1,3 +1,4 @@ +import type { Api } from '@pc/common/types/api'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; @@ -15,7 +16,6 @@ import { useSimpleLogoutLayout } from '@/hooks/layouts'; import { useProjectGroups } from '@/hooks/projects'; import DeleteProjectModal from '@/modules/core/components/modals/DeleteProjectModal'; import { StableId } from '@/utils/stable-ids'; -import type { Project } from '@/utils/types'; import type { NextPageWithLayout } from '@/utils/types'; const Projects: NextPageWithLayout = () => { @@ -96,6 +96,8 @@ const Projects: NextPageWithLayout = () => { ); }; +type Project = Api.Query.Output<'/projects/list'>[number]; + function ProjectRow(props: { project: Project; showDelete: boolean; isTop: boolean; onDelete: () => void }) { const [showModal, setShowModal] = useState(false); diff --git a/frontend/pages/ui.tsx b/frontend/pages/ui.tsx index 5bd5df521..3c8fc75ba 100644 --- a/frontend/pages/ui.tsx +++ b/frontend/pages/ui.tsx @@ -1,3 +1,4 @@ +import type { Net } from '@pc/database/clients/core'; import { useCombobox } from 'downshift'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -56,7 +57,7 @@ import config from '@/utils/config'; import { formValidations } from '@/utils/constants'; import { mergeInputProps } from '@/utils/merge-input-props'; import { StableId } from '@/utils/stable-ids'; -import type { NetOption, NextPageWithLayout } from '@/utils/types'; +import type { NextPageWithLayout } from '@/utils/types'; import { validateMaxNearDecimalLength, validateMaxNearU128, validateMaxYoctoU128 } from '@/utils/validations'; const Block = styled('div', { @@ -219,7 +220,7 @@ const tableRows = [ ]; const WithNetDropdown: FC<{ children: ReactNode }> = ({ children }) => { - const [net, setNet] = useState('MAINNET'); + const [net, setNet] = useState('MAINNET'); return ( <> diff --git a/frontend/stores/settings/types.ts b/frontend/stores/settings/types.ts index 0a9606342..744412990 100644 --- a/frontend/stores/settings/types.ts +++ b/frontend/stores/settings/types.ts @@ -1,5 +1,3 @@ -import type { PersistedStore } from '@/utils/types'; - export interface ProjectSettings { nftContract?: string; selectedEnvironmentSubId?: number; @@ -10,6 +8,10 @@ export interface UserSettings { selectedProjectSlug?: string; } +interface PersistedStore { + hasHydrated?: boolean; +} + export interface SettingsStore extends PersistedStore { currentUser: UserSettings | undefined; hasInitialized: boolean; diff --git a/frontend/utils/config.ts b/frontend/utils/config.ts index b5cb31206..0c8ac2690 100644 --- a/frontend/utils/config.ts +++ b/frontend/utils/config.ts @@ -1,10 +1,9 @@ +import type { Net } from '@pc/database/clients/core'; import type { FirebaseOptions } from 'firebase/app'; -import type { NetOption } from './types'; - -type ExplorerNets = Record; -type RpcNets = Record; -type EapiNets = Record; +type ExplorerNets = Record; +type RpcNets = Record; +type EapiNets = Record; type DeployEnvironment = 'LOCAL' | 'DEVELOPMENT' | 'PRODUCTION'; // * NOTE: This is ugly, but we are limited in how we can diff --git a/frontend/utils/deploy-contract-template.ts b/frontend/utils/deploy-contract-template.ts index 730c5b422..a651ea30d 100644 --- a/frontend/utils/deploy-contract-template.ts +++ b/frontend/utils/deploy-contract-template.ts @@ -1,12 +1,14 @@ +import type { Api } from '@pc/common/types/api'; import { connect, KeyPair, keyStores, transactions } from 'near-api-js'; import config from '@/utils/config'; import { authenticatedPost } from '@/utils/http'; -import type { Contract, Project } from '@/utils/types'; import type { ContractTemplate } from '../hooks/contract-templates'; import { sleep } from './helpers'; +type Project = Api.Mutation.Output<'/projects/create'>; + export async function deployContractTemplate(project: Project, template: ContractTemplate) { const environmentSubId = 1; // Only TESTNET is supported for now const keyStore = new keyStores.BrowserLocalStorageKeyStore(); @@ -51,7 +53,7 @@ export async function deployContractTemplate(project: Project, template: Contrac // Remove key from browser storage, it was removed from the account. await keyStore.removeKey(nearConfig.networkId, accountId); - const contract = await authenticatedPost('/projects/addContract', { + const contract = await authenticatedPost('/projects/addContract', { project: project.slug, environment: environmentSubId, address: accountId, diff --git a/frontend/utils/helpers.ts b/frontend/utils/helpers.ts index d58cf3bad..e91ce6858 100644 --- a/frontend/utils/helpers.ts +++ b/frontend/utils/helpers.ts @@ -1,11 +1,12 @@ +import type { Api } from '@pc/common/types/api'; import type { NextRouter } from 'next/router'; -import type { Environment } from './types'; - export function assertUnreachable(x: never): never { throw new Error(`Unreachable Case: ${x}`); } +type Environment = Api.Query.Output<'/projects/getEnvironments'>[number]; + export function returnContractAddressRegex(environment?: Environment) { // https://docs.near.org/docs/concepts/account#account-id-rules diff --git a/frontend/utils/http.ts b/frontend/utils/http.ts index 4886c6088..646d16046 100644 --- a/frontend/utils/http.ts +++ b/frontend/utils/http.ts @@ -1,12 +1,17 @@ +import type { Api } from '@pc/common/types/api'; import { getAuth, getIdToken } from 'firebase/auth'; import config from '@/utils/config'; -export const unauthenticatedPost = async ( - endpoint: string, - body?: Record, - headers?: HeadersInit, -): Promise => { +export const unauthenticatedPost = async ( + ...[endpoint, body, headers]: [ + K, + K extends Api.Mutation.Key ? Api.Mutation.Input : K extends Api.Query.Key ? Api.Query.Input : never, + HeadersInit?, + ] +): Promise< + K extends Api.Mutation.Key ? Api.Mutation.Output : K extends Api.Query.Key ? Api.Query.Output : never +> => { const res = await fetch(`${config.url.api}${endpoint}`, { method: 'POST', headers: { @@ -41,11 +46,27 @@ async function parseFetchResponse(res: Response): Promise { return resJson; } -export const authenticatedPost = async (endpoint: string, body?: Record): Promise => { +export const authenticatedPost = async ( + ...[endpoint, body]: K extends Api.Mutation.Key + ? Api.Mutation.Input extends void + ? [K] + : [K, Api.Mutation.Input] + : K extends Api.Query.Key + ? Api.Query.Input extends void + ? [K] + : [K, Api.Query.Input] + : never +): Promise< + K extends Api.Mutation.Key ? Api.Mutation.Output : K extends Api.Query.Key ? Api.Query.Output : never +> => { const user = getAuth().currentUser; if (!user) throw new Error('No authenticated user'); const headers = { Authorization: `Bearer ${await getIdToken(user)}`, }; - return unauthenticatedPost(endpoint, body, headers); + return unauthenticatedPost( + endpoint as K, + body as K extends Api.Mutation.Key ? Api.Mutation.Input : K extends Api.Query.Key ? Api.Query.Input : never, + headers, + ); }; diff --git a/frontend/utils/types.ts b/frontend/utils/types.ts index cd0c771e0..26e4272b9 100644 --- a/frontend/utils/types.ts +++ b/frontend/utils/types.ts @@ -1,88 +1,6 @@ -import type JSBI from 'jsbi'; import type { NextPage } from 'next'; import type { ReactElement, ReactNode } from 'react'; export type NextPageWithLayout = NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; -export type NetOption = 'MAINNET' | 'TESTNET'; -export type TutorialOption = 'NFT_MARKET' | 'CROSSWORD'; - -export interface PersistedStore { - hasHydrated?: boolean; -} - -export interface Contract { - slug: string; - address: string; - net: NetOption; -} - -export interface Project { - id: number; - name: string; - slug: string; - tutorial: TutorialOption; - org: { - name?: string; - slug: string; - isPersonal: boolean; - }; -} - -export type Organization = { - slug: string; - name: string; - isPersonal: boolean; -}; - -export type OrganizationRole = 'ADMIN' | 'COLLABORATOR'; - -export type OrganizationMember = { - role: OrganizationRole; - orgSlug: string; - user: { - uid: string | null; - email: string; - }; - isInvite?: boolean; -}; - -export interface Environment { - name: string; - subId: number; - net: NetOption; - project?: Project; -} - -export interface User { - uid: string; - email: string; - name?: string; - photoUrl?: string; -} - -export interface FinalityStatus { - finalBlockHeight: number; - finalBlockTimestampNanosecond: JSBI; -} - -export interface ApiKey { - keySlug: string; - description: string; - key: string; -} - -export interface ViewAccount { - id: string; - jsonrpc: string; - result: { - amount: string; - block_hash: string; - block_height: number; - code_hash: string; - locked: string; - storage_paid_at: number; - storage_usage: number; - }; -} diff --git a/package-lock.json b/package-lock.json index 713b5a194..7e6eab34b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,13 @@ "workspaces": [ "./frontend", "./backend", - "database" + "database", + "common" ], "devDependencies": { "husky": "^8.0.0", "prettier": "^2.7.1", - "turbo": "^1.6.1" + "turbo": "^1.6.3" }, "engines": { "node": "^16.0.0", @@ -32,6 +33,7 @@ "@nestjs/core": "^8.0.0", "@nestjs/passport": "^8.0.1", "@nestjs/platform-express": "^8.0.0", + "@pc/common": "*", "@pc/database": "*", "@prisma/client": "^3.4.2", "@types/json-schema": "^7.0.11", @@ -82,6 +84,11 @@ "typescript": "^4.8.3" } }, + "common": { + "name": "@pc/common", + "version": "1.0.0", + "license": "ISC" + }, "database": { "name": "@pc/database", "version": "1.0.0", @@ -103,6 +110,7 @@ "@near-wallet-selector/my-near-wallet": "^7.0.3", "@near-wallet-selector/near-wallet": "^7.0.3", "@near-wallet-selector/sender": "^7.0.3", + "@pc/common": "*", "@pc/database": "*", "@radix-ui/react-accordion": "^1.0.0", "@radix-ui/react-dialog": "^1.0.0", @@ -4108,36 +4116,6 @@ "@mdx-js/react": "*" } }, - "node_modules/@next/swc-android-arm-eabi": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz", - "integrity": "sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-android-arm64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz", - "integrity": "sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-darwin-arm64": { "version": "12.3.1", "cpu": [ @@ -4152,156 +4130,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz", - "integrity": "sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-freebsd-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz", - "integrity": "sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz", - "integrity": "sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz", - "integrity": "sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz", - "integrity": "sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz", - "integrity": "sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz", - "integrity": "sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", - "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", - "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", - "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -4357,6 +4185,10 @@ "node": ">=10.13.0" } }, + "node_modules/@pc/common": { + "resolved": "common", + "link": true + }, "node_modules/@pc/database": { "resolved": "database", "link": true @@ -14543,9 +14375,8 @@ }, "node_modules/lint-staged": { "version": "13.0.3", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", - "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.17", @@ -14573,9 +14404,8 @@ }, "node_modules/lint-staged/node_modules/execa": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", - "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", @@ -14596,18 +14426,16 @@ }, "node_modules/lint-staged/node_modules/human-signals": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", - "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.20.0" } }, "node_modules/lint-staged/node_modules/is-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -14617,9 +14445,8 @@ }, "node_modules/lint-staged/node_modules/mimic-fn": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -14629,9 +14456,8 @@ }, "node_modules/lint-staged/node_modules/npm-run-path": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -14644,9 +14470,8 @@ }, "node_modules/lint-staged/node_modules/onetime": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -14659,9 +14484,8 @@ }, "node_modules/lint-staged/node_modules/path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -14671,9 +14495,8 @@ }, "node_modules/lint-staged/node_modules/strip-final-newline": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -14683,9 +14506,8 @@ }, "node_modules/lint-staged/node_modules/yaml": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 14" } @@ -17566,9 +17388,8 @@ }, "node_modules/pidtree": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, + "license": "MIT", "bin": { "pidtree": "bin/pidtree.js" }, @@ -20548,27 +20369,27 @@ } }, "node_modules/turbo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.6.1.tgz", - "integrity": "sha512-CkcJo17cbwfTzmxtxJo2AbbeVqaz1yQotBUqVwZDdcrVSNKci2nvw+JHJ3sy/z9YY9xOJmoRaZifbkja3UXUWA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.6.3.tgz", + "integrity": "sha512-FtfhJLmEEtHveGxW4Ye/QuY85AnZ2ZNVgkTBswoap7UMHB1+oI4diHPNyqrQLG4K1UFtCkjOlVoLsllUh/9QRw==", "dev": true, "hasInstallScript": true, "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "1.6.1", - "turbo-darwin-arm64": "1.6.1", - "turbo-linux-64": "1.6.1", - "turbo-linux-arm64": "1.6.1", - "turbo-windows-64": "1.6.1", - "turbo-windows-arm64": "1.6.1" + "turbo-darwin-64": "1.6.3", + "turbo-darwin-arm64": "1.6.3", + "turbo-linux-64": "1.6.3", + "turbo-linux-arm64": "1.6.3", + "turbo-windows-64": "1.6.3", + "turbo-windows-arm64": "1.6.3" } }, "node_modules/turbo-darwin-64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.6.1.tgz", - "integrity": "sha512-xsItJ/hmnd6R8V60cCe0RAZQjO+En/LVXVkZhiw0Fyfxoo+iKcAA4sVeWkaL+cg5sQd5UWlWfD1EOKbHDjVb9Q==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.6.3.tgz", + "integrity": "sha512-QmDIX0Yh1wYQl0bUS0gGWwNxpJwrzZU2GIAYt3aOKoirWA2ecnyb3R6ludcS1znfNV2MfunP+l8E3ncxUHwtjA==", "cpu": [ "x64" ], @@ -20579,9 +20400,9 @@ ] }, "node_modules/turbo-darwin-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.1.tgz", - "integrity": "sha512-wRfAJWCLYB29IGTx6sF6QvexK/89AbAgnfYA5yVcuUJT+xz2/zLeGcOODQBCnP4rB+vX5ipXLY0XjkLGl+z6fA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.3.tgz", + "integrity": "sha512-75DXhFpwE7CinBbtxTxH08EcWrxYSPFow3NaeFwsG8aymkWXF+U2aukYHJA6I12n9/dGqf7yRXzkF0S/9UtdyQ==", "cpu": [ "arm64" ], @@ -20592,9 +20413,9 @@ ] }, "node_modules/turbo-linux-64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.6.1.tgz", - "integrity": "sha512-NZ88muC3hHbWW/cBgl9DFFbyzDcFVvZHQBXKTwVA8l2yLOOvesX+aQ2Knr4Pxu9Kb0F3t6ABsOSf8SbI7CpJsg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.6.3.tgz", + "integrity": "sha512-O9uc6J0yoRPWdPg9THRQi69K6E2iZ98cRHNvus05lZbcPzZTxJYkYGb5iagCmCW/pq6fL4T4oLWAd6evg2LGQA==", "cpu": [ "x64" ], @@ -20605,9 +20426,9 @@ ] }, "node_modules/turbo-linux-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.6.1.tgz", - "integrity": "sha512-HDgx+0ozqMpoDBOSzWz43nYMDp/+giEz8+vmLOB6mTQU/9IlZQVwachzwkqLRsJyBUhYALBlWGcuRWO3KqXMmg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.6.3.tgz", + "integrity": "sha512-dCy667qqEtZIhulsRTe8hhWQNCJO0i20uHXv7KjLHuFZGCeMbWxB8rsneRoY+blf8+QNqGuXQJxak7ayjHLxiA==", "cpu": [ "arm64" ], @@ -20618,9 +20439,9 @@ ] }, "node_modules/turbo-windows-64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.6.1.tgz", - "integrity": "sha512-jnR0V0YBlFJKEoAeq0GQFLmZ1UNl6vh+RHTHX546+o5jKcE6nfp9oTOEwtR0PLutiuxxDDm6roAc+9mSfycffw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.6.3.tgz", + "integrity": "sha512-lKRqwL3mrVF09b9KySSaOwetehmGknV9EcQTF7d2dxngGYYX1WXoQLjFP9YYH8ZV07oPm+RUOAKSCQuDuMNhiA==", "cpu": [ "x64" ], @@ -20631,9 +20452,9 @@ ] }, "node_modules/turbo-windows-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.6.1.tgz", - "integrity": "sha512-vOqw/iPgLjkwpni2vNFK9YO19lN9QZ8JG8v1unvL09/rnXyKpHygrYECj+efJptEVJKBG2xLIauJYmZ/2LV1Uw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.6.3.tgz", + "integrity": "sha512-BXY1sDPEA1DgPwuENvDCD8B7Hb0toscjus941WpL8CVd10hg9pk/MWn9CNgwDO5Q9ks0mw+liDv2EMnleEjeNA==", "cpu": [ "arm64" ], @@ -24448,100 +24269,10 @@ "dev": true, "requires": {} }, - "@next/swc-android-arm-eabi": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz", - "integrity": "sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==", - "optional": true - }, - "@next/swc-android-arm64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz", - "integrity": "sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==", - "optional": true - }, "@next/swc-darwin-arm64": { "version": "12.3.1", "optional": true }, - "@next/swc-darwin-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz", - "integrity": "sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==", - "optional": true - }, - "@next/swc-freebsd-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz", - "integrity": "sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==", - "optional": true - }, - "@next/swc-linux-arm-gnueabihf": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz", - "integrity": "sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==", - "optional": true - }, - "@next/swc-linux-arm64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz", - "integrity": "sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==", - "optional": true - }, - "@next/swc-linux-arm64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz", - "integrity": "sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==", - "optional": true - }, - "@next/swc-linux-x64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz", - "integrity": "sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==", - "optional": true - }, - "@next/swc-linux-x64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz", - "integrity": "sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==", - "optional": true - }, - "@next/swc-win32-arm64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", - "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", - "optional": true - }, - "@next/swc-win32-ia32-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", - "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", - "optional": true - }, - "@next/swc-win32-x64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", - "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", - "optional": true - }, - "@next/swc-win32-arm64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", - "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", - "optional": true - }, - "@next/swc-win32-ia32-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", - "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", - "optional": true - }, - "@next/swc-win32-x64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", - "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", - "optional": true - }, "@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -24573,10 +24304,12 @@ "@panva/asn1.js": { "version": "1.0.0" }, + "@pc/common": { + "version": "file:common" + }, "@pc/database": { "version": "file:database", "requires": { - "@prisma/client": "^4.4.0", "lint-staged": "^13.0.3", "prisma": "3.15.2", "ts-node": "^10.9.1", @@ -27282,6 +27015,7 @@ "@nestjs/platform-express": "^8.0.0", "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", + "@pc/common": "*", "@pc/database": "*", "@prisma/client": "^3.4.2", "@types/express": "^4.17.13", @@ -29322,6 +29056,7 @@ "@near-wallet-selector/near-wallet": "^7.0.3", "@near-wallet-selector/sender": "^7.0.3", "@next/mdx": "^12.0.10", + "@pc/common": "*", "@pc/database": "*", "@playwright/test": "^1.20.0", "@radix-ui/react-accordion": "^1.0.0", @@ -31374,8 +31109,6 @@ }, "lint-staged": { "version": "13.0.3", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", - "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", "dev": true, "requires": { "cli-truncate": "^3.1.0", @@ -31395,8 +31128,6 @@ "dependencies": { "execa": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", - "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", "dev": true, "requires": { "cross-spawn": "^7.0.3", @@ -31412,26 +31143,18 @@ }, "human-signals": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", - "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", "dev": true }, "is-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true }, "mimic-fn": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true }, "npm-run-path": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", "dev": true, "requires": { "path-key": "^4.0.0" @@ -31439,8 +31162,6 @@ }, "onetime": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, "requires": { "mimic-fn": "^4.0.0" @@ -31448,20 +31169,14 @@ }, "path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true }, "strip-final-newline": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true }, "yaml": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", "dev": true } } @@ -33165,8 +32880,6 @@ }, "pidtree": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true }, "pinkie": { @@ -34973,58 +34686,58 @@ } }, "turbo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.6.1.tgz", - "integrity": "sha512-CkcJo17cbwfTzmxtxJo2AbbeVqaz1yQotBUqVwZDdcrVSNKci2nvw+JHJ3sy/z9YY9xOJmoRaZifbkja3UXUWA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.6.3.tgz", + "integrity": "sha512-FtfhJLmEEtHveGxW4Ye/QuY85AnZ2ZNVgkTBswoap7UMHB1+oI4diHPNyqrQLG4K1UFtCkjOlVoLsllUh/9QRw==", "dev": true, "requires": { - "turbo-darwin-64": "1.6.1", - "turbo-darwin-arm64": "1.6.1", - "turbo-linux-64": "1.6.1", - "turbo-linux-arm64": "1.6.1", - "turbo-windows-64": "1.6.1", - "turbo-windows-arm64": "1.6.1" + "turbo-darwin-64": "1.6.3", + "turbo-darwin-arm64": "1.6.3", + "turbo-linux-64": "1.6.3", + "turbo-linux-arm64": "1.6.3", + "turbo-windows-64": "1.6.3", + "turbo-windows-arm64": "1.6.3" } }, "turbo-darwin-64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.6.1.tgz", - "integrity": "sha512-xsItJ/hmnd6R8V60cCe0RAZQjO+En/LVXVkZhiw0Fyfxoo+iKcAA4sVeWkaL+cg5sQd5UWlWfD1EOKbHDjVb9Q==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.6.3.tgz", + "integrity": "sha512-QmDIX0Yh1wYQl0bUS0gGWwNxpJwrzZU2GIAYt3aOKoirWA2ecnyb3R6ludcS1znfNV2MfunP+l8E3ncxUHwtjA==", "dev": true, "optional": true }, "turbo-darwin-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.1.tgz", - "integrity": "sha512-wRfAJWCLYB29IGTx6sF6QvexK/89AbAgnfYA5yVcuUJT+xz2/zLeGcOODQBCnP4rB+vX5ipXLY0XjkLGl+z6fA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.3.tgz", + "integrity": "sha512-75DXhFpwE7CinBbtxTxH08EcWrxYSPFow3NaeFwsG8aymkWXF+U2aukYHJA6I12n9/dGqf7yRXzkF0S/9UtdyQ==", "dev": true, "optional": true }, "turbo-linux-64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.6.1.tgz", - "integrity": "sha512-NZ88muC3hHbWW/cBgl9DFFbyzDcFVvZHQBXKTwVA8l2yLOOvesX+aQ2Knr4Pxu9Kb0F3t6ABsOSf8SbI7CpJsg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.6.3.tgz", + "integrity": "sha512-O9uc6J0yoRPWdPg9THRQi69K6E2iZ98cRHNvus05lZbcPzZTxJYkYGb5iagCmCW/pq6fL4T4oLWAd6evg2LGQA==", "dev": true, "optional": true }, "turbo-linux-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.6.1.tgz", - "integrity": "sha512-HDgx+0ozqMpoDBOSzWz43nYMDp/+giEz8+vmLOB6mTQU/9IlZQVwachzwkqLRsJyBUhYALBlWGcuRWO3KqXMmg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.6.3.tgz", + "integrity": "sha512-dCy667qqEtZIhulsRTe8hhWQNCJO0i20uHXv7KjLHuFZGCeMbWxB8rsneRoY+blf8+QNqGuXQJxak7ayjHLxiA==", "dev": true, "optional": true }, "turbo-windows-64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.6.1.tgz", - "integrity": "sha512-jnR0V0YBlFJKEoAeq0GQFLmZ1UNl6vh+RHTHX546+o5jKcE6nfp9oTOEwtR0PLutiuxxDDm6roAc+9mSfycffw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.6.3.tgz", + "integrity": "sha512-lKRqwL3mrVF09b9KySSaOwetehmGknV9EcQTF7d2dxngGYYX1WXoQLjFP9YYH8ZV07oPm+RUOAKSCQuDuMNhiA==", "dev": true, "optional": true }, "turbo-windows-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.6.1.tgz", - "integrity": "sha512-vOqw/iPgLjkwpni2vNFK9YO19lN9QZ8JG8v1unvL09/rnXyKpHygrYECj+efJptEVJKBG2xLIauJYmZ/2LV1Uw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.6.3.tgz", + "integrity": "sha512-BXY1sDPEA1DgPwuENvDCD8B7Hb0toscjus941WpL8CVd10hg9pk/MWn9CNgwDO5Q9ks0mw+liDv2EMnleEjeNA==", "dev": true, "optional": true }, diff --git a/package.json b/package.json index 659a8e042..ed6b19e75 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "workspaces": [ "./frontend", "./backend", - "database" + "database", + "common" ], "scripts": { "prepare": "husky install", @@ -30,6 +31,6 @@ "devDependencies": { "husky": "^8.0.0", "prettier": "^2.7.1", - "turbo": "^1.6.1" + "turbo": "^1.6.3" } } diff --git a/tsconfig.json b/tsconfig.json index 2d12c5128..3e43718b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,6 @@ "moduleResolution": "node", "declaration": true, "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "es2020", "sourceMap": true,