Compare commits

..

11 Commits

Author SHA1 Message Date
955c358138 Improve per on price update
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
.NET / build (push) Has been cancelled
2025-08-16 17:02:31 +07:00
750f6cebbb rollback contracts 2025-08-16 07:37:47 +07:00
14f5cb0971 Update logs 2025-08-16 06:32:25 +07:00
7271889bdf Fix orleans local 2025-08-16 06:21:26 +07:00
3dbd2e91ea fix github build 2025-08-16 06:10:41 +07:00
4ff2ccdae3 Add Admin roles 2025-08-16 06:06:02 +07:00
7923b38a26 update orleans 2025-08-16 05:30:12 +07:00
2861a7f469 fix a bit orleans 2025-08-16 05:23:28 +07:00
6df6061d66 Update silo 2025-08-16 05:17:04 +07:00
eeb2923646 Update silo/cluster config 2025-08-16 05:09:04 +07:00
d2975be0f5 Merge workers into API 2025-08-16 04:55:33 +07:00
47 changed files with 907 additions and 1113 deletions

View File

@@ -24,7 +24,3 @@ jobs:
run: dotnet restore ./src/Managing.Api/Managing.Api.csproj
- name: Build API
run: dotnet build --no-restore ./src/Managing.Api/Managing.Api.csproj
- name: Restore Worker dependencies
run: dotnet restore ./src/Managing.Api.Workers/Managing.Api.Workers.csproj
- name: Build Worker
run: dotnet build --no-restore ./src/Managing.Api.Workers/Managing.Api.Workers.csproj

View File

@@ -0,0 +1,114 @@
# Worker Consolidation Summary
## Overview
Successfully consolidated the separate Managing.Api.Workers project into the main Managing.Api project as background services. This eliminates Orleans conflicts and simplifies deployment while maintaining all worker functionality.
## Changes Made
### 1. ✅ Updated ApiBootstrap.cs
- **File**: `src/Managing.Bootstrap/ApiBootstrap.cs`
- **Changes**: Added all worker services from WorkersBootstrap to the main AddWorkers method
- **Workers Added**:
- PricesFifteenMinutesWorker
- PricesOneHourWorker
- PricesFourHoursWorker
- PricesOneDayWorker
- PricesFiveMinutesWorker
- SpotlightWorker
- TraderWatcher
- LeaderboardWorker
- FundingRatesWatcher
- GeneticAlgorithmWorker
- BundleBacktestWorker
- BalanceTrackingWorker
- NotifyBundleBacktestWorker
### 2. ✅ Configuration Files Updated
- **File**: `src/Managing.Api/appsettings.json`
- **File**: `src/Managing.Api/appsettings.Oda-docker.json`
- **Changes**: Added worker configuration flags to control which workers run
- **Default Values**: All workers disabled by default (set to `false`)
### 3. ✅ Deployment Scripts Updated
- **Files**:
- `scripts/build_and_run.sh`
- `scripts/docker-deploy-local.cmd`
- `scripts/docker-redeploy-oda.cmd`
- `scripts/docker-deploy-sandbox.cmd`
- **Changes**: Removed worker-specific build and deployment commands
### 4. ✅ Docker Compose Files Updated
- **Files**:
- `src/Managing.Docker/docker-compose.yml`
- `src/Managing.Docker/docker-compose.local.yml`
- **Changes**: Removed managing.api.workers service definitions
### 5. ✅ Workers Project Deprecated
- **File**: `src/Managing.Api.Workers/Program.cs`
- **Changes**: Added deprecation notice and removed Orleans configuration
- **Note**: Project kept for reference but should not be deployed
## Benefits Achieved
### ✅ Orleans Conflicts Resolved
- **Before**: Two Orleans clusters competing for same ports (11111/30000)
- **After**: Single Orleans cluster in main API
- **Impact**: No more port conflicts or cluster identity conflicts
### ✅ Simplified Architecture
- **Before**: Two separate applications to deploy and monitor
- **After**: Single application with all functionality
- **Impact**: Easier deployment, monitoring, and debugging
### ✅ Resource Efficiency
- **Before**: Duplicate service registrations and database connections
- **After**: Shared resources and connection pools
- **Impact**: Better performance and resource utilization
### ✅ Configuration Management
- **Before**: Separate configuration files for workers
- **After**: Centralized configuration with worker flags
- **Impact**: Easier to manage and control worker execution
## How to Enable/Disable Workers
Workers are controlled via configuration flags in `appsettings.json`:
```json
{
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": false,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false
}
```
Set any worker to `true` to enable it in that environment.
## Testing
### ✅ Build Verification
- Main API project builds successfully
- All worker dependencies resolved
- No compilation errors
### Next Steps for Full Verification
1. **Runtime Testing**: Start the main API and verify workers load correctly
2. **Worker Functionality**: Test that enabled workers execute as expected
3. **Orleans Integration**: Verify workers can access Orleans grains properly
4. **Configuration Testing**: Test enabling/disabling workers via config
## Migration Complete
The worker consolidation is now complete. The Managing.Api project now contains all functionality previously split between the API and Workers projects, providing a more maintainable and efficient architecture.
**Deployment**: Use only the main API deployment scripts. The Workers project should not be deployed.

View File

@@ -3,11 +3,8 @@
# Navigate to the src directory
cd ../src
# Build the managing.api image
# Build the managing.api image (now includes all workers as background services)
docker build -t managing.api -f Managing.Api/Dockerfile . --no-cache
# Build the managing.api.workers image
docker build -t managing.api.workers -f Managing.Api.Workers/Dockerfile . --no-cache
# Start up the project using docker-compose
docker compose -f Managing.Docker/docker-compose.yml -f Managing.Docker/docker-compose.local.yml up -d

View File

@@ -1,5 +1,4 @@
cd ..
cd .\src\
docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache
docker build -t managing.api.workers -f ./Managing.Api.Workers/Dockerfile . --no-cache
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.local.yml up -d

View File

@@ -1,5 +1,4 @@
cd ..
cd .\src\
docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache
docker build -t managing.api.workers -f ./Managing.Api.Workers/Dockerfile . --no-cache
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d

View File

@@ -2,21 +2,16 @@ cd ..
cd .\src\
ECHO "Stopping containers..."
docker stop sandbox-managing.api-1
docker stop sandbox-managing.api.workers-1
ECHO "Contaiters stopped"
ECHO "Removing containers..."
docker rm sandbox-managing.api-1
docker rm sandbox-managing.api.workers-1
ECHO "Containers removed"
ECHO "Removing images..."
docker rmi managing.api
docker rmi managing.api:latest
docker rmi managing.api.workers
docker rmi managing.api.workers:latest
ECHO "Images removed"
ECHO "Building images..."
docker build -t managing.api -f ./Managing.Api/Dockerfile . --no-cache
docker build -t managing.api.workers -f ./Managing.Api.Workers/Dockerfile . --no-cache
ECHO "Deploying..."
docker-compose -f ./Managing.Docker/docker-compose.yml -f ./Managing.Docker/docker-compose.sandbox.yml up -d
ECHO "Deployed"

View File

@@ -1,32 +0,0 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Workers;
using Microsoft.AspNetCore.Mvc;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Controllers;
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class WorkerController : ControllerBase
{
private readonly IWorkerService _workerService;
public WorkerController(IWorkerService workerService)
{
_workerService = workerService;
}
[HttpGet]
public async Task<ActionResult<List<Worker>>> GetWorkers()
{
var workers = await _workerService.GetWorkers();
return Ok(workers.ToList());
}
[HttpPatch]
public async Task<ActionResult> ToggleWorker(WorkerType workerType)
{
return Ok(await _workerService.ToggleWorker(workerType));
}
}

View File

@@ -1,33 +0,0 @@
# Use the official Microsoft ASP.NET Core runtime as the base image.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
# Use the official Microsoft .NET SDK image to build the code.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Managing.Api.Workers/Managing.Api.Workers.csproj", "Managing.Api.Workers/"]
COPY ["Managing.Bootstrap/Managing.Bootstrap.csproj", "Managing.Bootstrap/"]
COPY ["Managing.Infrastructure.Storage/Managing.Infrastructure.Storage.csproj", "Managing.Infrastructure.Storage/"]
COPY ["Managing.Application/Managing.Application.csproj", "Managing.Application/"]
COPY ["Managing.Common/Managing.Common.csproj", "Managing.Common/"]
COPY ["Managing.Core/Managing.Core.csproj", "Managing.Core/"]
COPY ["Managing.Application.Abstractions/Managing.Application.Abstractions.csproj", "Managing.Application.Abstractions/"]
COPY ["Managing.Domain/Managing.Domain.csproj", "Managing.Domain/"]
COPY ["Managing.Infrastructure.Messengers/Managing.Infrastructure.Messengers.csproj", "Managing.Infrastructure.Messengers/"]
COPY ["Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj", "Managing.Infrastructure.Exchanges/"]
COPY ["Managing.Infrastructure.Database/Managing.Infrastructure.Databases.csproj", "Managing.Infrastructure.Database/"]
RUN dotnet restore "Managing.Api.Workers/Managing.Api.Workers.csproj"
COPY . .
WORKDIR "/src/Managing.Api.Workers"
RUN dotnet build "Managing.Api.Workers.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Managing.Api.Workers.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY managing_cert.pfx .
ENTRYPOINT ["dotnet", "Managing.Api.Workers.dll"]

View File

@@ -1,20 +0,0 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Managing.Api.Workers.Filters
{
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
model.Enum.Clear();
Enum.GetNames(context.Type)
.ToList()
.ForEach(n => model.Enum.Add(new OpenApiString(n)));
}
}
}
}

View File

@@ -1,60 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Platforms>AnyCPU;x64</Platforms>
<UserSecretsId>3900ce93-de15-49e5-9a61-7dc2209939ca</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<DockerComposeProjectPath>..\..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="8.1.0"/>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0"/>
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0"/>
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2"/>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1"/>
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7"/>
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0"/>
<PackageReference Include="Serilog.Exceptions" Version="8.4.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0"/>
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.1"/>
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.6.1"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.1"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.1"/>
<PackageReference Include="xunit" Version="2.8.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Bootstrap\Managing.Bootstrap.csproj"/>
<ProjectReference Include="..\Managing.Aspire.ServiceDefaults\Managing.Aspire.ServiceDefaults.csproj"/>
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Oda.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Sandbox.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.SandboxLocal.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Production.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.KaiServer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -1,90 +0,0 @@
using Sentry;
using System.Text;
namespace Managing.Api.Workers.Middleware
{
public class SentryDiagnosticsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SentryDiagnosticsMiddleware> _logger;
public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger<SentryDiagnosticsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only activate for the /api/sentry-diagnostics endpoint
if (context.Request.Path.StartsWithSegments("/api/sentry-diagnostics"))
{
await HandleDiagnosticsRequest(context);
return;
}
await _next(context);
}
private async Task HandleDiagnosticsRequest(HttpContext context)
{
var response = new StringBuilder();
response.AppendLine("Sentry Diagnostics Report");
response.AppendLine("========================");
response.AppendLine($"Timestamp: {DateTime.Now}");
response.AppendLine();
// Check if Sentry is initialized
response.AppendLine("## Sentry SDK Status");
response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}");
response.AppendLine($"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
response.AppendLine();
// Send a test event
response.AppendLine("## Test Event");
try
{
var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}", SentryLevel.Info);
response.AppendLine($"Test Event ID: {id}");
response.AppendLine("Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.");
// Try to send an exception too
try
{
throw new Exception("Test exception from diagnostics middleware");
}
catch (Exception ex)
{
var exceptionId = SentrySdk.CaptureException(ex);
response.AppendLine($"Test Exception ID: {exceptionId}");
}
}
catch (Exception ex)
{
response.AppendLine($"Error sending test event: {ex.Message}");
response.AppendLine(ex.StackTrace);
}
response.AppendLine();
response.AppendLine("## Connectivity Check");
response.AppendLine("If events are not appearing in Sentry, check the following:");
response.AppendLine("1. Verify your DSN is correct in appsettings.json");
response.AppendLine("2. Ensure your network allows outbound HTTPS connections to sentry.apps.managing.live");
response.AppendLine("3. Check Sentry server logs for any ingestion issues");
response.AppendLine("4. Verify your Sentry project is correctly configured to receive events");
// Return the diagnostic information
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(response.ToString());
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class SentryDiagnosticsMiddlewareExtensions
{
public static IApplicationBuilder UseSentryDiagnostics(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
}
}
}

View File

@@ -1,217 +0,0 @@
using System.Text.Json.Serialization;
using HealthChecks.UI.Client;
using Managing.Api.Workers.Filters;
using Managing.Application.Hubs;
using Managing.Bootstrap;
using Managing.Common;
using Managing.Core.Middleawares;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Databases.PostgreSql;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.OpenApi.Models;
using NSwag;
using NSwag.Generation.Processors.Security;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Elasticsearch;
using OpenApiSecurityRequirement = Microsoft.OpenApi.Models.OpenApiSecurityRequirement;
using OpenApiSecurityScheme = NSwag.OpenApiSecurityScheme;
// Builder
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json");
var influxUrl = builder.Configuration.GetSection(Constants.Databases.InfluxDb)["Url"];
var web3ProxyUrl = builder.Configuration.GetSection("Web3Proxy")["BaseUrl"];
var postgreSqlConnectionString = builder.Configuration.GetSection("PostgreSql")["ConnectionString"];
// Initialize Sentry
SentrySdk.Init(options =>
{
// A Sentry Data Source Name (DSN) is required.
options.Dsn = builder.Configuration["Sentry:Dsn"];
// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
options.Debug = false;
// Adds request URL and headers, IP and name for users, etc.
options.SendDefaultPii = true;
// This option is recommended. It enables Sentry's "Release Health" feature.
options.AutoSessionTracking = true;
// Enabling this option is recommended for client applications only. It ensures all threads use the same global scope.
options.IsGlobalModeEnabled = false;
// Example sample rate for your transactions: captures 10% of transactions
options.TracesSampleRate = 0.1;
options.Environment = builder.Environment.EnvironmentName;
});
// Add service discovery for Aspire
builder.Services.AddServiceDiscovery();
// Configure health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"])
.AddUrlGroup(new Uri($"{influxUrl}/health"), name: "influxdb", tags: ["database"])
.AddUrlGroup(new Uri($"{web3ProxyUrl}/health"), name: "web3proxy", tags: ["api"]);
builder.WebHost.UseUrls("http://localhost:5001");
builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
{
var envName = builder.Environment.EnvironmentName.ToLower().Replace(".", "-");
var indexFormat = $"managing-worker-{envName}-" + "{0:yyyy.MM.dd}";
var yourTemplateName = "dotnetlogs";
var es = new ElasticsearchSinkOptions(new Uri(hostBuilder.Configuration["ElasticConfiguration:Uri"]))
{
IndexFormat = indexFormat.ToLower(),
AutoRegisterTemplate = true,
OverwriteTemplate = true,
TemplateName = yourTemplateName,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
TypeName = null,
BatchAction = ElasticOpType.Create,
MinimumLogEventLevel = LogEventLevel.Information,
DetectElasticsearchVersion = true,
RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway,
};
loggerConfiguration
.WriteTo.Console()
.WriteTo.Elasticsearch(es);
});
builder.Services.AddOptions();
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Constants.ThirdParty.Privy));
builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{
builder
.SetIsOriginAllowed((host) => true)
.AllowAnyOrigin()
.WithOrigins("http://localhost:3000/")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
builder.Services.AddSignalR().AddJsonProtocol();
// Add PostgreSQL DbContext for worker services
builder.Services.AddDbContext<ManagingDbContext>(options =>
{
options.UseNpgsql(postgreSqlConnectionString, npgsqlOptions =>
{
npgsqlOptions.CommandTimeout(60);
npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10),
errorCodesToAdd: null);
});
if (builder.Environment.IsDevelopment())
{
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging();
options.EnableThreadSafetyChecks();
}
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
options.EnableServiceProviderCaching();
options.LogTo(msg => Console.WriteLine(msg), LogLevel.Warning);
}, ServiceLifetime.Scoped);
builder.Services.RegisterWorkersDependencies(builder.Configuration);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(document =>
{
document.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}."
});
document.OperationProcessors.Add(
new AspNetCoreOperationSecurityScopeProcessor("JWT"));
});
builder.Services.AddSwaggerGen(options =>
{
options.SchemaFilter<EnumSchemaFilter>();
options.AddSecurityDefinition("Bearer,", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "Please insert your JWT Token into field : Bearer {your_token}",
Name = "Authorization",
Type = SecuritySchemeType.Http,
In = ParameterLocation.Header,
Scheme = "Bearer",
BearerFormat = "JWT"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
builder.WebHost.SetupDiscordBot();
// App
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseOpenApi();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Managing Workers v1");
c.RoutePrefix = string.Empty;
});
app.UseCors("CorsPolicy");
// Add Sentry diagnostics middleware (now using shared version from Core)
app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from Core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<PositionHub>("/positionhub");
endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
});
app.Run();

View File

