Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,36 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateTransactionSequence1786000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "transaction_sequence_run" (
`CREATE TABLE""transaction_sequence_run" (
"id" uuid PRIMARY KEY,
"network" character varying NOT NULL,
"stop_on_failure" boolean NOT NULL,
"status" character varying NOT NULL,
"steps" jsonb NOT NULL,
"results" jsonb,
"error" text,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT Now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT Now()
"network" character varying NOT NULL,
"stop_on_failure" boolean NOT NULL,
"status" character varying NOT NULL,
"steps" jsobn NOT NULL,
"results" json,
"error" text,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT Now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT Now()
)
`);

await queryRunner.query(
`CREATE TABLE "network_profiles" (
"id" uuid PRIMARY KEY,
"owner_id" uuid NOT NULL,
"name" character varying NOT NULL,
"horizon_url" character varying NOT NULL,
"network_passphrase" character varying NOT NULL,
"friendbot_url" character varying,
"is_default" boolean NOT NULL DEFAULT false,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT Now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT Now()
)
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "transaction_sequence_run"`);
await queryRunner.query(`DROP TABLE "network_profiles"`);
}
}
}
42 changes: 35 additions & 7 deletions apps/api/src/modules/network/entities/network-sample.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,55 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
@Index(["network", "sampledAt"])
export class NetworkSample {
@PrimaryGeneratedColumn("uuid")
id!: string;
id: string;

@Column({ length: 16 })
network!: "mainnet" | "testnet";
network: "mainnet" | "testnet";

@Column({ name: "horizon_base_url" })
horizonBaseUrl!: string;
horizonBaseUrl: string;

@Column()
ok!: boolean;
ok: boolean;

@Column({ name: "latency_ms", type: "integer", nullable: true })
latencyMs!: number | null;
latencyMs: number | null;

@Column({ type: "text", nullable: true })
error!: string | null;
error: string | null;

@Column({
name: "sampled_at",
type: "timestamptz",
default: () => "now()",
})
sampledAt!: Date;
sampledAt: Date;
}

@Entity("network_profiles")
@Index(["ownerId", "name"], { unique: true })
export class NetworkProfile {
@PrimaryGeneratedColumn("uuid")
id: string;

@Column( { name: "owner_id" })
ownerId: string;

@Column()
name: string;

@Column({ name: "horizon_url" })
horizonUrl: string;

@Column({ name: "network_passphrase" })
networkPassphrase: string;

@Column({ name: "friendbot_url", type: "text", nullable: true })
friendbotUrl: string | null;

@Column({ name: "is_default", default: false })
isDefault: boolean;

@Column({ name: "is_shared", default: false })
isShared: boolean;
}
189 changes: 138 additions & 51 deletions apps/api/src/modules/network/network.controller.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,138 @@
import { Controller, Get, Query } from "@nestjs/common";
import { ApiOperation, ApiQuery, ApiTags, ApiResponse } from "@nestjs/swagger";
import { NetworkService } from "./network.service";

