Skip to content

Commit

Permalink
Stricter type checks (#169)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bartolomej authored Jun 25, 2023
1 parent fa60c6e commit ac67557
Show file tree
Hide file tree
Showing 52 changed files with 632 additions and 288 deletions.
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions backend/src/accounts/controllers/accounts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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 =
Expand Down
7 changes: 5 additions & 2 deletions backend/src/accounts/controllers/contracts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand Down
64 changes: 46 additions & 18 deletions backend/src/accounts/entities/account.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountEntity>;

@Entity({ name: "accounts" })
export class AccountEntity extends PollingEntity implements BlockContextEntity {
Expand All @@ -30,49 +33,74 @@ 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 {
return {
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(),
Expand Down
28 changes: 20 additions & 8 deletions backend/src/accounts/entities/contract.entity.ts
Original file line number Diff line number Diff line change
@@ -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<AccountContractEntity>,
"id"
>;

@Entity({ name: "contracts" })
export class AccountContractEntity
Expand All @@ -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 {
Expand All @@ -43,7 +55,7 @@ export class AccountContractEntity
}

get id() {
return `${this.accountAddress}.${this.name}`
return `${this.accountAddress}.${this.name}`;
}

public static decodeId(id: string) {
Expand Down
57 changes: 43 additions & 14 deletions backend/src/accounts/entities/key.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountKeyEntity>;

// https://developers.flow.com/tooling/flow-cli/accounts/create-accounts#key-weight
export const defaultKeyWeight = 1000;
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
30 changes: 22 additions & 8 deletions backend/src/accounts/entities/storage-item.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountStorageItemEntity>,
"_id" | "id"
>;

@Entity({ name: "storage" })
export class AccountStorageItemEntity extends PollingEntity {
Expand All @@ -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()}/${
Expand Down
5 changes: 4 additions & 1 deletion backend/src/accounts/services/contracts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion backend/src/accounts/services/keys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion backend/src/accounts/services/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
computeEntitiesDiff,
ensurePrefixedAddress,
processEntitiesDiff,
} from "../../utils";
} from "../../utils/common-utils";

@Injectable()
export class AccountStorageService {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/blocks/blocks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit ac67557

Please sign in to comment.