@@ -1,19 +0,0 @@
{
"InfluxDb": {
"Url": "http://localhost:8086/",
"Token": ""
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://localhost:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,24 +0,0 @@
{
"InfluxDb": {
"Url": "http://influxdb:8086/",
"Organization": "managing-org",
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,42 +0,0 @@
{
"PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
"Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
},
"InfluxDb": {
"Url": "http://localhost:8086/",
"Organization": "managing-org",
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://localhost:9200"
},
"AllowedHosts": "*",
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerFee": false,
"WorkerPositionManager": false,
"WorkerPositionFetcher": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": true
}

View File

@@ -1,34 +0,0 @@
{
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org",
"Token": "_BtklT_aQ7GRqWG-HGILYEd8MJzxdbxxckPadzUsRofnwJBKQuXYLbCrVcLD7TrD4BlXgGAsyuqQItsOtanfBw=="
},
"Privy": {
"AppId": "cm6kkz5ke00n5ffmpwdbr05mp",
"AppSecret": "3STq1UyPJ5WHixArBcVBKecWtyR4QpgZ1uju4HHvvJH2RwtacJnvoyzuaiNC8Xibi4rQb3eeH2YtncKrMxCYiV3a"
},
"Web3Proxy": {
"BaseUrl": "http://srv-captain--managing-web3:4111"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"AllowedHosts": "*"
}

View File

@@ -1,27 +0,0 @@
{
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"Url": "http://srv-captain--influx-db:8086/",
"Organization": "managing-org",
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,24 +0,0 @@
{
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",
"Organization": "managing-org",
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",
"AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
}

View File

@@ -1,53 +0,0 @@
{
"InfluxDb": {
"Url": "http://influxdb:8086/",
"Organization": "",
"Token": ""
},
"PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"N8n": {
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951"
},
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"Web3Proxy": {
"BaseUrl": "http://localhost:4111"
},
"Sentry": {
"Dsn": "https://8fdb299b69df4f9d9b709c8d4a556608@bugcenter.apps.managing.live/2",
"MinimumEventLevel": "Error",
"SendDefaultPii": true,
"MaxBreadcrumbs": 50,
"SampleRate": 1.0,
"TracesSampleRate": 0.2,
"Debug": false
},
"Discord": {
"BotActivity": "with jobs",
"HandleUserAction": true,
"ApplicationId": "1132062339592622221",
"PublicKey": "e422f3326307788608eceba919497d3f2758cc64d20bb8a6504c695192404808",
"TokenId": "MTEzMjA2MjMzOTU5MjYyMjIyMQ.GySuNX.rU-9uIX6-yDthBjT_sbXioaJGyJva2ABNNEaj4",
"SignalChannelId": 966080506473099314,
"TradesChannelId": 998374177763491851,
"TroublesChannelId": 1015761955321040917,
"CopyTradingChannelId": 1132022887012909126,
"RequestsChannelId": 1018589494968078356,
"FundingRateChannelId": 1263566138709774336,
"LeaderboardChannelId": 1133169725237633095,
"ButtonExpirationMinutes": 10
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,80 @@
# Admin Feature Documentation
## Overview
The admin feature allows specific users to manage all bots in the system, regardless of ownership. Admin users can start, stop, delete, and modify any bot without owning the associated account.
## How It Works
Admin privileges are granted through environment variables, making it secure and environment-specific. The system checks if a user is an admin by comparing their username against a comma-separated list of admin usernames configured in the environment.
## Configuration
### Environment Variable
Set the `AdminUsers` environment variable with a comma-separated list of usernames:
```bash
AdminUsers=admin1,superuser,john.doe
```
### CapRover Configuration
In your CapRover dashboard:
1. Go to your app's settings
2. Navigate to "Environment Variables"
3. Add a new environment variable:
- Key: `AdminUsers`
- Value: `admin1,superuser,john.doe` (replace with actual admin usernames)
### Local Development
For local development, you can set this in your `appsettings.Development.json`:
```json
{
"AdminUsers": "admin1,superuser,john.doe"
}
```
## Admin Capabilities
Admin users can perform all bot operations without ownership restrictions:
- **Start/Save Bot**: Create and start bots for any account
- **Stop Bot**: Stop any running bot
- **Delete Bot**: Delete any bot
- **Restart Bot**: Restart any bot
- **Open/Close Positions**: Manually open or close positions for any bot
- **Update Configuration**: Modify any bot's configuration
- **View Bot Configuration**: Access any bot's configuration details
## Security Notes
1. **Environment-Based**: Admin users are configured via environment variables, not through the API
2. **No Privilege Escalation**: Regular users cannot grant themselves admin access
3. **Audit Logging**: All admin actions are logged with the admin user's context
4. **Case-Insensitive**: Username matching is case-insensitive for convenience
## Implementation Details
The admin feature is implemented using:
- `IAdminConfigurationService`: Checks if a user is an admin
- Updated `UserOwnsBotAccount` method: Returns true for admin users
- Dependency injection: Service is registered as a singleton
- Configuration reading: Reads from `AdminUsers` environment variable
## Example Usage
1. **Set Admin Users**:
```bash
AdminUsers=alice,bob,charlie
```
2. **Admin Operations**:
- Alice, Bob, or Charlie can now manage any bot in the system
- They can use all existing bot endpoints without ownership restrictions
- All operations are logged with their username for audit purposes
## Troubleshooting
- **Admin not working**: Check if the username exactly matches the configuration (case-insensitive)
- **No admins configured**: Check the `AdminUsers` environment variable is set
- **Multiple environments**: Each environment (dev, staging, prod) should have its own admin configuration

View File

@@ -4,6 +4,7 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
using Managing.Application.Shared;
using Managing.Common;
using Managing.Core;
using Managing.Domain.Accounts;
@@ -40,6 +41,7 @@ public class BotController : BaseController
private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IAdminConfigurationService _adminService;
/// <summary>
/// Initializes a new instance of the <see cref="BotController"/> class.
@@ -56,7 +58,7 @@ public class BotController : BaseController
public BotController(ILogger<BotController> logger, IMediator mediator, IHubContext<BotHub> hubContext,
IBacktester backtester, IBotService botService, IUserService userService,
IAccountService accountService, IMoneyManagementService moneyManagementService,
IServiceScopeFactory scopeFactory) : base(userService)
IServiceScopeFactory scopeFactory, IAdminConfigurationService adminService) : base(userService)
{
_logger = logger;
_mediator = mediator;
@@ -66,6 +68,7 @@ public class BotController : BaseController
_accountService = accountService;
_moneyManagementService = moneyManagementService;
_scopeFactory = scopeFactory;
_adminService = adminService;
}
/// <summary>
@@ -73,7 +76,7 @@ public class BotController : BaseController
/// </summary>
/// <param name="identifier">The identifier of the bot to check</param>
/// <param name="accountName">Optional account name to check when creating a new bot</param>
/// <returns>True if the user owns the account, False otherwise</returns>
/// <returns>True if the user owns the account or is admin, False otherwise</returns>
private async Task<bool> UserOwnsBotAccount(Guid identifier, string accountName = null)
{
try
@@ -82,6 +85,9 @@ public class BotController : BaseController
if (user == null)
return false;
// Admin users can access all bots
if (_adminService.IsUserAdmin(user.Name))
return true;
if (identifier != default)
{

View File

@@ -1,5 +1,6 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Shared;
using Managing.Application.Trading.Commands;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Trades;
@@ -26,6 +27,8 @@ public class TradingController : BaseController
private readonly IMoneyManagementService _moneyManagementService;
private readonly IMediator _mediator;
private readonly ILogger<TradingController> _logger;
private readonly IAdminConfigurationService _adminService;
private readonly IAccountService _accountService;
/// <summary>
/// Initializes a new instance of the <see cref="TradingController"/> class.
@@ -35,13 +38,16 @@ public class TradingController : BaseController
/// <param name="closeTradeCommandHandler">Command handler for closing trades.</param>
/// <param name="tradingService">Service for trading operations.</param>
/// <param name="mediator">Mediator for handling commands and requests.</param>
/// <param name="adminService">Service for checking admin privileges.</param>
/// <param name="accountService">Service for account operations.</param>
public TradingController(
ILogger<TradingController> logger,
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
ITradingService tradingService,
IMediator mediator, IMoneyManagementService moneyManagementService,
IUserService userService) : base(userService)
IUserService userService, IAdminConfigurationService adminService,
IAccountService accountService) : base(userService)
{
_logger = logger;
_openTradeCommandHandler = openTradeCommandHandler;
@@ -49,6 +55,8 @@ public class TradingController : BaseController
_tradingService = tradingService;
_mediator = mediator;
_moneyManagementService = moneyManagementService;
_adminService = adminService;
_accountService = accountService;
}
@@ -149,6 +157,7 @@ public class TradingController : BaseController
/// <summary>
/// Initializes a Privy wallet address for the user.
/// Only admins can initialize any address, regular users can only initialize their own addresses.
/// </summary>
/// <param name="publicAddress">The public address of the Privy wallet to initialize.</param>
/// <returns>The initialization response containing success status and transaction hashes.</returns>
@@ -162,6 +171,18 @@ public class TradingController : BaseController
try
{
var user = await GetUser();
if (user == null)
{
return Unauthorized("User not found");
}
// Check if user has permission to initialize this address
if (!await CanUserInitializeAddress(user.Name, publicAddress))
{
return Forbid("You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses.");
}
var result = await _tradingService.InitPrivyWallet(publicAddress);
return Ok(result);
}
@@ -175,4 +196,42 @@ public class TradingController : BaseController
});
}
}
/// <summary>
/// Checks if the user can initialize the given public address.
/// Admins can initialize any address, regular users can only initialize their own addresses.
/// </summary>
/// <param name="userName">The username to check</param>
/// <param name="publicAddress">The public address to initialize</param>
/// <returns>True if the user can initialize the address, false otherwise</returns>
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
{
// Admin users can initialize any address
if (_adminService.IsUserAdmin(userName))
{
_logger.LogInformation("Admin user {UserName} initializing address {Address}", userName, publicAddress);
return true;
}
try
{
// Regular users can only initialize their own addresses
// Check if the address belongs to one of the user's accounts
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
if (account?.User?.Name == userName)
{
_logger.LogInformation("User {UserName} initializing their own address {Address}", userName, publicAddress);
return true;
}
_logger.LogWarning("User {UserName} attempted to initialize address {Address} that doesn't belong to them", userName, publicAddress);
return false;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to verify ownership of address {Address} for user {UserName}", publicAddress, userName);
return false;
}
}
}

View File

@@ -53,5 +53,8 @@
<Content Update="appsettings.Production.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.KaiServer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,119 @@
# Orleans Clustering Troubleshooting Guide
## Overview
This document provides troubleshooting steps for Orleans clustering issues, particularly the "Connection attempt to endpoint failed" errors that can occur in Docker deployments.
## Common Issues and Solutions
### 1. Connection Timeout Errors
**Error**: `Connection attempt to endpoint S10.0.0.9:11111:114298801 timed out after 00:00:05`
**Cause**: Orleans silos are trying to connect to each other but the network configuration is preventing proper communication.
**Solutions**:
#### A. Environment Variables
You can disable Orleans clustering completely by setting:
```bash
DISABLE_ORLEANS_CLUSTERING=true
```
This will fall back to localhost clustering mode for testing.
#### B. Docker Network Configuration
Ensure the Docker compose file includes proper network configuration:
```yaml
services:
managing.api:
ports:
- "11111:11111" # Orleans silo port
- "30000:30000" # Orleans gateway port
hostname: managing-api
```
#### C. Database Connection Issues
If the Orleans database is unavailable, the system will automatically fall back to:
- Localhost clustering
- Memory-based grain storage
### 2. Configuration Options
#### Production Settings
In `appsettings.Production.json`:
```json
{
"RunOrleansGrains": true,
"Orleans": {
"EnableClustering": true,
"ConnectionTimeout": 60,
"MaxJoinAttempts": 3
}
}
```
#### Environment Variables
- `RUN_ORLEANS_GRAINS`: Enable/disable Orleans grains (true/false)
- `DISABLE_ORLEANS_CLUSTERING`: Force localhost clustering (true/false)
- `ORLEANS_ADVERTISED_IP`: Set specific IP address for Orleans clustering (e.g., "192.168.1.100")
- `ASPNETCORE_ENVIRONMENT`: Set environment (Production/Development/etc.)
### 3. Network Configuration Improvements
The following improvements have been made to handle Docker networking issues:
1. **Endpoint Configuration**:
- `listenOnAnyHostAddress: true` allows binding to all network interfaces
- Increased timeout values for better reliability
2. **Fallback Mechanisms**:
- Automatic fallback to localhost clustering if database unavailable
- Memory storage fallback for grain persistence
3. **Improved Timeouts**:
- Response timeout: 60 seconds
- Probe timeout: 10 seconds
- Join attempt timeout: 120 seconds
### 4. Monitoring and Debugging
#### Orleans Dashboard
Available in development mode at: `http://localhost:9999`
- Username: admin
- Password: admin
#### Health Checks
Monitor application health at:
- `/health` - Full health check
- `/alive` - Basic liveness check
### 5. Emergency Procedures
If Orleans clustering is causing deployment issues:
1. **Immediate Fix**: Set environment variable `DISABLE_ORLEANS_CLUSTERING=true`
2. **Restart Services**: Restart the managing.api container
3. **Check Logs**: Monitor for connection timeout errors
4. **Database Check**: Verify PostgreSQL Orleans database connectivity
### 6. Database Requirements
Orleans requires these PostgreSQL databases:
- Main application database (from `PostgreSql:ConnectionString`)
- Orleans clustering database (from `PostgreSql:Orleans`)
If either is unavailable, the system will gracefully degrade functionality.
## Testing the Fix
1. Deploy with the updated configuration
2. Monitor logs for Orleans connection errors
3. Verify grain functionality through the dashboard (development) or API endpoints
4. Test failover scenarios by temporarily disabling database connectivity
## Related Files
- `src/Managing.Bootstrap/ApiBootstrap.cs` - Orleans configuration
- `src/Managing.Docker/docker-compose.yml` - Docker networking
- `src/Managing.Api/appsettings.*.json` - Environment-specific settings

View File

