From ac6755701e8d9598cf0c903b4e3f3008ef6a739a Mon Sep 17 00:00:00 2001 From: Bart Date: Sun, 25 Jun 2023 19:09:36 +0200 Subject: [PATCH] Stricter type checks (#169) * update typescript config to be stricter * initial approach to initializing entities in type-safe way * fix entity initialization * fix backend type issues * fix entity initialization issue * fix new emulator account handling * update comment * explicit stricter config in frontend --- backend/package.json | 1 + .../controllers/accounts.controller.ts | 10 +- .../controllers/contracts.controller.ts | 7 +- .../src/accounts/entities/account.entity.ts | 64 +++++-- .../src/accounts/entities/contract.entity.ts | 28 +++- backend/src/accounts/entities/key.entity.ts | 57 +++++-- .../accounts/entities/storage-item.entity.ts | 30 +++- .../accounts/services/contracts.service.ts | 5 +- backend/src/accounts/services/keys.service.ts | 5 +- .../src/accounts/services/storage.service.ts | 2 +- backend/src/blocks/blocks.controller.ts | 2 +- .../blocks/entities/block-context.entity.ts | 5 +- backend/src/blocks/entities/block.entity.ts | 18 +- backend/src/core/async-interval-scheduler.ts | 2 +- .../src/data-processing/processor.service.ts | 158 ++++++++++-------- backend/src/events/event.entity.ts | 24 ++- backend/src/events/events.controller.ts | 6 +- backend/src/events/events.service.ts | 4 +- backend/src/flow/entities/snapshot.entity.ts | 13 ++ backend/src/flow/flow.controller.ts | 8 +- backend/src/flow/services/cli.service.ts | 8 +- backend/src/flow/services/config.service.ts | 33 ++-- .../src/flow/services/dev-wallet.service.ts | 8 +- backend/src/flow/services/emulator.service.ts | 31 +++- backend/src/flow/services/gateway.service.ts | 2 +- backend/src/flow/services/snapshot.service.ts | 15 +- backend/src/flow/services/storage.service.ts | 40 ++--- backend/src/flow/tests/cli.service.spec.ts | 11 +- backend/src/flow/utils/cadence-utils.ts | 9 +- backend/src/flow/utils/project-context.ts | 14 +- .../src/processes/managed-process.entity.ts | 21 ++- .../processes/process-manager.controller.ts | 16 +- .../src/processes/process-manager.service.ts | 34 +++- .../src/projects/dto/create-project.dto.ts | 5 + .../dto/dev-wallet-configuration.dto.ts | 1 + .../dto/emulator-configuration.dto.ts | 24 +++ .../projects/dto/gateway-configuration.dto.ts | 3 + .../src/projects/dto/update-project.dto.ts | 1 + backend/src/projects/project.entity.ts | 69 +++++--- backend/src/projects/projects.controller.ts | 2 +- backend/src/projects/projects.service.ts | 2 +- .../src/transactions/transaction.entity.ts | 37 +++- .../transactions/transactions.controller.ts | 11 +- .../common-utils.spec.ts} | 2 +- .../src/{utils.ts => utils/common-utils.ts} | 8 +- backend/src/utils/type-utils.ts | 13 ++ backend/src/wallet/wallet.controller.ts | 2 +- backend/src/wallet/wallet.service.ts | 23 ++- backend/tsconfig.json | 8 +- frontend/tsconfig.json | 2 + shared/proto/entities/projects.proto | 2 +- yarn.lock | 14 ++ 52 files changed, 632 insertions(+), 288 deletions(-) rename backend/src/{utils.spec.ts => utils/common-utils.spec.ts} (95%) rename backend/src/{utils.ts => utils/common-utils.ts} (95%) create mode 100644 backend/src/utils/type-utils.ts diff --git a/backend/package.json b/backend/package.json index 33c2bcbf..e365d2e0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -58,6 +58,7 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/cron": "^1.7.3", + "@types/elliptic": "^6.4.14", "@types/express": "^4.17.13", "@types/jest": "^27.0.2", "@types/node": "^16.11.6", diff --git a/backend/src/accounts/controllers/accounts.controller.ts b/backend/src/accounts/controllers/accounts.controller.ts index 53eada14..dc5734b7 100644 --- a/backend/src/accounts/controllers/accounts.controller.ts +++ b/backend/src/accounts/controllers/accounts.controller.ts @@ -41,7 +41,7 @@ export class AccountsController { @ApiQuery({ name: "timestamp", type: Number }) @Post("/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingAccountsResponse)) - async findAllNew(@Body() data) { + async findAllNew(@Body() data: unknown) { const request = GetPollingAccountsRequest.fromJSON(data); const accounts = await this.accountsService.findAllNewerThanTimestamp( new Date(request.timestamp) @@ -53,8 +53,8 @@ export class AccountsController { @Post(":address/keys/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingKeysResponse)) async findAllNewKeysByAccount( - @Param("address") accountAddress, - @Body() data + @Param("address") accountAddress: string, + @Body() data: unknown ) { const request = GetPollingKeysRequest.fromJSON(data); const keys = await this.keysService.findAllNewerThanTimestampByAccount( @@ -68,8 +68,8 @@ export class AccountsController { @Post(":address/storage/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingStorageResponse)) async findAllNewStorageByAccount( - @Param("address") accountAddress, - @Body() data + @Param("address") accountAddress: string, + @Body() data: unknown ) { const request = GetPollingStorageRequest.fromJSON(data); const storageItems = diff --git a/backend/src/accounts/controllers/contracts.controller.ts b/backend/src/accounts/controllers/contracts.controller.ts index 16f5bbf4..f09a0690 100644 --- a/backend/src/accounts/controllers/contracts.controller.ts +++ b/backend/src/accounts/controllers/contracts.controller.ts @@ -31,7 +31,7 @@ export class ContractsController { @Post("contracts/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingContractsResponse)) - async findAllNew(@Body() data) { + async findAllNew(@Body() data: unknown) { const request = GetPollingContractsRequest.fromJSON(data); const contracts = await this.contractsService.findAllNewerThanTimestamp( new Date(request.timestamp) @@ -42,7 +42,10 @@ export class ContractsController { @ApiParam({ name: "id", type: String }) @Post("/accounts/:address/contracts/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingContractsResponse)) - async findAllNewByAccount(@Param("address") accountAddress, @Body() data) { + async findAllNewByAccount( + @Param("address") accountAddress: string, + @Body() data: unknown + ) { const request = GetPollingContractsByAccountRequest.fromJSON(data); const contracts = await this.contractsService.findAllNewerThanTimestampByAccount( diff --git a/backend/src/accounts/entities/account.entity.ts b/backend/src/accounts/entities/account.entity.ts index 75f0f1a7..a0169000 100644 --- a/backend/src/accounts/entities/account.entity.ts +++ b/backend/src/accounts/entities/account.entity.ts @@ -6,6 +6,9 @@ import { Account } from "@flowser/shared"; import { TransactionEntity } from "../../transactions/transaction.entity"; import { AccountStorageItemEntity } from "./storage-item.entity"; import { BlockContextEntity } from "../../blocks/entities/block-context.entity"; +import { PollingEntityInitArguments } from "../../utils/type-utils"; + +type AccountEntityInitArgs = PollingEntityInitArguments; @Entity({ name: "accounts" }) export class AccountEntity extends PollingEntity implements BlockContextEntity { @@ -30,36 +33,62 @@ export class AccountEntity extends PollingEntity implements BlockContextEntity { @OneToMany(() => AccountKeyEntity, (key) => key.account, { eager: true, }) - keys: AccountKeyEntity[]; + keys?: AccountKeyEntity[]; @OneToMany(() => AccountStorageItemEntity, (storage) => storage.account, { eager: true, }) - storage: AccountStorageItemEntity[]; + storage?: AccountStorageItemEntity[]; @OneToMany(() => AccountContractEntity, (contract) => contract.account, { eager: true, }) - contracts: AccountContractEntity[]; + contracts?: AccountContractEntity[]; @OneToMany(() => TransactionEntity, (key) => key.payer, { eager: true, }) - transactions: TransactionEntity[]; + transactions?: TransactionEntity[]; + + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: AccountEntityInitArgs | undefined) { + super(); + this.address = args?.address ?? ""; + this.blockId = args?.blockId ?? ""; + this.balance = args?.balance ?? 0; + this.code = args?.code ?? ""; + this.isDefaultAccount = args?.isDefaultAccount ?? false; + if (args?.keys) { + this.keys = args.keys; + } + if (args?.storage) { + this.storage = args.storage; + } + if (args?.contracts) { + this.contracts = args.contracts; + } + if (args?.transactions) { + this.transactions = args.transactions; + } + } /** * Creates an account with default values (where applicable). * It doesn't pre-set the values that should be provided. */ static createDefault(): AccountEntity { - const account = new AccountEntity(); - account.balance = 0; - account.code = ""; - account.keys = []; - account.transactions = []; - account.contracts = []; - account.storage = []; - return account; + return new AccountEntity({ + balance: 0, + address: "", + blockId: "", + isDefaultAccount: false, + code: "", + keys: [], + transactions: [], + contracts: [], + storage: [], + }); } toProto(): Account { @@ -67,12 +96,11 @@ export class AccountEntity extends PollingEntity implements BlockContextEntity { address: this.address, balance: this.balance, code: this.code, - storage: this.storage.map((storage) => storage.toProto()), - keys: this.keys.map((key) => key.toProto()), - contracts: this.contracts.map((contract) => contract.toProto()), - transactions: this.transactions.map((transaction) => - transaction.toProto() - ), + storage: this.storage?.map((storage) => storage.toProto()) ?? [], + keys: this.keys?.map((key) => key.toProto()) ?? [], + contracts: this.contracts?.map((contract) => contract.toProto()) ?? [], + transactions: + this.transactions?.map((transaction) => transaction.toProto()) ?? [], isDefaultAccount: this.isDefaultAccount, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString(), diff --git a/backend/src/accounts/entities/contract.entity.ts b/backend/src/accounts/entities/contract.entity.ts index f7411866..cfb904e1 100644 --- a/backend/src/accounts/entities/contract.entity.ts +++ b/backend/src/accounts/entities/contract.entity.ts @@ -1,14 +1,15 @@ import { PollingEntity } from "../../core/entities/polling.entity"; -import { - Column, - Entity, - ManyToOne, - PrimaryColumn, -} from "typeorm"; +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; import { AccountEntity } from "./account.entity"; import { BadRequestException } from "@nestjs/common"; import { AccountContract } from "@flowser/shared"; import { BlockContextEntity } from "../../blocks/entities/block-context.entity"; +import { PollingEntityInitArguments } from "../../utils/type-utils"; + +type AccountContractEntityInitArgs = Omit< + PollingEntityInitArguments, + "id" +>; @Entity({ name: "contracts" }) export class AccountContractEntity @@ -29,7 +30,18 @@ export class AccountContractEntity code: string; @ManyToOne(() => AccountEntity, (account) => account.contracts) - account: AccountEntity; + account: AccountEntity | null; + + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: AccountContractEntityInitArgs | undefined) { + super(); + this.accountAddress = args?.accountAddress ?? ""; + this.name = args?.name ?? ""; + this.blockId = args?.blockId ?? ""; + this.code = args?.code ?? ""; + this.account = args?.account ?? null; + } toProto(): AccountContract { return { @@ -43,7 +55,7 @@ export class AccountContractEntity } get id() { - return `${this.accountAddress}.${this.name}` + return `${this.accountAddress}.${this.name}`; } public static decodeId(id: string) { diff --git a/backend/src/accounts/entities/key.entity.ts b/backend/src/accounts/entities/key.entity.ts index dd66c44e..badb4da8 100644 --- a/backend/src/accounts/entities/key.entity.ts +++ b/backend/src/accounts/entities/key.entity.ts @@ -4,6 +4,9 @@ import { AccountEntity } from "./account.entity"; import { AccountKey } from "@flowser/shared"; import { HashAlgorithm, SignatureAlgorithm } from "@flowser/shared"; import { BlockContextEntity } from "../../blocks/entities/block-context.entity"; +import { PollingEntityInitArguments } from "../../utils/type-utils"; + +type AccountKeyEntityInitArgs = PollingEntityInitArguments; // https://developers.flow.com/tooling/flow-cli/accounts/create-accounts#key-weight export const defaultKeyWeight = 1000; @@ -21,13 +24,13 @@ export class AccountKeyEntity // Nullable for backward compatability - to not cause not null constraint failure on migration. @Column({ nullable: true }) - blockId: string; + blockId: string = "NULL"; @Column() publicKey: string; @Column({ nullable: true }) - privateKey: string | null; + privateKey: string; @Column() signAlgo: SignatureAlgorithm; @@ -45,24 +48,50 @@ export class AccountKeyEntity revoked: boolean; @ManyToOne(() => AccountEntity, (account) => account.storage) - account: AccountEntity; + account?: AccountEntity; + + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: AccountKeyEntityInitArgs | undefined) { + super(); + this.index = args?.index ?? -1; + this.accountAddress = args?.accountAddress ?? ""; + this.blockId = args?.blockId ?? ""; + this.publicKey = args?.publicKey ?? ""; + this.privateKey = args?.privateKey ?? ""; + this.signAlgo = + args?.signAlgo ?? SignatureAlgorithm.SIGNATURE_ALGORITHM_UNSPECIFIED; + this.hashAlgo = args?.hashAlgo ?? HashAlgorithm.HASH_ALGORITHM_UNSPECIFIED; + this.weight = args?.weight ?? -1; + this.sequenceNumber = args?.sequenceNumber ?? -1; + this.revoked = args?.revoked ?? false; + if (args?.account) { + this.account = args.account; + } + } /** * Creates a key with default values (where applicable). * It doesn't pre-set the values that should be provided. */ static createDefault(): AccountKeyEntity { - const key = new AccountKeyEntity(); - // https://developers.flow.com/tooling/flow-cli/accounts/create-accounts#public-key-signature-algorithm - key.signAlgo = SignatureAlgorithm.ECDSA_P256; - // Which has algorithm is actually used here by default? - // Flow CLI doesn't support the option to specify it as an argument, - // nor does it return this info when generating the key. - key.hashAlgo = HashAlgorithm.SHA3_256; - key.weight = defaultKeyWeight; - key.sequenceNumber = 0; - key.revoked = false; - return key; + return new AccountKeyEntity({ + // https://developers.flow.com/tooling/flow-cli/accounts/create-accounts#public-key-signature-algorithm + signAlgo: SignatureAlgorithm.ECDSA_P256, + // Which has algorithm is actually used here by default? + // Flow CLI doesn't support the option to specify it as an argument, + // nor does it return this info when generating the key. + hashAlgo: HashAlgorithm.SHA3_256, + weight: defaultKeyWeight, + sequenceNumber: 0, + revoked: false, + account: undefined, + accountAddress: "", + blockId: "", + index: 0, + privateKey: "", + publicKey: "", + }); } toProto(): AccountKey { diff --git a/backend/src/accounts/entities/storage-item.entity.ts b/backend/src/accounts/entities/storage-item.entity.ts index 98ca5ba5..4e6328c7 100644 --- a/backend/src/accounts/entities/storage-item.entity.ts +++ b/backend/src/accounts/entities/storage-item.entity.ts @@ -2,12 +2,12 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; import { AccountEntity } from "./account.entity"; import { AccountStorageDomain, AccountStorageItem } from "@flowser/shared"; import { PollingEntity } from "../../core/entities/polling.entity"; -import { - FlowAccountStorage, - FlowAccountStorageDomain, - FlowStorageIdentifier, -} from "../../flow/services/storage.service"; -import { ensurePrefixedAddress } from "../../utils"; +import { PollingEntityInitArguments } from "../../utils/type-utils"; + +type AccountStorageItemEntityInitArgs = Omit< + PollingEntityInitArguments, + "_id" | "id" +>; @Entity({ name: "storage" }) export class AccountStorageItemEntity extends PollingEntity { @@ -26,10 +26,24 @@ export class AccountStorageItemEntity extends PollingEntity { accountAddress: string; @Column("simple-json") - data: unknown; + data: any; @ManyToOne(() => AccountEntity, (account) => account.storage) - account: AccountEntity; + account?: AccountEntity; + + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: AccountStorageItemEntityInitArgs | undefined) { + super(); + this.pathIdentifier = args?.pathIdentifier ?? ""; + this.pathDomain = + args?.pathDomain ?? AccountStorageDomain.STORAGE_DOMAIN_UNKNOWN; + this.accountAddress = args?.accountAddress ?? ""; + this.data = args?.data ?? {}; + if (args?.account) { + this.account = args.account; + } + } get id() { return `${this.accountAddress}/${this.getLowerCasedPathDomain()}/${ diff --git a/backend/src/accounts/services/contracts.service.ts b/backend/src/accounts/services/contracts.service.ts index 72e7b348..1fd24bc5 100644 --- a/backend/src/accounts/services/contracts.service.ts +++ b/backend/src/accounts/services/contracts.service.ts @@ -2,7 +2,10 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { MoreThan, Repository } from "typeorm"; import { AccountContractEntity } from "../entities/contract.entity"; -import { computeEntitiesDiff, processEntitiesDiff } from "../../utils"; +import { + computeEntitiesDiff, + processEntitiesDiff, +} from "../../utils/common-utils"; import { removeByBlockIds } from "../../blocks/entities/block-context.entity"; @Injectable() diff --git a/backend/src/accounts/services/keys.service.ts b/backend/src/accounts/services/keys.service.ts index 6bb323b9..c089dc11 100644 --- a/backend/src/accounts/services/keys.service.ts +++ b/backend/src/accounts/services/keys.service.ts @@ -2,7 +2,10 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { AccountKeyEntity } from "../entities/key.entity"; import { MoreThan, Repository } from "typeorm"; -import { computeEntitiesDiff, processEntitiesDiff } from "../../utils"; +import { + computeEntitiesDiff, + processEntitiesDiff, +} from "../../utils/common-utils"; import { removeByBlockIds } from "../../blocks/entities/block-context.entity"; @Injectable() diff --git a/backend/src/accounts/services/storage.service.ts b/backend/src/accounts/services/storage.service.ts index 27e6471b..2984995b 100644 --- a/backend/src/accounts/services/storage.service.ts +++ b/backend/src/accounts/services/storage.service.ts @@ -6,7 +6,7 @@ import { computeEntitiesDiff, ensurePrefixedAddress, processEntitiesDiff, -} from "../../utils"; +} from "../../utils/common-utils"; @Injectable() export class AccountStorageService { diff --git a/backend/src/blocks/blocks.controller.ts b/backend/src/blocks/blocks.controller.ts index 57798d0b..2e4ec121 100644 --- a/backend/src/blocks/blocks.controller.ts +++ b/backend/src/blocks/blocks.controller.ts @@ -30,7 +30,7 @@ export class BlocksController { @Post("/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingBlocksResponse)) - async findAllNew(@Body() data) { + async findAllNew(@Body() data: unknown) { const request = GetPollingBlocksRequest.fromJSON(data); const blocks = await this.blocksService.findAllNewerThanTimestamp( new Date(request.timestamp) diff --git a/backend/src/blocks/entities/block-context.entity.ts b/backend/src/blocks/entities/block-context.entity.ts index f1d3b8d9..020c8e34 100644 --- a/backend/src/blocks/entities/block-context.entity.ts +++ b/backend/src/blocks/entities/block-context.entity.ts @@ -21,8 +21,5 @@ export async function removeByBlockIds(options: { export function implementsBlockContext( object: unknown ): object is BlockContextEntity { - if (typeof object !== "object") { - return false; - } - return "blockId" in object; + return typeof object === "object" && object !== null && "blockId" in object; } diff --git a/backend/src/blocks/entities/block.entity.ts b/backend/src/blocks/entities/block.entity.ts index 2e00203e..c6fe9ebd 100644 --- a/backend/src/blocks/entities/block.entity.ts +++ b/backend/src/blocks/entities/block.entity.ts @@ -1,8 +1,11 @@ import { Column, Entity, PrimaryColumn } from "typeorm"; import { PollingEntity } from "../../core/entities/polling.entity"; import { Block, CollectionGuarantee } from "@flowser/shared"; -import { typeOrmProtobufTransformer } from "../../utils"; +import { typeOrmProtobufTransformer } from "../../utils/common-utils"; import { BlockContextEntity } from "./block-context.entity"; +import { PollingEntityInitArguments } from "../../utils/type-utils"; + +type BlockEntityInitArgs = PollingEntityInitArguments; @Entity({ name: "blocks" }) export class BlockEntity extends PollingEntity implements BlockContextEntity { @@ -31,6 +34,19 @@ export class BlockEntity extends PollingEntity implements BlockContextEntity { @Column("simple-array") signatures: string[]; + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: BlockEntityInitArgs | undefined) { + super(); + this.blockId = args?.blockId ?? ""; + this.parentId = args?.parentId ?? ""; + this.blockHeight = args?.blockHeight ?? -1; + this.timestamp = args?.timestamp ?? new Date(); + this.collectionGuarantees = args?.collectionGuarantees ?? []; + this.blockSeals = args?.blockSeals ?? []; + this.signatures = args?.signatures ?? []; + } + toProto(): Block { return { id: this.blockId, diff --git a/backend/src/core/async-interval-scheduler.ts b/backend/src/core/async-interval-scheduler.ts index a2f5d46e..2a751716 100644 --- a/backend/src/core/async-interval-scheduler.ts +++ b/backend/src/core/async-interval-scheduler.ts @@ -16,7 +16,7 @@ export type AsyncIntervalOptions = { */ export class AsyncIntervalScheduler { private isRunning: boolean; - private runningTimeoutId: NodeJS.Timeout; + private runningTimeoutId: NodeJS.Timeout | undefined; private readonly options: AsyncIntervalOptions; constructor(options: AsyncIntervalOptions) { diff --git a/backend/src/data-processing/processor.service.ts b/backend/src/data-processing/processor.service.ts index f546a01f..ef2bcf73 100644 --- a/backend/src/data-processing/processor.service.ts +++ b/backend/src/data-processing/processor.service.ts @@ -22,7 +22,10 @@ import { BlockEntity } from "../blocks/entities/block.entity"; import { AccountContractEntity } from "../accounts/entities/contract.entity"; import { KeysService } from "../accounts/services/keys.service"; import { AccountKeyEntity } from "../accounts/entities/key.entity"; -import { ensureNonPrefixedAddress, ensurePrefixedAddress } from "../utils"; +import { + ensureNonPrefixedAddress, + ensurePrefixedAddress, +} from "../utils/common-utils"; import { getDataSourceInstance } from "../database"; import { ProjectContextLifecycle } from "../flow/utils/project-context"; import { ProjectEntity } from "../projects/project.entity"; @@ -220,13 +223,16 @@ export class ProcessorService implements ProjectContextLifecycle { } private async getUnprocessedBlocksInfo(): Promise { + if (!this.projectContext) { + throw new Error("Project context not found"); + } const [lastStoredBlock, latestBlock] = await Promise.all([ this.blockService.findLastBlock(), this.flowGatewayService.getLatestBlock(), ]); const nextBlockHeightToProcess = lastStoredBlock ? lastStoredBlock.blockHeight + 1 - : this.projectContext.startBlockHeight; + : this.projectContext.startBlockHeight ?? latestBlock.height; const latestUnprocessedBlockHeight = latestBlock.height; return { @@ -321,7 +327,6 @@ export class ProcessorService implements ProjectContextLifecycle { .create( this.createEventEntity({ flowEvent, - flowBlock: data.block, }) ) .catch((e) => @@ -462,7 +467,11 @@ export class ProcessorService implements ProjectContextLifecycle { case buildFlowTokensWithdrawnEvent( nonMonotonicAddresses.flowTokenAddress ): - return this.reprocessAccountFlowBalance(flowEvent.data.from); + // New emulator accounts are initialized + // with a default Flow balance coming from null address. + return flowEvent.data.from + ? this.reprocessAccountFlowBalance(flowEvent.data.from) + : undefined; case buildFlowTokensDepositedEvent(monotonicAddresses.flowTokenAddress): case buildFlowTokensDepositedEvent( nonMonotonicAddresses.flowTokenAddress @@ -493,7 +502,7 @@ export class ProcessorService implements ProjectContextLifecycle { private async storeNewAccountWithContractsAndKeys(options: { address: string; - flowBlock: FlowBlock | undefined; + flowBlock: FlowBlock; }) { const { address, flowBlock } = options; const flowAccount = await this.flowGatewayService.getAccount(address); @@ -521,7 +530,7 @@ export class ProcessorService implements ProjectContextLifecycle { await Promise.all([ this.accountKeysService.updateAccountKeys( address, - unSerializedAccount.keys + unSerializedAccount.keys ?? [] ), this.contractService.updateAccountContracts( unSerializedAccount.address, @@ -600,7 +609,7 @@ export class ProcessorService implements ProjectContextLifecycle { // but that service doesn't set the public key. // So if public key isn't present, // we know that we haven't processed this account yet. - return Boolean(serviceAccount.keys[0]?.publicKey); + return Boolean(serviceAccount.keys?.[0]?.publicKey); } catch (e) { // Service account not found return false; @@ -666,7 +675,6 @@ export class ProcessorService implements ProjectContextLifecycle { private createAccountEntity(options: { flowAccount: FlowAccount; - // Undefined in case we don't want to update block ID. flowBlock: FlowBlock; }): AccountEntity { const { flowAccount, flowBlock } = options; @@ -687,63 +695,64 @@ export class ProcessorService implements ProjectContextLifecycle { flowBlock: FlowBlock; }) { const { flowAccount, flowKey, flowBlock } = options; - const key = new AccountKeyEntity(); - key.blockId = flowBlock.id; - key.index = flowKey.index; - key.accountAddress = ensurePrefixedAddress(flowAccount.address); - key.publicKey = flowKey.publicKey; - key.signAlgo = flowKey.signAlgo; - key.hashAlgo = flowKey.hashAlgo; - key.weight = flowKey.weight; - key.sequenceNumber = flowKey.sequenceNumber; - key.revoked = flowKey.revoked; - return key; + return new AccountKeyEntity({ + index: flowKey.index, + accountAddress: ensurePrefixedAddress(flowAccount.address), + publicKey: flowKey.publicKey, + signAlgo: flowKey.signAlgo, + hashAlgo: flowKey.hashAlgo, + weight: flowKey.weight, + sequenceNumber: flowKey.sequenceNumber, + revoked: flowKey.revoked, + blockId: flowBlock.id, + privateKey: "", + account: undefined, + }); } private createEventEntity(options: { flowEvent: ExtendedFlowEvent; - flowBlock: FlowBlock; }): EventEntity { - const { flowEvent, flowBlock } = options; - const event = new EventEntity(); - event.blockId = flowBlock.id; - event.type = flowEvent.type; - event.transactionIndex = flowEvent.transactionIndex; - event.transactionId = flowEvent.transactionId; - event.blockId = flowEvent.blockId; - event.eventIndex = flowEvent.eventIndex; - event.data = flowEvent.data; - return event; + const { flowEvent } = options; + return new EventEntity({ + type: flowEvent.type, + transactionIndex: flowEvent.transactionIndex, + transactionId: flowEvent.transactionId, + blockId: flowEvent.blockId, + eventIndex: flowEvent.eventIndex, + data: flowEvent.data, + }); } private createBlockEntity(options: { flowBlock: FlowBlock }): BlockEntity { const { flowBlock } = options; - const block = new BlockEntity(); - block.blockId = flowBlock.id; - block.collectionGuarantees = flowBlock.collectionGuarantees; - block.blockSeals = flowBlock.blockSeals; - // TODO(milestone-x): "signatures" field is not present in block response - // https://github.com/onflow/fcl-js/issues/1355 - block.signatures = flowBlock.signatures ?? []; - block.timestamp = new Date(flowBlock.timestamp); - block.blockHeight = flowBlock.height; - block.parentId = flowBlock.parentId; - return block; + return new BlockEntity({ + blockId: flowBlock.id, + collectionGuarantees: flowBlock.collectionGuarantees, + blockSeals: flowBlock.blockSeals, + // TODO(milestone-x): "signatures" field is not present in block response + // https://github.com/onflow/fcl-js/issues/1355 + signatures: flowBlock.signatures ?? [], + timestamp: new Date(flowBlock.timestamp), + blockHeight: flowBlock.height, + parentId: flowBlock.parentId, + }); } private createContractEntity(options: { - flowBlock: FlowBlock | undefined; + flowBlock: FlowBlock; flowAccount: FlowAccount; name: string; code: string; }) { const { flowAccount, flowBlock, name, code } = options; - const contract = new AccountContractEntity(); - contract.blockId = flowBlock.id; - contract.accountAddress = ensurePrefixedAddress(flowAccount.address); - contract.name = name; - contract.code = code; - return contract; + return new AccountContractEntity({ + blockId: flowBlock.id, + accountAddress: ensurePrefixedAddress(flowAccount.address), + name: name, + code: code, + account: null, + }); } private createTransactionEntity(options: { @@ -752,33 +761,34 @@ export class ProcessorService implements ProjectContextLifecycle { flowTransactionStatus: FlowTransactionStatus; }): TransactionEntity { const { flowBlock, flowTransaction, flowTransactionStatus } = options; - const transaction = new TransactionEntity(); - transaction.id = flowTransaction.id; - transaction.script = flowTransaction.script; - transaction.payerAddress = ensurePrefixedAddress(flowTransaction.payer); - transaction.blockId = flowBlock.id; - transaction.referenceBlockId = flowTransaction.referenceBlockId; - transaction.gasLimit = flowTransaction.gasLimit; - transaction.authorizers = flowTransaction.authorizers.map((address) => - ensurePrefixedAddress(address) - ); - transaction.args = flowTransaction.args; - transaction.proposalKey = { - ...flowTransaction.proposalKey, - address: ensurePrefixedAddress(flowTransaction.proposalKey.address), - }; - transaction.envelopeSignatures = this.deserializeSignableObjects( - flowTransaction.envelopeSignatures - ); - transaction.payloadSignatures = this.deserializeSignableObjects( - flowTransaction.payloadSignatures - ); - transaction.status = TransactionStatus.fromJSON({ - errorMessage: flowTransactionStatus.errorMessage, - grcpStatus: flowTransactionStatus.statusCode, - executionStatus: flowTransactionStatus.status, + return new TransactionEntity({ + id: flowTransaction.id, + script: flowTransaction.script, + payerAddress: ensurePrefixedAddress(flowTransaction.payer), + blockId: flowBlock.id, + referenceBlockId: flowTransaction.referenceBlockId, + gasLimit: flowTransaction.gasLimit, + authorizers: flowTransaction.authorizers.map((address) => + ensurePrefixedAddress(address) + ), + args: flowTransaction.args, + proposalKey: { + ...flowTransaction.proposalKey, + address: ensurePrefixedAddress(flowTransaction.proposalKey.address), + }, + envelopeSignatures: this.deserializeSignableObjects( + flowTransaction.envelopeSignatures + ), + payloadSignatures: this.deserializeSignableObjects( + flowTransaction.payloadSignatures + ), + status: TransactionStatus.fromJSON({ + errorMessage: flowTransactionStatus.errorMessage, + grcpStatus: flowTransactionStatus.statusCode, + executionStatus: flowTransactionStatus.status, + }), + payer: undefined, }); - return transaction; } private deserializeSignableObjects(signableObjects: FlowSignableObject[]) { diff --git a/backend/src/events/event.entity.ts b/backend/src/events/event.entity.ts index 5c015c81..862ff87f 100644 --- a/backend/src/events/event.entity.ts +++ b/backend/src/events/event.entity.ts @@ -1,12 +1,13 @@ import { PollingEntity } from "../core/entities/polling.entity"; -import { AfterLoad, Column, Entity, PrimaryColumn } from "typeorm"; +import { Column, Entity, PrimaryColumn } from "typeorm"; import { Event } from "@flowser/shared"; import { BlockContextEntity } from "../blocks/entities/block-context.entity"; +import { PollingEntityInitArguments } from "../utils/type-utils"; + +type EventEntityInitArgs = Omit, "id">; @Entity({ name: "events" }) export class EventEntity extends PollingEntity implements BlockContextEntity { - id: string; - @PrimaryColumn() transactionId: string; @@ -25,9 +26,20 @@ export class EventEntity extends PollingEntity implements BlockContextEntity { @Column("simple-json") data: object; - @AfterLoad() - private computeId() { - this.id = `${this.transactionId}.${this.eventIndex}`; + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: EventEntityInitArgs | undefined) { + super(); + this.transactionId = args?.transactionId ?? ""; + this.blockId = args?.blockId ?? ""; + this.eventIndex = args?.eventIndex ?? -1; + this.type = args?.type ?? ""; + this.transactionIndex = args?.transactionIndex ?? -1; + this.data = args?.data ?? {}; + } + + get id() { + return `${this.transactionId}.${this.eventIndex}`; } toProto(): Event { diff --git a/backend/src/events/events.controller.ts b/backend/src/events/events.controller.ts index 9bc50111..b1405988 100644 --- a/backend/src/events/events.controller.ts +++ b/backend/src/events/events.controller.ts @@ -30,7 +30,7 @@ export class EventsController { @ApiParam({ name: "id", type: String }) @Get("/transactions/:id/events") - async findAllByTransaction(@Param("id") transactionId) { + async findAllByTransaction(@Param("id") transactionId: string) { const events = await this.eventsService.findAllByTransaction(transactionId); return GetAllEventsResponse.toJSON({ events: events.map((event) => event.toProto()), @@ -40,7 +40,7 @@ export class EventsController { @ApiParam({ name: "id", type: String }) @Post("/transactions/:id/events/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingEventsResponse)) - async findAllNewByTransaction(@Param("id") transactionId, @Body() data) { + async findAllNewByTransaction(@Param("id") transactionId: string, @Body() data: unknown) { const request = GetPollingEventsByTransactionRequest.fromJSON(data); const events = await this.eventsService.findAllByTransactionNewerThanTimestamp( @@ -52,7 +52,7 @@ export class EventsController { @Post("/events/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingEventsResponse)) - async findAllNew(@Body() data) { + async findAllNew(@Body() data: unknown) { const request = GetPollingEventsRequest.fromJSON(data); const events = await this.eventsService.findAllNewerThanTimestamp( new Date(request.timestamp) diff --git a/backend/src/events/events.service.ts b/backend/src/events/events.service.ts index e681a41d..43a467e9 100644 --- a/backend/src/events/events.service.ts +++ b/backend/src/events/events.service.ts @@ -21,7 +21,7 @@ export class EventsService { }); } - findAllNewerThanTimestamp(timestamp): Promise { + findAllNewerThanTimestamp(timestamp: Date): Promise { return this.eventRepository.find({ where: [ { createdAt: MoreThan(timestamp) }, @@ -38,7 +38,7 @@ export class EventsService { }); } - findAllByTransactionNewerThanTimestamp(transactionId: string, timestamp) { + findAllByTransactionNewerThanTimestamp(transactionId: string, timestamp: Date) { return this.eventRepository.find({ where: { createdAt: MoreThan(timestamp), diff --git a/backend/src/flow/entities/snapshot.entity.ts b/backend/src/flow/entities/snapshot.entity.ts index b0b3b2bb..5a445a28 100644 --- a/backend/src/flow/entities/snapshot.entity.ts +++ b/backend/src/flow/entities/snapshot.entity.ts @@ -2,6 +2,9 @@ import { Column, Entity, PrimaryColumn } from "typeorm"; import { PollingEntity } from "../../core/entities/polling.entity"; import { EmulatorSnapshot } from "@flowser/shared"; import { BlockContextEntity } from "../../blocks/entities/block-context.entity"; +import { PollingEntityInitArguments } from "../../utils/type-utils"; + +type SnapshotEntityInitArgs = PollingEntityInitArguments; @Entity() export class SnapshotEntity @@ -22,6 +25,16 @@ export class SnapshotEntity @Column({ nullable: true }) projectId: string; + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: SnapshotEntityInitArgs | undefined) { + super(); + this.id = args?.id ?? ""; + this.description = args?.description ?? ""; + this.blockId = args?.blockId ?? ""; + this.projectId = args?.projectId ?? ""; + } + toProto(): EmulatorSnapshot { return { id: this.id, diff --git a/backend/src/flow/flow.controller.ts b/backend/src/flow/flow.controller.ts index 44226174..ae87c049 100644 --- a/backend/src/flow/flow.controller.ts +++ b/backend/src/flow/flow.controller.ts @@ -58,7 +58,7 @@ export class FlowController { @UseInterceptors( new PollingResponseInterceptor(GetPollingEmulatorSnapshotsResponse) ) - async getSnapshotsWithPolling(@Body() data) { + async getSnapshotsWithPolling(@Body() data: unknown) { const request = GetPollingEmulatorSnapshotsRequest.fromJSON(data); const snapshots = await this.flowSnapshotService.findAllByProjectNewerThanTimestamp( @@ -68,7 +68,7 @@ export class FlowController { } @Post("snapshots") - async createSnapshot(@Body() body) { + async createSnapshot(@Body() body: unknown) { const request = CreateEmulatorSnapshotRequest.fromJSON(body); const snapshot = await this.flowSnapshotService.create(request); return RevertToEmulatorSnapshotResponse.toJSON( @@ -79,7 +79,7 @@ export class FlowController { } @Put("snapshots") - async checkoutSnapshot(@Body() body) { + async checkoutSnapshot(@Body() body: unknown) { const request = RevertToEmulatorSnapshotRequest.fromJSON(body); const snapshot = await this.flowSnapshotService.checkout(request); return RevertToEmulatorSnapshotResponse.toJSON( @@ -90,7 +90,7 @@ export class FlowController { } @Post("rollback") - async rollbackEmulator(@Body() body) { + async rollbackEmulator(@Body() body: unknown) { const request = RollbackToHeightRequest.fromJSON(body); await this.flowSnapshotService.rollback(request); return RollbackToHeightResponse.toJSON( diff --git a/backend/src/flow/services/cli.service.ts b/backend/src/flow/services/cli.service.ts index 70f26bdc..537e7a87 100644 --- a/backend/src/flow/services/cli.service.ts +++ b/backend/src/flow/services/cli.service.ts @@ -96,6 +96,9 @@ export class FlowCliService implements ProjectContextLifecycle { } async initConfig() { + if (!this.projectContext) { + throw new Error("Project context not found") + } const childProcess = new ManagedProcessEntity({ id: FlowCliService.processId, name: "Flow init", @@ -181,7 +184,7 @@ export class FlowCliService implements ProjectContextLifecycle { // This should only happen with a test build, // but let's handle it anyway just in case const unknownVersionMessage = "Version information unknown!"; - if (versionLog.data === unknownVersionMessage) { + if (!versionLog || versionLog.data === unknownVersionMessage) { throw new NotFoundException("Flow CLI version not found"); } const [_, version] = versionLog?.data?.split(/: /) ?? []; @@ -200,6 +203,9 @@ export class FlowCliService implements ProjectContextLifecycle { const lineWithData = output.find( (outputLine) => outputLine.data.length > 0 ); + if (!lineWithData) { + throw new Error("Output line with JSON data not found") + } return JSON.parse(lineWithData.data) as Output; } diff --git a/backend/src/flow/services/config.service.ts b/backend/src/flow/services/config.service.ts index 8115b0cd..3236ba12 100644 --- a/backend/src/flow/services/config.service.ts +++ b/backend/src/flow/services/config.service.ts @@ -10,7 +10,7 @@ import { ProjectEntity } from "../../projects/project.entity"; import { ContractTemplate, TransactionTemplate } from "@flowser/shared"; import { AbortController } from "node-abort-controller"; import * as fs from "fs"; -import { isObject } from "../../utils"; +import { isObject } from "../../utils/common-utils"; type FlowAddress = string; @@ -106,6 +106,9 @@ export class FlowConfigService implements ProjectContextLifecycle { } public getAccounts(): FlowAbstractAccountConfig[] { + if (!this.config.accounts) { + throw new Error("Accounts config not loaded"); + } const accountEntries = Object.entries(this.config.accounts); return accountEntries.map( @@ -120,6 +123,9 @@ export class FlowConfigService implements ProjectContextLifecycle { public async updateAccounts( newOrUpdatedAccounts: FlowAbstractAccountConfig[] ): Promise { + if (!this.config.accounts) { + throw new Error("Accounts config not loaded") + } for (const newOrUpdatedAccount of newOrUpdatedAccounts) { this.config.accounts[newOrUpdatedAccount.name] = { address: newOrUpdatedAccount.address, @@ -129,12 +135,13 @@ export class FlowConfigService implements ProjectContextLifecycle { await this.save(); } - private getPrivateKey(keyConfig: FlowAccountKeyConfig) { - if (typeof keyConfig === "string") { - return keyConfig; - } else { - return keyConfig.privateKey; + private getPrivateKey(keyConfig: FlowAccountKeyConfig): string { + const privateKey = + typeof keyConfig === "string" ? keyConfig : keyConfig.privateKey; + if (!privateKey) { + throw new Error("Private key not found in config"); } + return privateKey; } public async getContractTemplates(): Promise { @@ -197,6 +204,9 @@ export class FlowConfigService implements ProjectContextLifecycle { } private getContractFilePath(contractNameKey: string) { + if (!this.config.contracts) { + throw new Error("Contracts config not loaded") + } const contractConfig = this.config.contracts[contractNameKey]; const isSimpleFormat = typeof contractConfig === "string"; return isSimpleFormat ? contractConfig : contractConfig?.source; @@ -218,14 +228,6 @@ export class FlowConfigService implements ProjectContextLifecycle { ); } - private getAccountConfig(accountKey: string) { - return this.config.accounts[accountKey]; - } - - private getDatabasePath() { - return this.buildProjectPath(this.projectContext?.emulator.databasePath); - } - private getConfigPath() { return this.buildProjectPath(this.configFileName); } @@ -243,6 +245,9 @@ export class FlowConfigService implements ProjectContextLifecycle { if (!pathPostfix) { throw new InternalServerErrorException("Postfix path not provided"); } + if (!this.projectContext) { + throw new Error("Project context not found") + } // TODO(milestone-3): Detect if pathPostfix is absolute or relative and use it accordingly return path.join(this.projectContext.filesystemPath, pathPostfix); } diff --git a/backend/src/flow/services/dev-wallet.service.ts b/backend/src/flow/services/dev-wallet.service.ts index ffdbfaec..89cae65b 100644 --- a/backend/src/flow/services/dev-wallet.service.ts +++ b/backend/src/flow/services/dev-wallet.service.ts @@ -9,7 +9,7 @@ import * as http from "http"; @Injectable() export class FlowDevWalletService implements ProjectContextLifecycle { static readonly processId = "dev-wallet"; - private projectContext: ProjectEntity; + private projectContext: ProjectEntity | undefined; constructor(private readonly processManagerService: ProcessManagerService) {} @@ -29,6 +29,12 @@ export class FlowDevWalletService implements ProjectContextLifecycle { } async start() { + if (!this.projectContext) { + throw new Error("Project context not found") + } + if (!this.projectContext?.emulator) { + throw new Error("Emulator settings not found in project context") + } const devWalletProcess = new ManagedProcessEntity({ id: FlowDevWalletService.processId, name: "Dev wallet", diff --git a/backend/src/flow/services/emulator.service.ts b/backend/src/flow/services/emulator.service.ts index 4c0d48bc..e4c9e9a5 100644 --- a/backend/src/flow/services/emulator.service.ts +++ b/backend/src/flow/services/emulator.service.ts @@ -12,7 +12,7 @@ import { ProjectEntity } from "../../projects/project.entity"; import { ProcessManagerService } from "../../processes/process-manager.service"; import { ManagedProcessEntity } from "../../processes/managed-process.entity"; import { FlowGatewayService } from "./gateway.service"; -import { waitForMs } from "../../utils"; +import { isDefined, waitForMs } from "../../utils/common-utils"; type FlowWellKnownAddresses = { serviceAccountAddress: string; @@ -61,6 +61,9 @@ export class FlowEmulatorService implements ProjectContextLifecycle { public getWellKnownAddresses( options?: WellKnownAddressesOptions ): FlowWellKnownAddresses { + if (!this.projectContext?.emulator) { + throw new Error("Emulator settings not found on project context"); + } // When "simple-addresses" flag is provided, // a monotonic address generation mechanism is used: // https://github.com/onflow/flow-emulator/blob/ebb90a8e721344861bb7e44b58b934b9065235f9/emulator/blockchain.go#L336-L342 @@ -84,6 +87,9 @@ export class FlowEmulatorService implements ProjectContextLifecycle { } async start() { + if (!this.projectContext) { + throw new Error("Project context not found"); + } this.process = new ManagedProcessEntity({ id: FlowEmulatorService.processId, name: "Flow emulator", @@ -112,12 +118,12 @@ export class FlowEmulatorService implements ProjectContextLifecycle { snapshot: true, withContracts: true, blockTime: 0, - servicePrivateKey: undefined, + servicePrivateKey: "", databasePath: "./flowdb", tokenSupply: 1000000000, transactionExpiry: 10, - storagePerFlow: undefined, - minAccountBalance: undefined, + storagePerFlow: 100, + minAccountBalance: 0, transactionMaxGasLimit: 9999, scriptGasLimit: 100000, serviceSignatureAlgorithm: SignatureAlgorithm.ECDSA_P256, @@ -131,8 +137,14 @@ export class FlowEmulatorService implements ProjectContextLifecycle { private async waitUntilApisStarted() { // Wait until emulator process emits "Started " logs. - const hasStarted = () => - this.process.output.some((output) => output.data.includes("Started")); + const hasStarted = () => { + if (!this.process) { + throw new Error("Process not found"); + } + return this.process.output.some((output) => + output.data.includes("Started") + ); + }; while (!hasStarted()) { await waitForMs(100); } @@ -141,12 +153,17 @@ export class FlowEmulatorService implements ProjectContextLifecycle { private getAppliedFlags(): string[] { const { emulator } = this.projectContext ?? {}; + if (!emulator) { + throw new Error("Emulator not found in project context"); + } + const formatTokenSupply = (tokenSupply: number) => tokenSupply.toFixed(1); const flag = (name: string, userValue: any, defaultValue?: any) => { const value = userValue || defaultValue; return value ? `--${name}=${value}` : undefined; }; + // TODO: I think windows support for snapshots was fixed, so we can remove this check const isWindows = process.platform === "win32"; const isSnapshotFeatureDisabled = emulator.snapshot && isWindows; if (isSnapshotFeatureDisabled) { @@ -189,6 +206,6 @@ export class FlowEmulatorService implements ProjectContextLifecycle { flag("transaction-fees", emulator.transactionFees), flag("transaction-max-gas-limit", emulator.transactionMaxGasLimit), flag("script-gas-limit", emulator.scriptGasLimit), - ].filter(Boolean); + ].filter(isDefined); } } diff --git a/backend/src/flow/services/gateway.service.ts b/backend/src/flow/services/gateway.service.ts index 0e217d3c..a59dded8 100644 --- a/backend/src/flow/services/gateway.service.ts +++ b/backend/src/flow/services/gateway.service.ts @@ -159,7 +159,7 @@ export class FlowGatewayService implements ProjectContextLifecycle { ): Promise<{ transactionId: string }> { const transactionId = await fcl.mutate({ cadence: options.cadence, - args: (_arg, _t) => [], + args: (_arg: unknown, _t: unknown) => [], proposer: options.proposer, authorizations: options.authorizations, payer: options.payer, diff --git a/backend/src/flow/services/snapshot.service.ts b/backend/src/flow/services/snapshot.service.ts index 400ba810..5a2cb345 100644 --- a/backend/src/flow/services/snapshot.service.ts +++ b/backend/src/flow/services/snapshot.service.ts @@ -19,7 +19,7 @@ import { } from "@flowser/shared"; import { ProjectContextLifecycle } from "../utils/project-context"; import { ProjectEntity } from "src/projects/project.entity"; -import { computeEntitiesDiff } from "../../utils"; +import { computeEntitiesDiff } from "../../utils/common-utils"; import { BlocksService } from "../../blocks/blocks.service"; type CreateSnapshotRequest = { @@ -93,13 +93,14 @@ export class FlowSnapshotService implements ProjectContextLifecycle { ); } - const snapshot = new SnapshotEntity(); - snapshot.id = createdSnapshot.context; - snapshot.blockId = createdSnapshot.blockId; - snapshot.projectId = request.projectId; - snapshot.description = request.description; + const snapshotEntity = new SnapshotEntity({ + id: createdSnapshot.context, + blockId: createdSnapshot.blockId, + projectId: request.projectId, + description: request.description, + }); - return this.snapshotRepository.save(snapshot); + return this.snapshotRepository.save(snapshotEntity); } async checkout(request: RevertToEmulatorSnapshotRequest) { diff --git a/backend/src/flow/services/storage.service.ts b/backend/src/flow/services/storage.service.ts index 61ae8c2d..35f6540e 100644 --- a/backend/src/flow/services/storage.service.ts +++ b/backend/src/flow/services/storage.service.ts @@ -2,7 +2,7 @@ import { Injectable, HttpException } from "@nestjs/common"; import axios from "axios"; import { FlowAccount } from "./gateway.service"; import { AccountStorageItemEntity } from "../../accounts/entities/storage-item.entity"; -import { ensurePrefixedAddress } from "../../utils"; +import { ensurePrefixedAddress } from "../../utils/common-utils"; import { AccountStorageDomain } from "@flowser/shared"; /** @@ -98,25 +98,27 @@ export class FlowAccountStorageService { const storageData = flowAccountStorage[flowStorageDomain][flowStorageIdentifier]; - const storageItem = new AccountStorageItemEntity(); - storageItem.pathIdentifier = flowStorageIdentifier; - storageItem.pathDomain = this.flowStorageDomainToEnum(flowStorageDomain); - - // TODO(milestone-x): For now we will just show plain (unparsed) storage data - // But in the future we will want to parse it so that we can extract info - // This will be possible after storage API implements proper deserialization of storage data - if (typeof storageData !== "object") { - // In case the data is a simple value (string, number, boolean,...) - // we need to store it in object form (e.g. under "value" key). - // Otherwise, it won't get properly encoded/decoded by protocol buffers. - storageItem.data = { value: storageData }; - } else { - storageItem.data = storageData; + function getStorageData() { + // TODO(milestone-x): For now we will just show plain (unparsed) storage data + // But in the future we will want to parse it so that we can extract info + // This will be possible after storage API implements proper deserialization of storage data + if (typeof storageData !== "object") { + // In case the data is a simple value (string, number, boolean,...) + // we need to store it in object form (e.g. under "value" key). + // Otherwise, it won't get properly encoded/decoded by protocol buffers. + return { value: storageData }; + } else { + return storageData; + } } - storageItem.accountAddress = ensurePrefixedAddress( - flowAccountStorage.Address - ); - return storageItem; + + return new AccountStorageItemEntity({ + account: undefined, + accountAddress: ensurePrefixedAddress(flowAccountStorage.Address), + data: getStorageData(), + pathDomain: this.flowStorageDomainToEnum(flowStorageDomain), + pathIdentifier: flowStorageIdentifier, + }); } private flowStorageDomainToEnum( diff --git a/backend/src/flow/tests/cli.service.spec.ts b/backend/src/flow/tests/cli.service.spec.ts index 51477ff1..583c3602 100644 --- a/backend/src/flow/tests/cli.service.spec.ts +++ b/backend/src/flow/tests/cli.service.spec.ts @@ -2,6 +2,7 @@ import { FlowCliService } from "../services/cli.service"; import { FlowConfigService } from "../services/config.service"; import { ProcessManagerService } from "../../processes/process-manager.service"; import { ProjectEntity } from "../../projects/project.entity"; +import { DevWallet, Emulator, Gateway } from "@flowser/shared"; describe("FlowCliService", function () { let cliService: FlowCliService; @@ -11,7 +12,15 @@ describe("FlowCliService", function () { const processManagerService = new ProcessManagerService(); cliService = new FlowCliService(configService, processManagerService); - const mockProject = new ProjectEntity(); + const mockProject = new ProjectEntity({ + devWallet: DevWallet.fromPartial({}), + emulator: Emulator.fromPartial({}), + filesystemPath: "", + gateway: Gateway.fromPartial({}), + id: "", + name: "", + startBlockHeight: 0, + }); await cliService.onEnterProjectContext(mockProject); }); diff --git a/backend/src/flow/utils/cadence-utils.ts b/backend/src/flow/utils/cadence-utils.ts index 10fc69e8..a22b5570 100644 --- a/backend/src/flow/utils/cadence-utils.ts +++ b/backend/src/flow/utils/cadence-utils.ts @@ -5,19 +5,16 @@ import { cadenceTypeFromJSON, } from "@flowser/shared"; import { CadenceUtils as SharedCadenceUtils } from "@flowser/shared"; -import { isArray } from "../../utils"; +import { isArray } from "../../utils/common-utils"; export class CadenceUtils { static isCadenceObject(value: unknown): value is FlowCadenceObject { - return typeof value === "object" && "type" in value; + return typeof value === "object" && value !== null && "type" in value; } static serializeCadenceObject( - cadenceObject: FlowCadenceObject | unknown + cadenceObject: FlowCadenceObject ): CadenceObject { - if (!this.isCadenceObject(cadenceObject)) { - return; - } const cadenceType = cadenceTypeFromJSON(cadenceObject.type); if (SharedCadenceUtils.isNumericType(cadenceType)) { return this.serializeNumericValue(cadenceObject); diff --git a/backend/src/flow/utils/project-context.ts b/backend/src/flow/utils/project-context.ts index 2678b59d..26a6b64d 100644 --- a/backend/src/flow/utils/project-context.ts +++ b/backend/src/flow/utils/project-context.ts @@ -19,10 +19,12 @@ export interface ProjectContextLifecycle { // TODO: Use this to retrieve all services that implement the above interface automatically export function implementsProjectContextLifecycle( - object: unknown -): object is ProjectContextLifecycle { - if (typeof object !== "object") { - return false; - } - return "onEnterProjectContext" in object && "onExitProjectContext" in object; + value: unknown +): value is ProjectContextLifecycle { + return ( + typeof value === "object" && + value !== null && + "onEnterProjectContext" in value && + "onExitProjectContext" in value + ); } diff --git a/backend/src/processes/managed-process.entity.ts b/backend/src/processes/managed-process.entity.ts index d5c6c1b4..35beec32 100644 --- a/backend/src/processes/managed-process.entity.ts +++ b/backend/src/processes/managed-process.entity.ts @@ -53,7 +53,7 @@ export class ManagedProcessEntity extends EventEmitter { async waitOnExit() { return new Promise((resolve) => { - this.childProcess.once("exit", resolve); + this.childProcess?.once("exit", resolve); }); } @@ -65,7 +65,7 @@ export class ManagedProcessEntity extends EventEmitter { } this.logger.debug( - `Starting ${this.name} with command: ${command.name} ${command.args.join( + `Starting ${this.name} with command: ${command.name} ${command.args?.join( " " )}` ); @@ -86,7 +86,10 @@ export class ManagedProcessEntity extends EventEmitter { if (!this.isRunning()) { return; } - return new Promise(async (resolve, reject) => { + return new Promise(async (resolve, reject) => { + if (!this.childProcess) { + return; + } const isKilledSuccessfully = this.childProcess.kill("SIGINT"); this.childProcess.once("error", (error) => { reject(error); @@ -102,7 +105,7 @@ export class ManagedProcessEntity extends EventEmitter { const rejectionTimeoutSec = 6; setTimeout(() => { const timeoutError = new Error( - `Couldn't kill process ${this.id} (pid=${this.childProcess.pid}) within ${rejectionTimeoutSec}s timeout` + `Couldn't kill process ${this.id} (pid=${this.childProcess?.pid}) within ${rejectionTimeoutSec}s timeout` ); reject(timeoutError); }, rejectionTimeoutSec * 1000); @@ -118,7 +121,7 @@ export class ManagedProcessEntity extends EventEmitter { return { id: this.id, name: this.name, - command: { name, args }, + command: { name, args: args ?? [] }, state: this.state, output: this.output, updatedAt: this.updatedAt.toISOString(), @@ -127,6 +130,9 @@ export class ManagedProcessEntity extends EventEmitter { } private attachEventListeners() { + if (!this.childProcess) { + return; + } this.childProcess.once("spawn", () => { this.logger.debug(`Process ${this.id} started`); this.setState(ManagedProcessState.MANAGED_PROCESS_STATE_RUNNING); @@ -136,7 +142,7 @@ export class ManagedProcessEntity extends EventEmitter { `Process ${this.id} exited (code=${code}, signal=${signal})` ); this.setState( - code > 0 + code !== null && code > 0 ? ManagedProcessState.MANAGED_PROCESS_STATE_ERROR : ManagedProcessState.MANAGED_PROCESS_STATE_NOT_RUNNING ); @@ -152,6 +158,9 @@ export class ManagedProcessEntity extends EventEmitter { } private detachEventListeners() { + if (!this.childProcess) { + return; + } // Make sure to remove all listeners to prevent memory leaks this.childProcess.stdout.removeAllListeners("data"); this.childProcess.stderr.removeAllListeners("data"); diff --git a/backend/src/processes/process-manager.controller.ts b/backend/src/processes/process-manager.controller.ts index 58f9ab2b..bba32332 100644 --- a/backend/src/processes/process-manager.controller.ts +++ b/backend/src/processes/process-manager.controller.ts @@ -21,17 +21,17 @@ export class ProcessManagerController { constructor(private processManagerService: ProcessManagerService) {} @Post(":id/start") - async startProcess(@Param("id") processId) { - return this.processManagerService.start(processId); + async startProcess(@Param("id") processId: string) { + return this.processManagerService.startExisting(processId); } @Post(":id/stop") - async stopProcess(@Param("id") processId) { + async stopProcess(@Param("id") processId: string) { return this.processManagerService.stop(processId); } @Post(":id/restart") - async restartProcess(@Param("id") processId) { + async restartProcess(@Param("id") processId: string) { return this.processManagerService.restart(processId); } @@ -44,7 +44,7 @@ export class ProcessManagerController { @UseInterceptors( new PollingResponseInterceptor(GetPollingManagedProcessesResponse) ) - async getAllProcessesNewerThanTimestamp(@Body() body) { + async getAllProcessesNewerThanTimestamp(@Body() body: unknown) { const request = GetPollingManagedProcessesRequest.fromJSON(body); const processes = this.processManagerService.findAllProcessesNewerThanTimestamp( @@ -63,7 +63,7 @@ export class ProcessManagerController { @Post(":outputs/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingOutputsResponse)) - async getAllOutputsNewerThanTimestamp(@Body() data) { + async getAllOutputsNewerThanTimestamp(@Body() data: unknown) { const request = GetPollingOutputsRequest.fromJSON(data); return this.processManagerService.findAllLogsNewerThanTimestamp( new Date(request.timestamp) @@ -73,8 +73,8 @@ export class ProcessManagerController { @Post(":processId/outputs/polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingOutputsResponse)) async getAllOutputsByProcessNewerThanTimestamp( - @Param("processId") processId, - @Body() data + @Param("processId") processId: string, + @Body() data: unknown ) { const request = GetPollingOutputsRequest.fromJSON(data); return this.processManagerService.findAllLogsByProcessIdNewerThanTimestamp( diff --git a/backend/src/processes/process-manager.service.ts b/backend/src/processes/process-manager.service.ts index 911fcbe3..5b25a183 100644 --- a/backend/src/processes/process-manager.service.ts +++ b/backend/src/processes/process-manager.service.ts @@ -51,6 +51,9 @@ export class ProcessManagerService extends EventEmitter { timestamp: Date ): ManagedProcessOutput[] { const process = this.processLookupById.get(processId); + if (!process) { + return []; + } return process.output?.filter( (log) => new Date(log.createdAt).getTime() > timestamp.getTime() ); @@ -69,19 +72,30 @@ export class ProcessManagerService extends EventEmitter { } async start(process: ManagedProcessEntity) { - const existingProcess = this.processLookupById.get(process.id); - if (existingProcess) { - await existingProcess.stop(); - this.processLookupById.set(process.id, process); - this.emit(ProcessManagerEvent.PROCESS_UPDATED, process); + const isExisting = this.processLookupById.has(process.id); + if (isExisting) { + await this.startExisting(process.id); } else { - this.processLookupById.set(process.id, process); - this.emit(ProcessManagerEvent.PROCESS_ADDED, process); + await this.startNew(process); } + } + async startNew(process: ManagedProcessEntity) { + this.processLookupById.set(process.id, process); + this.emit(ProcessManagerEvent.PROCESS_ADDED, process); await process.start(); } + async startExisting(processId: string) { + const existingProcess = this.processLookupById.get(processId); + if (!existingProcess) { + throw new NotFoundException(`Existing process not found: ${processId}`); + } + await existingProcess.stop(); + this.emit(ProcessManagerEvent.PROCESS_UPDATED, process); + await existingProcess.start(); + } + /** * Starts the process, waits until it terminates (exits), * and returns the output it produced. @@ -93,7 +107,11 @@ export class ProcessManagerService extends EventEmitter { ): Promise { await this.start(process); await process.waitOnExit(); - if (process.childProcess.exitCode > 0) { + if ( + process.childProcess && + process.childProcess.exitCode !== null && + process.childProcess.exitCode > 0 + ) { const errorOutput = process.output.filter( (outputLine) => outputLine.source == ProcessOutputSource.OUTPUT_SOURCE_STDERR diff --git a/backend/src/projects/dto/create-project.dto.ts b/backend/src/projects/dto/create-project.dto.ts index b4aea121..dc466616 100644 --- a/backend/src/projects/dto/create-project.dto.ts +++ b/backend/src/projects/dto/create-project.dto.ts @@ -9,15 +9,18 @@ import { DevWalletConfigurationDto } from "./dev-wallet-configuration.dto"; export class CreateProjectDto implements Partial { @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. name: string; @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. filesystemPath: string; @ApiProperty({ description: "Data will be fetched from this block height forward.", }) + // @ts-ignore As this is always set automatically by the framework. startBlockHeight: number; @ApiProperty() @@ -28,10 +31,12 @@ export class CreateProjectDto implements Partial { @IsNotEmpty() @ValidateNested() @Type(() => EmulatorConfigurationDto) + // @ts-ignore As this is always set automatically by the framework. emulator: EmulatorConfigurationDto; @IsNotEmpty() @ValidateNested() @Type(() => DevWalletConfigurationDto) + // @ts-ignore As this is always set automatically by the framework. devWallet: DevWalletConfigurationDto; } diff --git a/backend/src/projects/dto/dev-wallet-configuration.dto.ts b/backend/src/projects/dto/dev-wallet-configuration.dto.ts index ee263fb3..85337c08 100644 --- a/backend/src/projects/dto/dev-wallet-configuration.dto.ts +++ b/backend/src/projects/dto/dev-wallet-configuration.dto.ts @@ -5,5 +5,6 @@ import { DevWallet } from "@flowser/shared"; export class DevWalletConfigurationDto implements DevWallet { @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. port: number; } diff --git a/backend/src/projects/dto/emulator-configuration.dto.ts b/backend/src/projects/dto/emulator-configuration.dto.ts index c6d02aa4..4a42b074 100644 --- a/backend/src/projects/dto/emulator-configuration.dto.ts +++ b/backend/src/projects/dto/emulator-configuration.dto.ts @@ -3,51 +3,75 @@ import { Emulator } from "@flowser/shared"; export class EmulatorConfigurationDto implements Emulator { @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. adminServerPort: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. blockTime: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. databasePath: string; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. enableGrpcDebug: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. enableRestDebug: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. grpcServerPort: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. logFormat: string; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. minAccountBalance: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. persist: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. snapshot: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. restServerPort: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. scriptGasLimit: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. serviceHashAlgorithm: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. servicePrivateKey: string; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. serviceSignatureAlgorithm: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. storageLimit: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. storagePerFlow: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. tokenSupply: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. transactionExpiry: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. transactionFees: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. transactionMaxGasLimit: number; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. useSimpleAddresses: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. verboseLogging: boolean; @ApiProperty() + // @ts-ignore As this is always set automatically by the framework. withContracts: boolean; } diff --git a/backend/src/projects/dto/gateway-configuration.dto.ts b/backend/src/projects/dto/gateway-configuration.dto.ts index ec9babd9..8f9fd906 100644 --- a/backend/src/projects/dto/gateway-configuration.dto.ts +++ b/backend/src/projects/dto/gateway-configuration.dto.ts @@ -5,13 +5,16 @@ import { Gateway, ServiceStatus } from "@flowser/shared"; export class GatewayConfigurationDto implements Gateway { @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. grpcServerAddress: string; @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. restServerAddress: string; @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. status: ServiceStatus; } diff --git a/backend/src/projects/dto/update-project.dto.ts b/backend/src/projects/dto/update-project.dto.ts index a9cf3e2e..18472d15 100644 --- a/backend/src/projects/dto/update-project.dto.ts +++ b/backend/src/projects/dto/update-project.dto.ts @@ -6,5 +6,6 @@ import { ApiProperty } from "@nestjs/swagger"; export class UpdateProjectDto extends PartialType(CreateProjectDto) { @ApiProperty() @IsNotEmpty() + // @ts-ignore As this is always set automatically by the framework. id: string; } diff --git a/backend/src/projects/project.entity.ts b/backend/src/projects/project.entity.ts index 66640fb5..a47b8623 100644 --- a/backend/src/projects/project.entity.ts +++ b/backend/src/projects/project.entity.ts @@ -1,10 +1,14 @@ import { Column, Entity, PrimaryColumn } from "typeorm"; -import { typeOrmProtobufTransformer } from "../utils"; +import { BadRequestException } from "@nestjs/common"; +import { typeOrmProtobufTransformer } from "../utils/common-utils"; import { CreateProjectDto } from "./dto/create-project.dto"; import { PollingEntity } from "../core/entities/polling.entity"; import { DevWallet, Emulator, Gateway, Project } from "@flowser/shared"; import { UpdateProjectDto } from "./dto/update-project.dto"; import * as crypto from "crypto"; +import { PollingEntityInitArguments } from "../utils/type-utils"; + +type ProjectEntityInitArgs = PollingEntityInitArguments; @Entity({ name: "projects" }) export class ProjectEntity extends PollingEntity { @@ -40,7 +44,20 @@ export class ProjectEntity extends PollingEntity { // User can specify (on a project level) the starting block height. // Blockchain data will be fetched from this height value if set. @Column({ nullable: true }) - startBlockHeight: number | null = 0; + startBlockHeight: number = 0; + + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args: ProjectEntityInitArgs | undefined) { + super(); + this.id = args?.id ?? ""; + this.name = args?.name ?? ""; + this.filesystemPath = args?.filesystemPath ?? ""; + this.devWallet = args?.devWallet ?? DevWallet.fromPartial({}); + this.gateway = args?.gateway ?? Gateway.fromPartial({}); + this.emulator = args?.emulator ?? Emulator.fromPartial({}); + this.startBlockHeight = args?.startBlockHeight ?? 0; + } hasGatewayConfiguration() { return this.gateway !== null; @@ -51,34 +68,44 @@ export class ProjectEntity extends PollingEntity { id: this.id, name: this.name, filesystemPath: this.filesystemPath, - startBlockHeight: this.startBlockHeight, + startBlockHeight: this.startBlockHeight ?? -1, gateway: this.gateway, devWallet: this.devWallet, - emulator: this.emulator, + emulator: this.emulator ?? undefined, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString(), }; } static create(projectDto: CreateProjectDto | UpdateProjectDto) { - const project = new ProjectEntity(); const isUpdateDto = "id" in projectDto && Boolean(projectDto.id); - if (isUpdateDto) { - project.id = projectDto.id; - } else { - project.id = crypto.randomUUID(); + + if (!projectDto.filesystemPath) { + throw new BadRequestException("Missing project filesystem path"); } - project.name = projectDto.name; - project.startBlockHeight = projectDto.startBlockHeight; - project.filesystemPath = projectDto.filesystemPath; - project.gateway = projectDto.gateway - ? Gateway.fromJSON(projectDto.gateway) - : Gateway.fromPartial({ - restServerAddress: `http://localhost:${projectDto.emulator.restServerPort}`, - grpcServerAddress: `http://localhost:${projectDto.emulator.grpcServerPort}`, - }); - project.devWallet = DevWallet.fromJSON(projectDto.devWallet); - project.emulator = Emulator.fromJSON(projectDto.emulator); - return project; + if (!projectDto.name) { + throw new BadRequestException("Missing project name"); + } + if ( + projectDto.startBlockHeight === null || + projectDto.startBlockHeight === undefined + ) { + throw new BadRequestException("Missing project start block height"); + } + + return new ProjectEntity({ + id: isUpdateDto ? projectDto.id : crypto.randomUUID(), + name: projectDto.name, + startBlockHeight: projectDto.startBlockHeight, + filesystemPath: projectDto.filesystemPath, + gateway: projectDto.emulator + ? Gateway.fromPartial({ + restServerAddress: `http://localhost:${projectDto.emulator.restServerPort}`, + grpcServerAddress: `http://localhost:${projectDto.emulator.grpcServerPort}`, + }) + : Gateway.fromJSON(projectDto.gateway), + devWallet: DevWallet.fromJSON(projectDto.devWallet), + emulator: Emulator.fromJSON(projectDto.emulator), + }); } } diff --git a/backend/src/projects/projects.controller.ts b/backend/src/projects/projects.controller.ts index e0e2919e..29391bb6 100644 --- a/backend/src/projects/projects.controller.ts +++ b/backend/src/projects/projects.controller.ts @@ -69,7 +69,7 @@ export class ProjectsController { @Post("polling") @UseInterceptors(new PollingResponseInterceptor(GetPollingProjectsResponse)) - async findAllNew(@Body() data) { + async findAllNew(@Body() data: unknown) { const request = GetPollingProjectsRequest.fromJSON(data); const projects = await this.projectsService.findAllNewerThanTimestamp( new Date(request.timestamp) diff --git a/backend/src/projects/projects.service.ts b/backend/src/projects/projects.service.ts index 6a37b114..8c92ab4c 100644 --- a/backend/src/projects/projects.service.ts +++ b/backend/src/projects/projects.service.ts @@ -43,7 +43,7 @@ const semver = require("semver"); @Injectable() export class ProjectsService { - private currentProject: ProjectEntity; + private currentProject: ProjectEntity | undefined; private readonly logger = new Logger(ProjectsService.name); // TODO: This should be refactored sooner or later. It's a weird system of bootstrapping services. diff --git a/backend/src/transactions/transaction.entity.ts b/backend/src/transactions/transaction.entity.ts index 515b2e06..b7e9dcbf 100644 --- a/backend/src/transactions/transaction.entity.ts +++ b/backend/src/transactions/transaction.entity.ts @@ -6,17 +6,14 @@ import { SignableObject, TransactionStatus, } from "@flowser/shared"; -import { - FlowBlock, - FlowCadenceObject, - FlowSignableObject, - FlowTransaction, - FlowTransactionStatus, -} from "../flow/services/gateway.service"; -import { ensurePrefixedAddress, typeOrmProtobufTransformer } from "../utils"; +import { FlowCadenceObject } from "../flow/services/gateway.service"; +import { typeOrmProtobufTransformer } from "../utils/common-utils"; import { AccountEntity } from "../accounts/entities/account.entity"; import { CadenceUtils } from "../flow/utils/cadence-utils"; import { BlockContextEntity } from "../blocks/entities/block-context.entity"; +import { PollingEntityInitArguments } from "../utils/type-utils"; + +type TransactionEntityInitArgs = PollingEntityInitArguments; @Entity({ name: "transactions" }) export class TransactionEntity @@ -42,7 +39,7 @@ export class TransactionEntity payerAddress: string; // payer account address @ManyToOne(() => AccountEntity, (account) => account.transactions) - payer: AccountEntity; // payer account address + payer?: AccountEntity; // payer account address @Column("simple-array") authorizers: string[]; // authorizers account addresses @@ -70,6 +67,28 @@ export class TransactionEntity }) status: TransactionStatus; + // Entities are also automatically initialized by TypeORM. + // In those cases no constructor arguments are provided. + constructor(args?: TransactionEntityInitArgs) { + super(); + this.id = args?.id ?? ""; + this.script = args?.script ?? ""; + this.blockId = args?.blockId ?? ""; + this.referenceBlockId = args?.referenceBlockId ?? ""; + this.gasLimit = args?.gasLimit ?? -1; + this.payerAddress = args?.payerAddress ?? ""; + if (args?.payer) { + this.payer = args.payer; + } + this.authorizers = args?.authorizers ?? []; + this.args = args?.args ?? []; + this.proposalKey = + args?.proposalKey ?? TransactionProposalKey.fromPartial({}); + this.envelopeSignatures = args?.envelopeSignatures ?? []; + this.payloadSignatures = args?.payloadSignatures ?? []; + this.status = args?.status ?? TransactionStatus.fromPartial({}); + } + toProto(): Transaction { return { id: this.id, diff --git a/backend/src/transactions/transactions.controller.ts b/backend/src/transactions/transactions.controller.ts index 3bf65269..30a1819e 100644 --- a/backend/src/transactions/transactions.controller.ts +++ b/backend/src/transactions/transactions.controller.ts @@ -35,7 +35,7 @@ export class TransactionsController { @UseInterceptors( new PollingResponseInterceptor(GetPollingTransactionsResponse) ) - async findAllNew(@Body() data) { + async findAllNew(@Body() data: unknown) { const request = GetPollingTransactionsRequest.fromJSON(data); const transactions = await this.transactionsService.findAllNewerThanTimestamp( @@ -46,7 +46,7 @@ export class TransactionsController { @ApiParam({ name: "id", type: String }) @Get("/blocks/:id/transactions") - async findAllByBlock(@Param("id") blockId) { + async findAllByBlock(@Param("id") blockId: string) { const transactions = await this.transactionsService.findAllByBlock(blockId); return GetAllTransactionsResponse.fromPartial({ transactions: transactions.map((transaction) => transaction.toProto()), @@ -58,7 +58,7 @@ export class TransactionsController { @UseInterceptors( new PollingResponseInterceptor(GetPollingTransactionsResponse) ) - async findAllNewByBlock(@Param("id") blockId, @Body() data) { + async findAllNewByBlock(@Param("id") blockId: string, @Body() data: unknown) { const request = GetPollingTransactionsByBlockRequest.fromJSON(data); const transactions = await this.transactionsService.findAllNewerThanTimestampByBlock( @@ -74,7 +74,10 @@ export class TransactionsController { @UseInterceptors( new PollingResponseInterceptor(GetPollingTransactionsResponse) ) - async findAllNewByAccount(@Param("address") accountAddress, @Body() data) { + async findAllNewByAccount( + @Param("address") accountAddress: string, + @Body() data: unknown + ) { const request = GetPollingTransactionsByAccountRequest.fromJSON(data); const transactions = await this.transactionsService.findAllNewerThanTimestampByAccount( diff --git a/backend/src/utils.spec.ts b/backend/src/utils/common-utils.spec.ts similarity index 95% rename from backend/src/utils.spec.ts rename to backend/src/utils/common-utils.spec.ts index 1cb3e802..1d011fe6 100644 --- a/backend/src/utils.spec.ts +++ b/backend/src/utils/common-utils.spec.ts @@ -1,4 +1,4 @@ -import { computeEntitiesDiff } from "./utils"; +import { computeEntitiesDiff } from "./common-utils"; describe("Utils", function () { it("should compute entities diff using deep compare", function () { diff --git a/backend/src/utils.ts b/backend/src/utils/common-utils.ts similarity index 95% rename from backend/src/utils.ts rename to backend/src/utils/common-utils.ts index d98171bf..ede884da 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils/common-utils.ts @@ -1,6 +1,12 @@ import { rm } from "fs/promises"; import { ProtobufLikeObject } from "@flowser/shared"; +export function isDefined( + value: Value | null | undefined +): value is Value { + return value !== null && value !== undefined; +} + export function waitForMs(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -10,7 +16,7 @@ export function isObject(value: unknown): value is Record { } export function isArray(value: unknown): value is unknown[] { - return typeof value === "object" && "map" in value; + return typeof value === "object" && value !== null && "map" in value; } export async function rmdir(path: string) { diff --git a/backend/src/utils/type-utils.ts b/backend/src/utils/type-utils.ts new file mode 100644 index 00000000..4424810a --- /dev/null +++ b/backend/src/utils/type-utils.ts @@ -0,0 +1,13 @@ +import { PollingEntity } from "../core/entities/polling.entity"; + +// https://stackoverflow.com/questions/58210331/exclude-function-types-from-an-object-type +type NonFunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? never : K; +}[keyof T]; +type NonFunctionProperties = Pick>; + +export type PollingEntityInitArguments = Omit< + NonFunctionProperties, + // Ignore, as these are set automatically. + "createdAt" | "updatedAt" +>; diff --git a/backend/src/wallet/wallet.controller.ts b/backend/src/wallet/wallet.controller.ts index 7be1b234..c7586535 100644 --- a/backend/src/wallet/wallet.controller.ts +++ b/backend/src/wallet/wallet.controller.ts @@ -10,7 +10,7 @@ export class WalletController { constructor(private readonly walletService: WalletService) {} @Post("accounts/transaction") - async sendTransaction(@Body() body) { + async sendTransaction(@Body() body: unknown) { const request = SendTransactionRequest.fromJSON(body); const response = await this.walletService.sendTransaction(request); return SendTransactionResponse.toJSON(response); diff --git a/backend/src/wallet/wallet.service.ts b/backend/src/wallet/wallet.service.ts index cd384360..099b5569 100644 --- a/backend/src/wallet/wallet.service.ts +++ b/backend/src/wallet/wallet.service.ts @@ -8,7 +8,7 @@ import { SHA3 } from "sha3"; import { FlowCliService, KeyWithWeight } from "../flow/services/cli.service"; import { AccountsService } from "../accounts/services/accounts.service"; import { AccountEntity } from "../accounts/entities/account.entity"; -import { ensurePrefixedAddress } from "../utils"; +import { ensurePrefixedAddress } from "../utils/common-utils"; import { AccountKeyEntity, defaultKeyWeight, @@ -42,7 +42,8 @@ export class WalletService implements ProjectContextLifecycle { ) {} async onEnterProjectContext(project: ProjectEntity): Promise { - // TODO(snapshots-revamp): Re-import accounts when emulator state changes? + // TODO(snapshots-revamp): Re-import accounts when flow.json is updated + // (it could be the case that user is manually adding new managed accounts with `flow accounts create` command). await this.importAccountsFromConfig(); } @@ -74,6 +75,10 @@ export class WalletService implements ProjectContextLifecycle { private async withAuthorization(address: string) { const storedAccount = await this.accountsService.findOneByAddress(address); + if (!storedAccount.keys) { + throw new Error("Keys not loaded for account"); + } + const credentialsWithPrivateKeys = storedAccount.keys.filter((key) => Boolean(key.privateKey) ); @@ -95,7 +100,10 @@ export class WalletService implements ProjectContextLifecycle { tempId: `${address}-${credentialToUse.index}`, addr: fcl.sansPrefix(address), keyId: credentialToUse.index, - signingFunction: (signable) => { + signingFunction: (signable: any) => { + if (!credentialToUse.privateKey) { + throw new Error("Private key not found"); + } return { addr: fcl.withPrefix(address), keyId: credentialToUse.index, @@ -191,6 +199,12 @@ export class WalletService implements ProjectContextLifecycle { accountEntity.keys ); + // Assume only a single key per account for now + const singlePrivateKey = accountEntity.keys[0].privateKey; + if (!singlePrivateKey) { + throw new Error("Private key not found"); + } + // For now, we just write new accounts to flow.json, // but they don't get recreated on the next emulator run. // See: https://github.com/onflow/flow-emulator/issues/405 @@ -199,8 +213,7 @@ export class WalletService implements ProjectContextLifecycle { // TODO(custom-wallet): Come up with a human-readable name generation name: accountEntity.address, address: accountEntity.address, - // Assume only a single key per account for now - privateKey: accountEntity.keys[0].privateKey, + privateKey: singlePrivateKey, }, ]); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 484962fb..1a4db3a8 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -14,11 +14,11 @@ "outDir": "./dist", "baseUrl": "./", "incremental": true, - "skipLibCheck": false, - "strictNullChecks": false, - "noImplicitAny": false, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": true } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 6cd7b43c..065dfae3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -17,6 +17,8 @@ "esModuleInterop": true, "allowJs": true, "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, diff --git a/shared/proto/entities/projects.proto b/shared/proto/entities/projects.proto index 68cd2464..c96d3d69 100644 --- a/shared/proto/entities/projects.proto +++ b/shared/proto/entities/projects.proto @@ -22,7 +22,7 @@ message Project { string name = 2; string filesystem_path = 10; // Blockchain data will be fetched from this block height - // Set this null to start fetching from the latest block + // Set this -1 to start fetching from the latest block int32 start_block_height = 5; Gateway gateway = 6; diff --git a/yarn.lock b/yarn.lock index 12c6d5e0..84ed39e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4176,6 +4176,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/bn.js@*": + "integrity" "sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==" + "resolved" "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz" + "version" "5.1.1" + dependencies: + "@types/node" "*" + "@types/body-parser@*": "integrity" "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==" "resolved" "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" @@ -4216,6 +4223,13 @@ dependencies: "@types/ms" "*" +"@types/elliptic@^6.4.14": + "integrity" "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==" + "resolved" "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz" + "version" "6.4.14" + dependencies: + "@types/bn.js" "*" + "@types/eslint-scope@^3.7.3": "integrity" "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==" "resolved" "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz"