Skip to content

Commit

Permalink
Support project templates and new import standard (#180)
Browse files Browse the repository at this point in the history
* add go-bindings readme

* remove old templates logic, add basic project file tree traversal

* build template messages

* display project templates in interaction screen

* set static templates wrapper height

* increase polling interval

* set updated/created date for project templates

* validate flow.json account config, ignore sensitive private keys

* ignore "not a directory" errors

* remove unused methods in config service

* load flow.json config in fcl (first pass)

* upgrade react-scripts to v5

* fix fcl imports, fix error message handling

* upgrade to latest fcl version, fix no flow config handling

* load flow.json on the backend

* ignore blocks without transactions

* fix transaction name calculation
  • Loading branch information
bartolomej authored Aug 20, 2023
1 parent 9ccb447 commit 2fe0061
Show file tree
Hide file tree
Showing 38 changed files with 17,036 additions and 18,333 deletions.
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@nestjs/serve-static": "^3.0.0",
"@nestjs/swagger": "^5.1.1",
"@nestjs/typeorm": "^8.1.0",
"@onflow/fcl": "^1.2.0",
"@onflow/fcl": "^1.5.1",
"@onflow/types": "^1.0.3",
"axios": "^0.21.4",
"class-transformer": "0.4.0",
Expand Down
37 changes: 19 additions & 18 deletions backend/src/flow/flow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
Put,
UseInterceptors,
} from "@nestjs/common";
import { FlowGatewayService } from "./services/gateway.service";
import { FlowEmulatorService } from "./services/emulator.service";
import { FlowCliService } from "./services/cli.service";
import { FlowSnapshotService } from "./services/snapshot.service";
import {
Expand All @@ -17,41 +15,44 @@ import {
RevertToEmulatorSnapshotRequest,
RevertToEmulatorSnapshotResponse,
GetPollingEmulatorSnapshotsRequest,
GetProjectObjectsResponse,
RollbackToHeightRequest,
RollbackToHeightResponse,
GetFlowInteractionTemplatesResponse,
GetFlowConfigResponse,
} from "@flowser/shared";
import { PollingResponseInterceptor } from "../core/interceptors/polling-response.interceptor";
import { FlowTemplatesService } from "./services/templates.service";
import { FlowConfigService } from "./services/config.service";

@Controller("flow")
export class FlowController {
constructor(
private flowGatewayService: FlowGatewayService,
private flowEmulatorService: FlowEmulatorService,
private flowCliService: FlowCliService,
private flowConfigService: FlowConfigService,
private flowSnapshotService: FlowSnapshotService
private flowSnapshotService: FlowSnapshotService,
private flowTemplatesService: FlowTemplatesService
) {}

@Get("config")
async getConfig() {
const flowJson = this.flowConfigService.getRawConfig();
return GetFlowConfigResponse.toJSON({
flowJson: flowJson ? JSON.stringify(flowJson) : "",
});
}

@Get("version")
async getVersion() {
const info = await this.flowCliService.getVersion();
return GetFlowCliInfoResponse.toJSON(info);
}

@Get("objects")
async findCurrentProjectObjects() {
const [transactions, contracts] = await Promise.all([
this.flowConfigService.getTransactionTemplates(),
this.flowConfigService.getContractTemplates(),
]);
return GetProjectObjectsResponse.toJSON(
GetProjectObjectsResponse.fromPartial({
transactions,
contracts,
})
);
@Get("templates")
async getInteractionTemplates() {
const templates = await this.flowTemplatesService.getLocalTemplates();
return GetFlowInteractionTemplatesResponse.toJSON({
templates,
});
}

@Post("snapshots/polling")
Expand Down
5 changes: 5 additions & 0 deletions backend/src/flow/flow.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { FlowSnapshotService } from "./services/snapshot.service";
import { ProcessesModule } from "../processes/processes.module";
import { CoreModule } from "../core/core.module";
import { BlocksModule } from "../blocks/blocks.module";
import { FlowTemplatesService } from './services/templates.service';
import { GoBindingsModule } from '../go-bindings/go-bindings.module';

@Module({
imports: [
Expand All @@ -22,9 +24,11 @@ import { BlocksModule } from "../blocks/blocks.module";
// to access data removal service from snapshots service.
// Otherwise, this module shouldn't depend on many other modules.
CoreModule,
GoBindingsModule
],
controllers: [FlowController],
providers: [
FlowTemplatesService,
FlowGatewayService,
FlowEmulatorService,
FlowCliService,
Expand All @@ -33,6 +37,7 @@ import { BlocksModule } from "../blocks/blocks.module";
FlowSnapshotService,
],
exports: [
FlowTemplatesService,
FlowGatewayService,
FlowEmulatorService,
FlowCliService,
Expand Down
105 changes: 42 additions & 63 deletions backend/src/flow/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {
Injectable,
Logger,
InternalServerErrorException,
PreconditionFailedException,
} from "@nestjs/common";
import { readFile, writeFile, watch } from "fs/promises";
import * as path from "path";
import { ProjectContextLifecycle } from "../utils/project-context";
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/common-utils";
Expand Down Expand Up @@ -36,8 +36,8 @@ type FlowAccountsConfig = Record<FlowAccountName, FlowAccountConfig>;
type FlowAccountName = "emulator-account" | string;

type FlowAccountConfig = {
address: string;
key: FlowAccountKeyConfig;
address?: string;
key?: FlowAccountKeyConfig;
};

type FlowAccountKeySimpleConfig = string;
Expand Down Expand Up @@ -77,14 +77,14 @@ export type FlowAbstractAccountConfig = {
name: string;
// Possibly without the '0x' prefix.
address: string;
privateKey: string;
privateKey: string | undefined;
};

@Injectable()
export class FlowConfigService implements ProjectContextLifecycle {
private logger = new Logger(FlowConfigService.name);
private fileListenerController: AbortController | undefined;
private config: FlowCliConfig = {};
private config: FlowCliConfig | undefined;
private configFileName = "flow.json";
private projectContext: ProjectEntity | undefined;

Expand All @@ -98,6 +98,10 @@ export class FlowConfigService implements ProjectContextLifecycle {
this.detachListeners();
}

public getRawConfig(): FlowCliConfig | undefined {
return this.config;
}

public async reload() {
this.logger.debug("Reloading flow.json config");
this.detachListeners();
Expand All @@ -106,25 +110,37 @@ export class FlowConfigService implements ProjectContextLifecycle {
}

public getAccounts(): FlowAbstractAccountConfig[] {
if (!this.config.accounts) {
if (!this.config?.accounts) {
throw new Error("Accounts config not loaded");
}
const accountEntries = Object.entries(this.config.accounts);

return accountEntries.map(
([name, config]): FlowAbstractAccountConfig => ({
name,
address: config.address,
privateKey: this.getPrivateKey(config.key),
})
([name, accountConfig]): FlowAbstractAccountConfig => {
if (!accountConfig.address) {
throw this.missingConfigError(
`accounts.${accountConfig.address}.address`
);
}
if (!accountConfig.key) {
throw this.missingConfigError(
`accounts.${accountConfig.address}.key`
);
}
return {
name,
address: accountConfig.address,
privateKey: this.getPrivateKey(accountConfig.key),
};
}
);
}

public async updateAccounts(
newOrUpdatedAccounts: FlowAbstractAccountConfig[]
): Promise<void> {
if (!this.config.accounts) {
throw new Error("Accounts config not loaded")
if (!this.config?.accounts) {
throw new Error("Accounts config not loaded");
}
for (const newOrUpdatedAccount of newOrUpdatedAccounts) {
this.config.accounts[newOrUpdatedAccount.name] = {
Expand All @@ -135,46 +151,12 @@ export class FlowConfigService implements ProjectContextLifecycle {
await this.save();
}

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<ContractTemplate[]> {
const contractNamesAndPaths = Object.keys(this.config.contracts ?? {}).map(
(nameKey) => ({
name: nameKey,
filePath: this.getContractFilePath(nameKey),
})
);

const contractsSourceCode = await Promise.all(
contractNamesAndPaths.map(({ filePath }) =>
this.readProjectFile(filePath)
)
);

return contractNamesAndPaths.map(({ name, filePath }, index) =>
ContractTemplate.fromPartial({
name,
filePath,
sourceCode: contractsSourceCode[index],
})
);
}

public async getTransactionTemplates(): Promise<TransactionTemplate[]> {
// TODO(milestone-x): Is there a way to retrieve all project transaction files?
// For now we can't reliably tell where are transactions source files located,
// because they are not defined in flow.json config file - but this may be doable in the future.
// For now we have 2 options:
// - try to find a /transactions folder and read all files (hopefully transactions) within it
// - provide a Flowser setting to specify a path to the transactions folder
return [];
private getPrivateKey(keyConfig: FlowAccountKeyConfig): string | undefined {
// Private keys can also be defined in external files or env variables,
// but for now just ignore those, since those are likely very sensitive credentials,
// that should be used for deployments only.
// See: https://developers.flow.com/next/tools/toolchains/flow-cli/flow.json/configuration#accounts
return typeof keyConfig === "string" ? keyConfig : keyConfig.privateKey;
}

public hasConfigFile(): boolean {
Expand Down Expand Up @@ -203,15 +185,6 @@ export class FlowConfigService implements ProjectContextLifecycle {
this.fileListenerController?.abort();
}

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;
}

private async load() {
try {
const data = await this.readProjectFile(this.configFileName);
Expand Down Expand Up @@ -246,9 +219,15 @@ export class FlowConfigService implements ProjectContextLifecycle {
throw new InternalServerErrorException("Postfix path not provided");
}
if (!this.projectContext) {
throw new Error("Project context not found")
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);
}

private missingConfigError(path: string) {
return new PreconditionFailedException(
`Missing flow.json configuration key: ${path}`
);
}
}
15 changes: 12 additions & 3 deletions backend/src/flow/services/gateway.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
Gateway,
ServiceStatus,
} from "@flowser/shared";

const fcl = require("@onflow/fcl");
import * as fcl from "@onflow/fcl";
import { FlowConfigService } from "./config.service";

// https://docs.onflow.org/fcl/reference/api/#collectionguaranteeobject
export type FlowCollectionGuarantee = {
Expand Down Expand Up @@ -145,6 +145,8 @@ export class FlowGatewayService implements ProjectContextLifecycle {
private static readonly logger = new Logger(FlowGatewayService.name);
private projectContext: ProjectEntity | undefined;

constructor(private readonly flowConfigService: FlowConfigService) {}

onEnterProjectContext(project: ProjectEntity): void {
this.projectContext = project;
const { restServerAddress } = this.projectContext.gateway ?? {};
Expand All @@ -154,7 +156,14 @@ export class FlowGatewayService implements ProjectContextLifecycle {
FlowGatewayService.logger.debug(
`@onflow/fcl listening on ${restServerAddress}`
);
fcl.config().put("accessNode.api", restServerAddress);
fcl
.config({
"accessNode.api": restServerAddress,
"flow.network": "emulator",
})
.load({
flowJSON: this.flowConfigService.getRawConfig(),
});
}
onExitProjectContext(): void {
this.projectContext = undefined;
Expand Down
Loading

0 comments on commit 2fe0061

Please sign in to comment.