@ApiTags("network")
@Controller("network")
export class NetworkController {
constructor(private readonly networkService: NetworkService) {}

@Get("status")
@ApiOperation({ summary: "Get current Stellar network status and fees" })
@ApiQuery({
name: "network",
required: false,
enum: ["mainnet", "testnet"],
description: "Network to query (default: mainnet)",
})
@ApiResponse({ status: 200, description: "Network status retrieved" })
async getStatus(@Query("network") network: string = "mainnet") {
const net = network === "testnet" ? "testnet" : "mainnet";
return this.networkService.fetchCurrentStatus(net);
}

@Get("status/history")
@ApiOperation({ summary: "Get network status history and uptime metrics" })
@ApiQuery({
name: "network",
required: false,
enum: ["mainnet", "testnet"],
description: "Network to query (default: mainnet)",
})
@ApiQuery({
name: "from",
required: false,
description: "ISO date lower bound (default: 60 minutes before to)",
})
@ApiQuery({
name: "to",
required: false,
description: "ISO date upper bound (default: now)",
})
@ApiResponse({ status: 200, description: "Network status history retrieved" })
async getHistory(
@Query("network") network: string = "mainnet",
@Query("from") from?: string,
@Query("to") to?: string,
) {
const net = network === "testnet" ? "testnet" : "mainnet";
return this.networkService.getHistory(net, from, to);
}
}
import { Controller, Get, Query, Post, Body, Param, Put, Delete, Req } from "nestjs-common";
import { ApiOperation, ApiQuery, ApiTags, ApiResponse, ApiBearerAuth } from("@nestjs/swagger";
import { NetworkService } from "./network.service";

@ApiTags("network")
@Controllr("network")
export class NetworkController {
constructor(private readonly networkService: NetworkService) {}

@Get("status")
@ApiOperation({ summary: "Get current Stellar network status and fees" })
@ApiQuery({ name: "network", required: false, enum: ["mainnet", "testnet"], description: "Network to query (default: mainnet)" })
@ApiResponse({ status: 200, description: "Network status retrieved" })
async getStatus(@Query("network") network: string = "mainnet") {
const net = network === "testnet" ? "testnet" : "mainnet";
return this.networkService.fetchCurrentStatus(net);
}

@Get("status/history")
@ApiOperation({ summary: "Get network status history and uptime metrics" })
@ApiQuery({ name: "network", required: false, enum: ["mainnet", "testnet"], description: "Network to query (default: mainnet)" })
@ApiQuery({ name: "from", required: false, description: "ISO date lower bound (default: 60 minutes before to)" })
@ApiQuery({ name: "to", required: false, description: "ISO date upper bound (default: now)" })
@ApiResponse({ status: 200, description: "Network status history retrieved" })
async getHistory(
@Query("network") network: string = "mainnet",
@Query("from") from?: string,
@Query("to") to?: string,
) {
const net = network === "testnet" ? "testnet" : "mainnet";
return this.networkService.getHistory(net, from, to);
}

@Get("profiles")
@ApiOperation({ summary: "List network profiles for the authenticated user" })
@ApiResponse({ status: 200, description: "List of profiles returned" })
asyng listProfiles(@Req() req: Request) {
const userId = req.user.id;
return this.networkService.listProfiles(userId);
}

Post("profiles")
@ApiOperation({ summary: "Create a new network profile" })
@ApiResponse({ status: 201, description: "Profile created" })
async createProfile(
@Req() req: Request,
@Body() body: {
name: string;
horizon_url: string;
network_passphrase: string;
friendbot_url?: string;
is_default?: boolean;
},
) {
const userId = req.user.id;
return this.networkService.createProfile(userId, body);
}

@Put("profiles/:id")
@ApiOperation({ summary: "Update a network profile" })
@ApiResponse({ status: 200, description: "Profile updated" })
async updateProfile(
@Param("id") id: string,
@Req() req: Request,
@Body() body: {
name?: string;
horizon_url?: string;
network_passphrase?: string;
friendbot_url?: string;
is_default?: boolean;
},
) {
const userId = req.user.id;
return this.networkService.updateProfile(userId, id, body);
}

@Delete("profiles/:id")
@ApiOperation({ summary: "Delete a network profile" })
@ApiResponse({ status: 200, description: "Profile deleted" })
async deleteProfile(
@Param("id") id: string,
@Req() req: Request,
) {
const userId = req.user.id;
return this.networkService.deleteProfile(userId, id);
}

Post("profiles/:id/select")
@ApiOperation({ summary: "Select a profile and apply it as the active network configuration" })
@ApiResponse({ status: 200, description: "Profile selected and verified" })
@ApiResponse({ status: 409, description: "Passphrase mismatch warning" })
async selectProfile(
@Param("id") id: string,
@Req() req: Request,
) {
const userId = req.user.id;
return this.networkService.selectProfile(userId, id);
}

@Post("profiles/:id/default")
@ApiOperation({ summary: "Mark a profile as the default for startup" })
@ApiResponse({ status: 200, description: "Profile marked as default" })
async setDefaultProfile(
@Param("id") id: string,
@Req() req: Request,
) {
const userId = req.user.id;
return this.networkService.setDefaultProfile(userId, id);
}

@Get("profiles/:id/export")
@ApiOperation({ summary: "Export a profile as JSON" })
@ApiResponse({ status: 200, description: "Profile exported as JSON" })
async exportProfile(
@Param("id") id: string,
@Req() req: Request,
) {
const userId = req.user.id;
return this.networkService.exportProfile(userId, id);
}

@Post("profiles/import")
@ApiOperation({ summary: "Import a network profile from JSON" })
@ApiResponse({ status: 201, description: "Profile imported" })
async importProfile(
@Req() req: Request,
@Body() body: {
name: string;
horizon_url: string;
network_passphrase: string;
friendbot_url?: string;
is_default?: boolean;
},
) {
const userId = req.user.id;
return this.networkService.importProfile(userId, body);
}
}
16 changes: 11 additions & 5 deletions apps/api/src/modules/network/network.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { NetworkController } from "./network.controller";
import { NetworkService } from "./network.service";
import { NetworkProfileController } from "./network-profile.controller";
import { NetworkProfileService } from "./network-profile.service";
import { MetricsModule } from "../metrics/metrics.module";
import { NetworkSample } from "./entities/network-sample.entity";
import { NetworkProfile } from "./entities/network-profile.entity";

@Module({
imports: [MetricsModule, TypeOrmModule.forFeature([NetworkSample])],
controllers: [NetworkController],
providers: [NetworkService],
exports: [NetworkService],
imports: [
MetricsModule,
TypeOrmModule.forFeature([NetworkSample, NetworkProfile]),
],
controllers: [NetworkController, NetworkProfileController],
providers: [NetworkService, NetworkProfileService],
exports: [NetworkService, NetworkProfileService],
})
export class NetworkModule {}
export class NetworkModule {}
Loading