Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support project templates and new import standard #180

Merged
merged 17 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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