@@ -0,0 +1,125 @@
# TradingController Security Enhancement
## Overview
The `InitPrivyWallet` endpoint in `TradingController` has been enhanced with admin role security. This ensures that only authorized users can initialize wallet addresses.
## Security Rules
### For Regular Users
- Can **only** initialize wallet addresses that they own
- The system verifies ownership by checking if the provided public address exists in one of the user's accounts
- If a user tries to initialize an address they don't own, they receive a `403 Forbidden` response
### For Admin Users
- Can initialize **any** wallet address in the system
- Admin status is determined by the `AdminUsers` environment variable
- All admin actions are logged for audit purposes
## Implementation Details
### Authentication Flow
1. **User Authentication**: Endpoint requires valid JWT token
2. **Admin Check**: System checks if user is in the admin list via `IAdminConfigurationService`
3. **Ownership Verification**: For non-admin users, verifies address ownership via `IAccountService.GetAccountByKey()`
4. **Action Logging**: All operations are logged with user context
### Security Validation
```csharp
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
{
// Admin users can initialize any address
if (_adminService.IsUserAdmin(userName))
{
return true;
}
// Regular users can only initialize their own addresses
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
return account?.User?.Name == userName;
}
```
## Error Responses
### 401 Unauthorized
- Missing or invalid JWT token
- User not found in system
### 403 Forbidden
- Non-admin user trying to initialize address they don't own
- Message: "You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses."
### 400 Bad Request
- Empty or null public address provided
### 500 Internal Server Error
- System error during wallet initialization
- Database connectivity issues
- External service failures
## Admin Configuration
Set admin users via environment variable:
```bash
AdminUsers=admin1,admin2,superuser
```
## Audit Logging
All operations are logged with appropriate context:
**Admin Operations:**
```
Admin user {UserName} initializing address {Address}
```
**User Operations:**
```
User {UserName} initializing their own address {Address}
```
**Security Violations:**
```
User {UserName} attempted to initialize address {Address} that doesn't belong to them
```
## Usage Examples
### Regular User - Own Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {user-jwt-token}
Content-Type: application/json
"0x1234567890123456789012345678901234567890"
```
**Result**: ✅ Success (if address belongs to user)
### Regular User - Other's Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {user-jwt-token}
Content-Type: application/json
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
```
**Result**: ❌ 403 Forbidden
### Admin User - Any Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {admin-jwt-token}
Content-Type: application/json
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
```
**Result**: ✅ Success (admin can initialize any address)
## Security Benefits
1. **Prevents Unauthorized Access**: Users cannot initialize wallets they don't own
2. **Admin Oversight**: Admins can manage any wallet for system administration
3. **Audit Trail**: All actions are logged for compliance and security monitoring
4. **Clear Authorization**: Explicit permission checks with meaningful error messages
5. **Secure Configuration**: Admin privileges configured via environment variables, not API calls

View File

@@ -31,5 +31,18 @@
"ButtonExpirationMinutes": 2
},
"RunOrleansGrains": true,
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": true,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false,
"AllowedHosts": "*"
}

View File

@@ -36,9 +36,23 @@
},
"RunOrleansGrains": true,
"AllowedHosts": "*",
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
"KAIGEN_CREDITS_ENABLED": false,
"WorkerBotManager": true,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false,
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
"KAIGEN_CREDITS_ENABLED": false
"WorkerPricesFifteenMinutes": true,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerFee": false,
"WorkerPositionManager": false,
"WorkerPositionFetcher": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": false
}

View File

@@ -29,6 +29,11 @@
},
"RunOrleansGrains": true,
"DeploymentMode": false,
"Orleans": {
"EnableClustering": true,
"ConnectionTimeout": 60,
"MaxJoinAttempts": 3
},
"AllowedHosts": "*",
"WorkerBotManager": true,
"WorkerBalancesTracking": true,

View File

@@ -68,5 +68,19 @@
},
"RunOrleansGrains": true,
"DeploymentMode": false,
"WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": false,
"WorkerPricesFourHours": false,
"WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false,
"WorkerSpotlight": false,
"WorkerTraderWatcher": false,
"WorkerLeaderboard": false,
"WorkerFundingRatesWatcher": false,
"WorkerGeneticAlgorithm": false,
"WorkerBundleBacktest": false,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false,
"AdminUsers": "",
"AllowedHosts": "*"
}

View File

@@ -24,5 +24,5 @@ public interface ICandleRepository
Enums.TradingExchanges exchange,
Enums.Timeframe timeframe,
DateTime start);
void InsertCandle(Candle candle);
Task InsertCandle(Candle candle);
}

View File

@@ -97,11 +97,11 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
if (botStatus == BotStatus.Running && _tradingBot == null)
{
// Now, we can proceed with resuming the bot.
await ResumeBotInternalAsync();
await ResumeBotInternalAsync(botStatus);
}
}
private async Task ResumeBotInternalAsync()
private async Task ResumeBotInternalAsync(BotStatus previousStatus)
{
// Idempotency check
if (_tradingBot != null)
@@ -113,7 +113,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
{
// Create and initialize trading bot instance
_tradingBot = CreateTradingBotInstance(_state.State.Config);
await _tradingBot.Start();
await _tradingBot.Start(previousStatus);
// Set startup time when bot actually starts running
_state.State.StartupTime = DateTime.UtcNow;
@@ -155,7 +155,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
try
{
// Resume the bot - this handles registry status update internally
await ResumeBotInternalAsync();
await ResumeBotInternalAsync(status);
_logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey());
}
catch (Exception ex)

View File

@@ -54,7 +54,7 @@ public class TradingBotBase : ITradingBot
PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(config.Timeframe);
}
public async Task Start()
public async Task Start(BotStatus previousStatus)
{
if (!Config.IsForBacktest)
{
@@ -77,27 +77,37 @@ public class TradingBotBase : ITradingBot
// await CancelAllOrders();
// Send startup message only for fresh starts (not reboots)
if (!true)
switch (previousStatus)
{
var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList();
var startupMessage = $"🚀 **Bot Started Successfully!**\n\n" +
$"📊 **Trading Setup:**\n" +
$"🎯 Ticker: `{Config.Ticker}`\n" +
$" Timeframe: `{Config.Timeframe}`\n" +
$"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
$"💰 Balance: `${Config.BotTradingBalance:F2}`\n" +
$"👀 Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" +
$"📈 **Active Indicators:** `{string.Join(", ", indicatorNames)}`\n\n" +
$"✅ Ready to monitor signals and execute trades!\n" +
$"📢 I'll notify you when signals are triggered.";
case BotStatus.Saved:
var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList();
var startupMessage = $"🚀 **Bot Started Successfully!**\n\n" +
$"📊 **Trading Setup:**\n" +
$"🎯 Ticker: `{Config.Ticker}`\n" +
$"⏰ Timeframe: `{Config.Timeframe}`\n" +
$"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
$"💰 Balance: `${Config.BotTradingBalance:F2}`\n" +
$"👀 Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" +
$"📈 **Active Indicators:** `{string.Join(", ", indicatorNames)}`\n\n" +
$"✅ Ready to monitor signals and execute trades!\n" +
$"📢 I'll notify you when signals are triggered.";
await LogInformation(startupMessage);
}
else
{
await LogInformation($"🔄 **Bot Restarted**\n" +
$"📊 Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" +
$"✅ Ready to continue trading");
await LogInformation(startupMessage);
break;
case BotStatus.Running:
return;
case BotStatus.Stopped:
// If status was Stopped we log a message to inform the user that the bot is restarting
await LogInformation($"🔄 **Bot Restarted**\n" +
$"📊 Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" +
$"✅ Ready to continue trading");
break;
default:
// Handle any other status if needed
break;
}
}
catch (Exception ex)
@@ -160,10 +170,8 @@ public class TradingBotBase : ITradingBot
{
ExecutionCount++;
Logger.LogInformation($"Signals : {Signals.Count}");
Logger.LogInformation($"ExecutionCount : {ExecutionCount}");
Logger.LogInformation($"Positions : {Positions.Count}");
Logger.LogInformation("__________________________________________________");
Logger.LogInformation("Bot Status - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}",
DateTime.UtcNow, LastCandle.Date, Signals.Count, ExecutionCount, Positions.Count);
}
}
@@ -377,26 +385,30 @@ public class TradingBotBase : ITradingBot
if (position.Status.Equals(PositionStatus.New))
{
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Filled);
// Notify platform summary about the executed trade
try
{
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory =>
{
var platformGrain = grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
var tradeExecutedEvent = new TradeExecutedEvent
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory,
async grainFactory =>
{
TradeId = position.Identifier,
Ticker = position.Ticker,
Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage
};
await platformGrain.OnTradeExecutedAsync(tradeExecutedEvent);
});
var platformGrain =
grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
var tradeExecutedEvent = new TradeExecutedEvent
{
TradeId = position.Identifier,
Ticker = position.Ticker,
Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage
};
await platformGrain.OnTradeExecutedAsync(tradeExecutedEvent);
});
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to notify platform summary about trade execution for position {PositionId}", position.Identifier);
Logger.LogWarning(ex,
"Failed to notify platform summary about trade execution for position {PositionId}",
position.Identifier);
}
}
@@ -979,7 +991,8 @@ public class TradingBotBase : ITradingBot
_scopeFactory, async (exchangeService, accountService, tradingService) =>
{
closedPosition =
await new ClosePositionCommandHandler(exchangeService, accountService, tradingService, _scopeFactory)
await new ClosePositionCommandHandler(exchangeService, accountService, tradingService,
_scopeFactory)
.Handle(command);
});
@@ -1024,35 +1037,39 @@ public class TradingBotBase : ITradingBot
if (currentCandle != null)
{
List<Candle> recentCandles = null;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, async exchangeService =>
{
recentCandles = Config.IsForBacktest
? (LastCandle != null ? new List<Candle>() { LastCandle } : new List<Candle>())
: (await exchangeService.GetCandlesInflux(TradingExchanges.Evm, Config.Ticker,
DateTime.UtcNow.AddHours(-4), Config.Timeframe)).ToList();
});
// Check if we have any candles before proceeding
if (recentCandles == null || !recentCandles.Any())
{
await LogWarning($"No recent candles available for position {position.Identifier}. Using current candle data instead.");
// Fallback to current candle if available
if (currentCandle != null)
List<Candle> recentCandles = null;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, async exchangeService =>
{
recentCandles = new List<Candle> { currentCandle };
}
else
{
await LogWarning($"No candle data available for position {position.Identifier}. Cannot determine stop loss/take profit hit.");
Logger.LogError("No candle data available for position {PositionId}. Cannot determine stop loss/take profit hit.", position.Identifier);
return;
}
}
recentCandles = Config.IsForBacktest
? (LastCandle != null ? new List<Candle>() { LastCandle } : new List<Candle>())
: (await exchangeService.GetCandlesInflux(TradingExchanges.Evm, Config.Ticker,
DateTime.UtcNow.AddHours(-4), Config.Timeframe)).ToList();
});
var minPriceRecent = recentCandles.Min(c => c.Low);
var maxPriceRecent = recentCandles.Max(c => c.High);
// Check if we have any candles before proceeding
if (recentCandles == null || !recentCandles.Any())
{
await LogWarning(
$"No recent candles available for position {position.Identifier}. Using current candle data instead.");
// Fallback to current candle if available
if (currentCandle != null)
{
recentCandles = new List<Candle> { currentCandle };
}
else
{
await LogWarning(
$"No candle data available for position {position.Identifier}. Cannot determine stop loss/take profit hit.");
Logger.LogError(
"No candle data available for position {PositionId}. Cannot determine stop loss/take profit hit.",
position.Identifier);
return;
}
}
var minPriceRecent = recentCandles.Min(c => c.Low);
var maxPriceRecent = recentCandles.Max(c => c.High);
bool wasStopLossHit = false;
bool wasTakeProfitHit = false;

View File

@@ -0,0 +1,55 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Shared;
public interface IAdminConfigurationService
{
bool IsUserAdmin(string userName);
List<string> GetAdminUserNames();
}
public class AdminConfigurationService : IAdminConfigurationService
{
private readonly IConfiguration _configuration;
private readonly ILogger<AdminConfigurationService> _logger;
public AdminConfigurationService(IConfiguration configuration, ILogger<AdminConfigurationService> logger)
{
_configuration = configuration;
_logger = logger;
}
public bool IsUserAdmin(string userName)
{
if (string.IsNullOrEmpty(userName))
{
return false;
}
var adminUserNames = GetAdminUserNames();
var isAdmin = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase);
if (isAdmin)
{
_logger.LogInformation("User {UserName} has admin privileges", userName);
}
return isAdmin;
}
public List<string> GetAdminUserNames()
{
var adminUsers = _configuration["AdminUsers"];
if (string.IsNullOrEmpty(adminUsers))
{
_logger.LogDebug("No admin users configured. Set AdminUsers environment variable.");
return new List<string>();
}
return adminUsers.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim())
.Where(u => !string.IsNullOrEmpty(u))
.ToList();
}
}

View File

@@ -39,39 +39,35 @@ public class PricesService : IPricesService
throw new Exception($"Enable to found account for exchange {exchange}");
var lastCandles =
await _candleRepository.GetCandles(exchange, ticker, timeframe, DateTime.UtcNow.AddDays(-2));
await _candleRepository.GetCandles(exchange, ticker, timeframe, DateTime.UtcNow.AddDays(-30), limit: 1)
.ConfigureAwait(false);
var lastCandle = lastCandles.LastOrDefault();
var startDate = lastCandle != null ? lastCandle.Date : new DateTime(2017, 1, 1);
List<Candle> newCandles;
if (!lastCandles.Any())
{
newCandles = await _exchangeService.GetCandles(account, ticker, startDate, timeframe, true);
}
else
{
newCandles = await _exchangeService.GetCandles(account, ticker, startDate, timeframe, false);
}
List<Candle> newCandles =
await _exchangeService.GetCandles(account, ticker, startDate, timeframe, true)
.ConfigureAwait(false);
var candlesToInsert = lastCandle == null
? newCandles
: newCandles.Where(c => c.Date > lastCandle.Date);
var candles = !lastCandles.Any() ? newCandles : newCandles.Where(c => c.Date > lastCandle?.Date);
var candlesInserted = 0;
foreach (var newCandle in candles)
foreach (var newCandle in candlesToInsert)
{
if (lastCandle == null || newCandle.Date > lastCandle.Date)
{
_candleRepository.InsertCandle(newCandle);
candlesInserted++;
}
await _candleRepository.InsertCandle(newCandle).ConfigureAwait(false);
candlesInserted++;
}
if (candlesInserted > 0)
_logger.LogInformation($"[{exchange}][{ticker}][{timeframe}] New candles inserted : {candlesInserted}");
_logger.LogInformation("[{exchange}][{ticker}][{timeframe}] New candles inserted : {candlesInserted}", exchange, ticker, timeframe, candlesInserted);
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
_logger.LogError($"[{exchange}][{ticker}][{timeframe}] Error : {ex.Message} | {ex.StackTrace}");
_logger.LogError("[{exchange}][{ticker}][{timeframe}] Error : {ex.Message} | {ex.StackTrace}", exchange, ticker, timeframe, ex.Message, ex.StackTrace);
}
}
}

View File

@@ -14,7 +14,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Api.Workers\Managing.Api.Workers.csproj"/>
<ProjectReference Include="..\Managing.Api\Managing.Api.csproj"/>
</ItemGroup>

View File

@@ -1,13 +1,13 @@
using Projects;
var builder = DistributedApplication.CreateBuilder(args);
// Add API projects
var managingApi = builder.AddProject<Projects.Managing_Api>("managing-api");
var workersApi = builder.AddProject<Projects.Managing_Api_Workers>("worker-api");
var managingApi = builder.AddProject<Managing_Api>("managing-api");
// No need to add containers - your APIs will use their existing connection strings
// from their respective appsettings.json files
// Connect services to resources
workersApi.WithReference(managingApi);
builder.Build().Run();

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using System.Net;
using System.Reflection;
using Discord.Commands;
using Discord.WebSocket;
using FluentValidation;
@@ -87,21 +88,33 @@ public static class ApiBootstrap
runOrleansGrains = runOrleansGrainsFromEnv;
}
// Allow disabling Orleans clustering entirely in case of issues
var disableOrleansClusteringEnv = Environment.GetEnvironmentVariable("DISABLE_ORLEANS_CLUSTERING");
var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) &&
bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled;
var postgreSqlConnectionString = configuration.GetSection("PostgreSql")["Orleans"];
return hostBuilder.UseOrleans(siloBuilder =>
{
// Configure clustering with improved networking
siloBuilder
.UseAdoNetClustering(options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
});
// Configure clustering with improved networking or use localhost clustering if disabled
if (!disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString))
{
siloBuilder
.UseAdoNetClustering(options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
});
}
else
{
// Fallback to localhost clustering for testing or when database is unavailable
siloBuilder.UseLocalhostClustering();
}
// Conditionally configure reminder service based on flag
if (runOrleansGrains)
if (runOrleansGrains && !disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString))
{
siloBuilder.UseAdoNetReminderService(options =>
{
@@ -110,9 +123,28 @@ public static class ApiBootstrap
});
}
// Configure networking for better silo communication
// Configure networking - simplified for Docker compatibility
if (disableOrleansClustering)
{
// Use localhost clustering when clustering is disabled
siloBuilder.ConfigureEndpoints(IPAddress.Loopback, 11111, 30000);
}
else
{
// Use localhost for development, proper hostname for production
if (isProduction)
{
// In production, use all interfaces
siloBuilder.ConfigureEndpoints(IPAddress.Any, 11111, 30000);
}
else
{
// In development, use localhost
siloBuilder.ConfigureEndpoints(IPAddress.Loopback, 11111, 30000);
}
}
siloBuilder
.ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000)
.Configure<ClusterOptions>(options =>
{
// Configure cluster options with unique identifiers
@@ -121,8 +153,26 @@ public static class ApiBootstrap
})
.Configure<MessagingOptions>(options =>
{
// Configure messaging for better reliability
options.ResponseTimeout = TimeSpan.FromSeconds(30);
// Configure messaging for better reliability with increased timeouts
options.ResponseTimeout = TimeSpan.FromSeconds(60);
options.DropExpiredMessages = true;
})
.Configure<ClusterMembershipOptions>(options =>
{
// Configure cluster membership for better resilience
options.EnableIndirectProbes = true;
options.ProbeTimeout = TimeSpan.FromSeconds(10);
options.IAmAliveTablePublishTimeout = TimeSpan.FromSeconds(30);
options.MaxJoinAttemptTime = TimeSpan.FromSeconds(120);
// Improved settings for development environments with stale members
options.DefunctSiloCleanupPeriod = TimeSpan.FromMinutes(1);
options.DefunctSiloExpiration = TimeSpan.FromMinutes(2);
})
.Configure<GatewayOptions>(options =>
{
// Configure gateway with improved timeouts
options.GatewayListRefreshPeriod = TimeSpan.FromSeconds(60);
});
// Conditionally configure grain execution based on flag
@@ -165,27 +215,42 @@ public static class ApiBootstrap
});
}
// Configure grain storage - use ADO.NET for production or memory for fallback
if (!disableOrleansClustering && !string.IsNullOrEmpty(postgreSqlConnectionString))
{
siloBuilder
.AddAdoNetGrainStorage("bot-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("registry-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("agent-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("platform-summary-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
});
}
else
{
// Fallback to memory storage when database is unavailable
siloBuilder
.AddMemoryGrainStorage("bot-store")
.AddMemoryGrainStorage("registry-store")
.AddMemoryGrainStorage("agent-store")
.AddMemoryGrainStorage("platform-summary-store");
}
siloBuilder
.AddAdoNetGrainStorage("bot-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("registry-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("agent-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("platform-summary-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.ConfigureServices(services =>
{
// Register existing services for Orleans DI
@@ -215,6 +280,7 @@ public static class ApiBootstrap
services.AddScoped<IWorkerService, WorkerService>();
services.AddScoped<ISynthPredictionService, SynthPredictionService>();
services.AddScoped<ISynthApiClient, SynthApiClient>();
services.AddScoped<IPricesService, PricesService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
@@ -234,6 +300,9 @@ public static class ApiBootstrap
services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IDiscordService, DiscordService>();
// Admin services
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
return services;
}
@@ -292,6 +361,7 @@ public static class ApiBootstrap
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
{
// Balance Workers
if (configuration.GetValue<bool>("WorkerBalancesTracking", false))
{
services.AddHostedService<BalanceTrackingWorker>();
@@ -302,6 +372,63 @@ public static class ApiBootstrap
services.AddHostedService<NotifyBundleBacktestWorker>();
}
// Price Workers
if (configuration.GetValue<bool>("WorkerPricesFifteenMinutes", false))
{
services.AddHostedService<PricesFifteenMinutesWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesOneHour", false))
{
services.AddHostedService<PricesOneHourWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesFourHours", false))
{
services.AddHostedService<PricesFourHoursWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesOneDay", false))
{
services.AddHostedService<PricesOneDayWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesFiveMinutes", false))
{
services.AddHostedService<PricesFiveMinutesWorker>();
}
// Other Workers
if (configuration.GetValue<bool>("WorkerSpotlight", false))
{
services.AddHostedService<SpotlightWorker>();
}
if (configuration.GetValue<bool>("WorkerTraderWatcher", false))
{
services.AddHostedService<TraderWatcher>();
}
if (configuration.GetValue<bool>("WorkerLeaderboard", false))
{
services.AddHostedService<LeaderboardWorker>();
}
if (configuration.GetValue<bool>("WorkerFundingRatesWatcher", false))
{
services.AddHostedService<FundingRatesWatcher>();
}
if (configuration.GetValue<bool>("WorkerGeneticAlgorithm", false))
{
services.AddHostedService<GeneticAlgorithmWorker>();
}
if (configuration.GetValue<bool>("WorkerBundleBacktest", false))
{
services.AddHostedService<BundleBacktestWorker>();
}
return services;
}

View File

@@ -1,215 +0,0 @@
using Managing.Application;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Accounts;
using Managing.Application.Backtests;
using Managing.Application.ManageBot;
using Managing.Application.MoneyManagements;
using Managing.Application.Scenarios;
using Managing.Application.Shared;
using Managing.Application.Synth;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Application.Trading.Handlers;
using Managing.Application.Users;
using Managing.Application.Workers;
using Managing.Domain.Trades;
using Managing.Infrastructure.Databases;
using Managing.Infrastructure.Databases.InfluxDb;
using Managing.Infrastructure.Databases.InfluxDb.Abstractions;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Databases.PostgreSql;
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
using Managing.Infrastructure.Evm;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models.Privy;
using Managing.Infrastructure.Evm.Services;
using Managing.Infrastructure.Evm.Subgraphs;
using Managing.Infrastructure.Exchanges;
using Managing.Infrastructure.Exchanges.Abstractions;
using Managing.Infrastructure.Exchanges.Exchanges;
using Managing.Infrastructure.Messengers.Discord;
using Managing.Infrastructure.Storage;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Managing.Bootstrap;
public static class WorkersBootstrap
{
public static IServiceCollection RegisterWorkersDependencies(this IServiceCollection services,
IConfiguration configuration)
{
return services
.AddApplication()
.AddInfrastructure(configuration)
.AddWorkers(configuration);
}
private static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<ITradingService, TradingService>();
services.AddScoped<IScenarioService, ScenarioService>();
services.AddScoped<IMoneyManagementService, MoneyManagementService>();
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IStatisticService, StatisticService>();
services.AddScoped<ISettingsService, SettingsService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IGeneticService, GeneticService>();
services.AddScoped<IBotService, BotService>();
services.AddScoped<IWorkerService, WorkerService>();
services.AddScoped<ISynthPredictionService, SynthPredictionService>();
services.AddScoped<ISynthApiClient, SynthApiClient>();
services.AddScoped<IPricesService, PricesService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
// Processors
services.AddTransient<IBacktester, Backtester>();
services.AddTransient<IExchangeProcessor, EvmProcessor>();
services.AddTransient<ITradaoService, TradaoService>();
services.AddTransient<IExchangeService, ExchangeService>();
services.AddTransient<IExchangeStream, ExchangeStream>();
services.AddTransient<IPrivyService, PrivyService>();
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
services.AddTransient<IWebhookService, WebhookService>();
services.AddTransient<IKaigenService, KaigenService>();
services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IDiscordService, DiscordService>();
return services;
}
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
{
// Price Workers
if (configuration.GetValue<bool>("WorkerPricesFifteenMinutes", false))
{
services.AddHostedService<PricesFifteenMinutesWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesOneHour", false))
{
services.AddHostedService<PricesOneHourWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesFourHours", false))
{
services.AddHostedService<PricesFourHoursWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesOneDay", false))
{
services.AddHostedService<PricesOneDayWorker>();
}
if (configuration.GetValue<bool>("WorkerPricesFiveMinutes", false))
{
services.AddHostedService<PricesFiveMinutesWorker>();
}
if (configuration.GetValue<bool>("WorkerSpotlight", false))
{
services.AddHostedService<SpotlightWorker>();
}
if (configuration.GetValue<bool>("WorkerTraderWatcher", false))
{
services.AddHostedService<TraderWatcher>();
}
if (configuration.GetValue<bool>("WorkerLeaderboard", false))
{
services.AddHostedService<LeaderboardWorker>();
}
if (configuration.GetValue<bool>("WorkerFundingRatesWatcher", false))
{
services.AddHostedService<FundingRatesWatcher>();
}
if (configuration.GetValue<bool>("WorkerGeneticAlgorithm", false))
{
services.AddHostedService<GeneticAlgorithmWorker>();
}
if (configuration.GetValue<bool>("WorkerBundleBacktest", false))
{
services.AddHostedService<BundleBacktestWorker>();
}
return services;
}
private static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
// Database
services.AddSingleton<IPostgreSqlSettings>(sp =>
sp.GetRequiredService<IOptions<PostgreSqlSettings>>().Value);
services.AddSingleton<IInfluxDbSettings>(sp =>
sp.GetRequiredService<IOptions<InfluxDbSettings>>().Value);
services.AddTransient<IInfluxDbRepository, InfluxDbRepository>();
services.AddSingleton<IPrivySettings>(sp =>
sp.GetRequiredService<IOptions<PrivySettings>>().Value);
// Evm
services.AddUniswapV2();
services.AddGbcFeed();
services.AddChainlink();
services.AddChainlinkGmx();
services.AddSingleton<IEvmManager, EvmManager>();
// Repositories
services.AddTransient<ICandleRepository, CandleRepository>();
services.AddTransient<IAgentBalanceRepository, AgentBalanceRepository>();
services.AddTransient<IWorkerRepository, PostgreSqlWorkerRepository>();
services.AddTransient<IStatisticRepository, PostgreSqlStatisticRepository>();
services.AddTransient<ICandleRepository, CandleRepository>();
services.AddTransient<IAccountRepository, PostgreSqlAccountRepository>();
services.AddTransient<ISettingsRepository, PostgreSqlSettingsRepository>();
services.AddTransient<ITradingRepository, PostgreSqlTradingRepository>();
services.AddTransient<IBacktestRepository, PostgreSqlBacktestRepository>();
services.AddTransient<IBotRepository, PostgreSqlBotRepository>();
services.AddTransient<IUserRepository, PostgreSqlUserRepository>();
services.AddTransient<ISynthRepository, PostgreSqlSynthRepository>();
services.AddTransient<IGeneticRepository, PostgreSqlGeneticRepository>();
// Cache
services.AddDistributedMemoryCache();
services.AddTransient<ICacheService, CacheService>();
services.AddTransient<ITaskCache, TaskCache>();
// Processors
services.AddTransient<IExchangeProcessor, EvmProcessor>();
// Web Clients
services.AddTransient<ITradaoService, TradaoService>();
services.AddTransient<IExchangeService, ExchangeService>();
services.AddSingleton<IPrivyService, PrivyService>();
services.AddSingleton<ISynthApiClient, SynthApiClient>();
// Web3Proxy Configuration
services.Configure<Web3ProxySettings>(configuration.GetSection("Web3Proxy"));
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
// Http Clients
services.AddHttpClient();
// Messengers
services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IDiscordService, DiscordService>();
services.AddSingleton<IWebhookService, WebhookService>();
return services;
}
}

View File

@@ -17,29 +17,7 @@ services:
- /Users/oda/ASP.NET/Https:/root/.aspnet/https:ro
- /Users/oda/Microsoft/UserSecrets:/root/.microsoft/usersecrets/$USER_SECRETS_ID
depends_on:
- managingdb
managing.api.workers:
environment:
- ASPNETCORE_ENVIRONMENT=Oda-docker
- ASPNETCORE_URLS=https://+:443;http://+:80
- ASPNETCORE_Kestrel__Certificates__Default__Password=!Managing94
- ASPNETCORE_Kestrel__Certificates__Default__Path=/app/managing_cert.pfx
ports:
- "81:80"
- "444:443"
volumes:
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
- /Users/oda/ASP.NET/Https:/root/.aspnet/https:ro
depends_on:
- managingdb
managingdb:
restart: always
volumes:
- mongodata:/data/db
ports:
- "27017:27017"
- postgres
postgres:
image: postgres:17.5

View File

@@ -1,11 +1,6 @@
version: '3.4'
services:
managingdb:
image: mongo
networks:
- managing-network
managing.api:
image: ${DOCKER_REGISTRY-}managingapi
build:
@@ -13,14 +8,13 @@ services:
dockerfile: Managing.Api/Dockerfile
networks:
- managing-network
managing.api.workers:
image: ${DOCKER_REGISTRY-}managingapiworkers
build:
context: ../.
dockerfile: Managing.Api.Workers/Dockerfile
networks:
- managing-network
ports:
- "11111:11111" # Orleans silo port
- "30000:30000" # Orleans gateway port
environment:
- ASPNETCORE_ENVIRONMENT=Production
- RUN_ORLEANS_GRAINS=true
hostname: managing-api
influxdb:
image: influxdb:latest
@@ -33,10 +27,10 @@ services:
- managing-network
volumes:
mongodata: {}
influxdata: {}
postgresdata: {}
networks:
managing-network:
name: managing-network
name: managing-network
driver: bridge

View File

@@ -8,4 +8,5 @@ public interface IInfluxDbRepository
Task<T> QueryAsync<T>(Func<QueryApi, Task<T>> action);
void Write(Action<WriteApi> action);
Task WriteAsync(Func<WriteApi, Task> action);
}

View File

@@ -111,9 +111,9 @@ public class CandleRepository : ICandleRepository
return results;
}
public void InsertCandle(Candle candle)
public async Task InsertCandle(Candle candle)
{
_influxDbRepository.Write(write =>
await _influxDbRepository.WriteAsync(write =>
{
PriceDto price = PriceHelpers.Map(candle);
write.WriteMeasurement(
@@ -121,6 +121,7 @@ public class CandleRepository : ICandleRepository
WritePrecision.Ns,
_priceBucket,
_influxDbRepository.Organization);
return Task.CompletedTask;
});
}

View File

@@ -23,6 +23,13 @@ public class InfluxDbRepository : IInfluxDbRepository
action(write);
}
public Task WriteAsync(Func<WriteApi, Task> action)
{
// Get write API asynchronously
using var client = new InfluxDBClient(_url, _token);
using var write = client.GetWriteApi();
return action(write);
}
public async Task<T> QueryAsync<T>(Func<QueryApi, Task<T>> action)
{
using var client = InfluxDBClientFactory.Create(_url, _token);

View File

@@ -1,6 +1,7 @@
import {type Address, zeroAddress} from "viem";
import {ARBITRUM, AVALANCHE, AVALANCHE_FUJI} from "./chains.js";
export const CONTRACTS = {
[ARBITRUM]: {
// arbitrum mainnet
@@ -50,29 +51,23 @@ export const CONTRACTS = {
DataStore: "0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8",
EventEmitter: "0xC8ee91A54287DB53897056e12D9819156D3822Fb",
SubaccountRouter: "0xa329221a77BE08485f59310b873b14815c82E10D",
ExchangeRouter: "0x602b805EedddBbD9ddff44A7dcBD46cb07849685",
ExchangeRouter: "0x5ac4e27341e4cccb3e5fd62f9e62db2adf43dd57",
DepositVault: "0xF89e77e8Dc11691C9e8757e84aaFbCD8A67d7A55",
WithdrawalVault: "0x0628D46b5D145f183AdB6Ef1f2c97eD1C4701C55",
OrderVault: "0x31eF83a530Fde1B38EE9A18093A333D8Bbbc40D5",
ShiftVault: "0xfe99609C4AA83ff6816b64563Bdffd7fa68753Ab",
SyntheticsReader: "0xcF2845Ab3866842A6b51Fb6a551b92dF58333574",
SyntheticsReader: "0x0537C767cDAC0726c76Bb89e92904fe28fd02fE1",
SyntheticsRouter: "0x7452c558d45f8afC8c83dAe62C3f8A5BE19c71f6",
GlvReader: "0x6a9505D0B44cFA863d9281EA5B0b34cB36243b45",
GlvRouter: "0x994c598e3b0661bb805d53c6fa6b4504b23b68dd",
GlvVault: "0x393053B58f9678C9c28c2cE941fF6cac49C3F8f9",
GelatoRelayRouter: "0x9EB239eDf4c6f4c4fC9d30ea2017F8716d049C8D",
SubaccountGelatoRelayRouter: "0x5F345B765d5856bC0843cEE8bE234b575eC77DBC",
ExternalHandler: "0x389CEf541397e872dC04421f166B5Bc2E0b374a5",
OpenOceanRouter: "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64",
ChainlinkPriceFeedProvider: "0x527FB0bCfF63C47761039bB386cFE181A92a4701",
Multicall: "0x842ec2c7d803033edf55e478f461fc547bc54eb2",
ArbitrumNodeInterface: "0x00000000000000000000000000000000000000C8",
ClaimHandler: "0xCF2b097517EEBD6c36756A82844D2ec21Ee4C025",
},
[AVALANCHE]: {
// avalanche
@@ -123,29 +118,23 @@ export const CONTRACTS = {
DataStore: "0x2F0b22339414ADeD7D5F06f9D604c7fF5b2fe3f6",
EventEmitter: "0xDb17B211c34240B014ab6d61d4A31FA0C0e20c26",
SubaccountRouter: "0x5aEb6AD978f59e220aA9099e09574e1c5E03AafD",
ExchangeRouter: "0xFa843af557824Be5127eaCB3c4B5D86EADEB73A1",
ExchangeRouter: "0xe37d052e1deb99901de205e7186e31a36e4ef70c",
DepositVault: "0x90c670825d0C62ede1c5ee9571d6d9a17A722DFF",
WithdrawalVault: "0xf5F30B10141E1F63FC11eD772931A8294a591996",
OrderVault: "0xD3D60D22d415aD43b7e64b510D86A30f19B1B12C",
ShiftVault: "0x7fC46CCb386e9bbBFB49A2639002734C3Ec52b39",
SyntheticsReader: "0xc304F8e9872A9c00371A7406662dC10A10740AA8",
SyntheticsReader: "0x618fCEe30D9A26e8533C3B244CAd2D6486AFf655",
SyntheticsRouter: "0x820F5FfC5b525cD4d88Cd91aCf2c28F16530Cc68",
GlvReader: "0xae9596a1C438675AcC75f69d32E21Ac9c8fF99bD",
GlvRouter: "0x16500c1d8ffe2f695d8dcadf753f664993287ae4",
GlvVault: "0x527FB0bCfF63C47761039bB386cFE181A92a4701",
GelatoRelayRouter: "0x035A9A047d20a486e14A613B04d5a95d7A617c5D",
SubaccountGelatoRelayRouter: "0x3B753c0D0aE55530f24532B8Bb9d0bAcD5B675C0",
ExternalHandler: "0xD149573a098223a9185433290a5A5CDbFa54a8A9",
OpenOceanRouter: "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64",
ChainlinkPriceFeedProvider: "0x713c6a2479f6C079055A6AD3690D95dEDCEf9e1e",
ExternalHandler: "0xD149573a098223a9185433290a5A5CDbFa54a8A9",
Multicall: "0xcA11bde05977b3631167028862bE2a173976CA11",
ArbitrumNodeInterface: zeroAddress,
ClaimHandler: "0xF73CE08A22c67f19d75892457817e917cB3f9493",
},
[AVALANCHE_FUJI]: {
@@ -199,7 +188,7 @@ export const CONTRACTS = {
WithdrawalVault: "0x74d49B6A630Bf519bDb6E4efc4354C420418A6A2",
OrderVault: "0x25D23e8E655727F2687CC808BB9589525A6F599B",
ShiftVault: "0x257D0EA0B040E2Cd1D456fB4C66d7814102aD346",
SyntheticsReader: "0xA71e8b30c9414852F065e4cE12bbCC05cF50937A",
SyntheticsReader: "0x16Fb5b8846fbfAe09c034fCdF3D3F9492484DA80",
SyntheticsRouter: "0x5e7d61e4C52123ADF651961e4833aCc349b61491",
Timelock: zeroAddress,
@@ -207,17 +196,11 @@ export const CONTRACTS = {
GlvRouter: "0x377d979AB35Cd848497707ffa6Ee91783f925b80",
GlvVault: "0x76f93b5240DF811a3fc32bEDd58daA5784e46C96",
GelatoRelayRouter: zeroAddress,
SubaccountGelatoRelayRouter: zeroAddress,
OpenOceanRouter: zeroAddress,
ExternalHandler: "0x0d9F90c66C392c4d0e70EE0d399c43729B942512",
ChainlinkPriceFeedProvider: zeroAddress,
Multicall: "0x0f53e512b49202a37c81c6085417C9a9005F2196",
ArbitrumNodeInterface: zeroAddress,
},
};

View File

@@ -11,6 +11,5 @@ export { default as CustomScenario } from './CustomScenario/CustomScenario'
export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge'
export { default as StatusBadge } from './StatusBadge/StatusBadge'
export { default as PositionsList } from './Positions/PositionList'
export { default as WorkflowCanvas } from './Workflow/workflowCanvas'
export { default as ScenarioModal } from './ScenarioModal'
export { default as BotNameModal } from './BotNameModal/BotNameModal'

View File

@@ -45,8 +45,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Managing.Application.Tests"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Managing.Infrastructure.Messengers", "Managing.Infrastructure.Messengers\Managing.Infrastructure.Messengers.csproj", "{AD40302A-27C7-4E9D-B644-C7B141571EAF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Managing.Api.Workers", "Managing.Api.Workers\Managing.Api.Workers.csproj", "{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Managing.Infrastructure.Databases", "Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj", "{E6CB238E-8F60-4139-BDE6-31534832198E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Managing.Application.Abstractions", "Managing.Application.Abstractions\Managing.Application.Abstractions.csproj", "{283AC491-97C3-49E0-AB17-272EFB4E5A9C}"
@@ -166,14 +164,6 @@ Global
{AD40302A-27C7-4E9D-B644-C7B141571EAF}.Release|Any CPU.Build.0 = Release|Any CPU
{AD40302A-27C7-4E9D-B644-C7B141571EAF}.Release|x64.ActiveCfg = Release|Any CPU
{AD40302A-27C7-4E9D-B644-C7B141571EAF}.Release|x64.Build.0 = Release|Any CPU
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Debug|x64.ActiveCfg = Debug|x64
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Debug|x64.Build.0 = Debug|x64
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Release|Any CPU.Build.0 = Release|Any CPU
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Release|x64.ActiveCfg = Release|x64
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E}.Release|x64.Build.0 = Release|x64
{E6CB238E-8F60-4139-BDE6-31534832198E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6CB238E-8F60-4139-BDE6-31534832198E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6CB238E-8F60-4139-BDE6-31534832198E}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -262,7 +252,6 @@ Global
{837B12AD-E96C-40CE-9DEE-931442A6C15E} = {E453D33B-5C2B-4AA1-834D-2C916EC95FC6}
{35A05E76-29F6-4DC1-886D-FD69926CB490} = {8F2ECEA7-5BCA-45DF-B6E3-88AADD7AFD45}
{AD40302A-27C7-4E9D-B644-C7B141571EAF} = {E453D33B-5C2B-4AA1-834D-2C916EC95FC6}
{0DC797C2-007C-496E-B4C9-FDBD29D4EF4E} = {A1296069-2816-43D4-882C-516BCB718D03}
{E6CB238E-8F60-4139-BDE6-31534832198E} = {E453D33B-5C2B-4AA1-834D-2C916EC95FC6}
{283AC491-97C3-49E0-AB17-272EFB4E5A9C} = {F6774DB0-DF13-4077-BC94-0E67EE105C4C}
{CDDF92D4-9D2E-4134-BD44-3064D6EF462D} = {E453D33B-5C2B-4AA1-834D-2C916EC95FC6}