diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 1220c7d66b8c88e9f84b3234914fad4060e03f0f..746356f5161079f1d13b82213ed35bd0b46cb55c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -7,3 +7,7 @@ services: environment: - TOKEN_ABI_LOCATION=../solidity/token.abi.json - "ASPNETCORE_ENVIRONMENT=Development" + - POSTGRES_USER=roesticoin + - POSTGRES_PASSWORD=roesticoin + - POSTGRES_DB=roesticoin + - POSTGRES_HOST=postgres diff --git a/Frontend/AssetStores/PotatoStorage.cs b/Frontend/AssetStores/PotatoStorage.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf72759489a8778fbcaba25e5cca14cc78721a68 --- /dev/null +++ b/Frontend/AssetStores/PotatoStorage.cs @@ -0,0 +1,48 @@ +using Frontend.Assets; +using Microsoft.EntityFrameworkCore; + +namespace Frontend.AssetStores; + +public class PotatoStorage(DbContextOptions<PotatoStorage> options) : DbContext(options) +{ + public required DbSet<PotatoTransaction> Transactions { get; set; } + + public async Task AddPotatoes(PotatoTransaction transaction) + { + await Transactions.AddAsync(transaction); + await SaveChangesAsync(); + } + + public async Task<PotatoTransaction> RemovePotatoes(double weight) + { + var transactions = Transactions + .OrderBy(p => p.Id) + .AsQueryable(); + + var newTransaction = new PotatoTransaction(); + foreach (var transaction in transactions) + { + var newTransactionWeight = newTransaction.Weight + transaction.Weight; + if (newTransactionWeight <= weight) + { + newTransaction.Weight += transaction.Weight; + Transactions.Remove(transaction); + } + else + { + var overshoot = newTransactionWeight - weight; + transaction.Weight -= overshoot; + newTransaction.Weight += transaction.Weight; + Transactions.Update(transaction); + } + } + + await SaveChangesAsync(); + return newTransaction; + } + + public double SupplyWeight() + { + return Transactions.Sum(p => p.Weight); + } +} diff --git a/Frontend/Assets/PotatoTransaction.cs b/Frontend/Assets/PotatoTransaction.cs new file mode 100644 index 0000000000000000000000000000000000000000..abf4d20f4ea9cecb9b37c88374409343e4d563c7 --- /dev/null +++ b/Frontend/Assets/PotatoTransaction.cs @@ -0,0 +1,7 @@ +namespace Frontend.Assets; + +public class PotatoTransaction +{ + public int Id { get; set; } + public double Weight { get; set; } +} diff --git a/Frontend/BackgroundServices/PrometheusService.cs b/Frontend/BackgroundServices/PrometheusService.cs index b0719a42856b51ea94778c728ff4f83335a07a61..9b3bf54f4ca0202149b26ca95c37d802c6e3ab28 100644 --- a/Frontend/BackgroundServices/PrometheusService.cs +++ b/Frontend/BackgroundServices/PrometheusService.cs @@ -7,16 +7,18 @@ namespace Frontend.BackgroundServices; public class PrometheusService : BackgroundService { - private readonly ILogger<PrometheusService> _logger ; + private readonly ILogger<PrometheusService> _logger; private readonly TimeSpan _interval = TimeSpan.FromSeconds(10); private readonly Erc20TokenMetrics _erc20TokenMetrics; private readonly EthMetrics _ethMetrics; private readonly MeterListener _meterListener; - + public PrometheusService( ILogger<PrometheusService> logger, Erc20TokenMetrics tokenMetrics, - EthMetrics ethMetrics + EthMetrics ethMetrics, + PotatoStorageMetrics potatoStorageMetrics, + PotatoMarketplaceMetrics potatoMarketplaceMetrics ) { _logger = logger; @@ -31,6 +33,8 @@ public class PrometheusService : BackgroundService }; _erc20TokenMetrics.CreateMetrics(); _ethMetrics.CreateMetrics(); + potatoStorageMetrics.CreateMetrics(); + potatoMarketplaceMetrics.CreateMetrics(); } protected async override Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/Frontend/Frontend.csproj b/Frontend/Frontend.csproj index 50385bfe73dcd355b7de44906bd58c60c5a72f3c..d114f3697144818ed000e0681c78000801fd397b 100644 --- a/Frontend/Frontend.csproj +++ b/Frontend/Frontend.csproj @@ -1,8 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> <PackageReference Include="Nethereum.Web3" Version="4.26.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> + <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" /> <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" /> </ItemGroup> diff --git a/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs b/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs new file mode 100644 index 0000000000000000000000000000000000000000..757223d802fe535e5395f22715d2f3830b54dbb3 --- /dev/null +++ b/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs @@ -0,0 +1,13 @@ +using Frontend.Assets; + +namespace Frontend.Marketplaces.Interfaces +{ + public interface IPotatoMarketPlace + { + public double PotatoPrice { get; } + public double PotatoKgToChf(double kg); + public double ChfToPotatoKg(double chf); + public PotatoTransaction Buy(double amountInChf); + public double Sell(PotatoTransaction transaction); + } +} diff --git a/Frontend/Marketplaces/PotatoMarketplaceMock.cs b/Frontend/Marketplaces/PotatoMarketplaceMock.cs new file mode 100644 index 0000000000000000000000000000000000000000..fb3accf14f4242772fef27eeaf06fc3da2bab29e --- /dev/null +++ b/Frontend/Marketplaces/PotatoMarketplaceMock.cs @@ -0,0 +1,32 @@ +using Frontend.Assets; +using Frontend.Marketplaces.Interfaces; + +namespace Frontend.Marketplaces; + +public class PotatoMarketplaceMock() : IPotatoMarketPlace +{ + private const double potatoKgPrice = 2.02; // source: https://www.blw.admin.ch/blw/de/home/markt/marktbeobachtung/kartoffeln.html + + public double PotatoPrice => potatoKgPrice; + + public double ChfToPotatoKg(double chf) + { + return chf / potatoKgPrice; + } + + public double PotatoKgToChf(double kg) + { + return kg * potatoKgPrice; + } + + public PotatoTransaction Buy(double amountInChf) + { + var amountInKg = ChfToPotatoKg(amountInChf); + return new() { Weight = amountInKg }; + } + + public double Sell(PotatoTransaction transaction) + { + return PotatoKgToChf(transaction.Weight); + } +} diff --git a/Frontend/Metrics/PotatoMarketplaceMetrics.cs b/Frontend/Metrics/PotatoMarketplaceMetrics.cs new file mode 100644 index 0000000000000000000000000000000000000000..57f839e3f7035cad62b0bee55c972d5b233ef634 --- /dev/null +++ b/Frontend/Metrics/PotatoMarketplaceMetrics.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.Metrics; +using Frontend.Marketplaces.Interfaces; + +namespace Frontend.Metrics; + +public class PotatoMarketplaceMetrics( + IPotatoMarketPlace marketplace, + IMeterFactory meterFactory +) +{ + private readonly IPotatoMarketPlace marketplace = marketplace; + private readonly IMeterFactory MeterFactory = meterFactory; + + public void CreateMetrics() + { + var meter = MeterFactory.Create("RoestiCoin"); + meter.CreateObservableGauge("Marketplace.Potato.Price", () => marketplace.PotatoPrice, unit: "CHF"); + } +} diff --git a/Frontend/Metrics/PotatoStorageMetrics.cs b/Frontend/Metrics/PotatoStorageMetrics.cs new file mode 100644 index 0000000000000000000000000000000000000000..e63861b1c1bb3bc462b7069418095325f12d29af --- /dev/null +++ b/Frontend/Metrics/PotatoStorageMetrics.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.Metrics; +using Frontend.AssetStores; +using Microsoft.EntityFrameworkCore; + +namespace Frontend.Metrics; + +public class PotatoStorageMetrics( + IDbContextFactory<PotatoStorage> potatoStorageFactory, + IMeterFactory meterFactory +) +{ + private readonly IDbContextFactory<PotatoStorage> potatoStorageFactory = potatoStorageFactory; + private readonly IMeterFactory MeterFactory = meterFactory; + + public void CreateMetrics() + { + var potatoStorage = potatoStorageFactory.CreateDbContext(); + var meter = MeterFactory.Create("RoestiCoin"); + meter.CreateObservableGauge("Storage.Potato.Supply.Weight", potatoStorage.SupplyWeight, unit: "kg"); + } +} diff --git a/Frontend/Program.cs b/Frontend/Program.cs index 461d006e60bb6ef800c75fe05155dd5134d7cac7..1b91a6c296b9247bcfa7773987f841408699f235 100644 --- a/Frontend/Program.cs +++ b/Frontend/Program.cs @@ -1,6 +1,10 @@ using Frontend; +using Frontend.AssetStores; using Frontend.BackgroundServices; +using Frontend.Marketplaces; +using Frontend.Marketplaces.Interfaces; using Frontend.Metrics; +using Microsoft.EntityFrameworkCore; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -11,6 +15,11 @@ var tokenAbi = File.ReadAllText(tokenAbiLocation)!; var chainApiUrl = Environment.GetEnvironmentVariable("API_URL")!; var accountPrivateKey = Environment.GetEnvironmentVariable("ACCOUNT_PRIVATE_KEY")!; +var postgresHost = Environment.GetEnvironmentVariable("POSTGRES_HOST")!; +var postgresUser = Environment.GetEnvironmentVariable("POSTGRES_USER")!; +var postgresPassword = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")!; +var postgresDb = Environment.GetEnvironmentVariable("POSTGRES_DB")!; + var builder = WebApplication.CreateBuilder(args); // Configurations @@ -20,6 +29,9 @@ var chainConfiguration = builder.Configuration.GetSection("Chain"); builder.Services.AddHostedService<PrometheusService>(); builder.Services.AddSingleton<Erc20TokenMetrics>(); builder.Services.AddSingleton<EthMetrics>(); +builder.Services.AddSingleton<PotatoStorageMetrics>(); +builder.Services.AddSingleton<PotatoMarketplaceMetrics>(); +builder.Services.AddSingleton<IPotatoMarketPlace>(sp => new PotatoMarketplaceMock()); builder.Services.AddSingleton(provider => new TokenClient( chainApiUrl, @@ -28,6 +40,9 @@ builder.Services.AddSingleton(provider => new TokenClient( accountPrivateKey )); +builder.Services.AddDbContextFactory<PotatoStorage>(options => + options.UseNpgsql($"Host={postgresHost};Database={postgresDb};Username={postgresUser};Password={postgresPassword}")); + builder.Services.AddRazorPages(); builder.Services.AddOpenTelemetry() .WithMetrics(builder => @@ -40,6 +55,11 @@ builder.Services.AddOpenTelemetry() }); var app = builder.Build(); + +var potatoStorageFactory = app.Services.GetRequiredService<IDbContextFactory<PotatoStorage>>(); +var potatoStorage = potatoStorageFactory.CreateDbContext(); +potatoStorage.Database.EnsureCreated(); + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { diff --git a/docker-compose.yml b/docker-compose.yml index b449e8494089ef547d30ed598c68a3633c2239ab..4a1b1aed32366b345367abd015057789e46dc13d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,17 @@ services: depends_on: - prometheus + postgres: + image: postgres:17-alpine + restart: unless-stopped + ports: + - "5432:5432" + environment: + POSTGRES_USER: roesticoin + POSTGRES_PASSWORD: roesticoin + volumes: + - postgres_data:/var/lib/postgresql/data + roesticoin: env_file: - .env @@ -32,3 +43,4 @@ services: volumes: prometheus_data: + postgres_data: