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 098fb2c93ae88d219407ac492b8888658e9bf4eb..0a617016651548d32404b8ced5c72ee365fb2f7d 100644 --- a/Frontend/BackgroundServices/PrometheusService.cs +++ b/Frontend/BackgroundServices/PrometheusService.cs @@ -1,16 +1,15 @@ using System.Diagnostics.Metrics; using Frontend.Configuration; using Frontend.Metrics; -using Frontend.Services; using Nethereum.Web3; - +using Nethereum.Web3.Accounts; 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; @@ -18,13 +17,17 @@ public class PrometheusService : BackgroundService private readonly PoolMetrics _poolMetrics; + public PrometheusService( ILogger<PrometheusService> logger, Erc20TokenMetrics tokenMetrics, EthMetrics ethMetrics, PoolMetrics poolMetrics, ChainSettings chainSettings, - Web3 web3 + Web3 web3, + Account account, + PotatoStorageMetrics potatoStorageMetrics, + PotatoMarketplaceMetrics potatoMarketplaceMetrics ) { _logger = logger; @@ -40,7 +43,9 @@ public class PrometheusService : BackgroundService }; _erc20TokenMetrics.CreateMetrics(); _ethMetrics.CreateMetrics(); - PoolV3Client poolClient = new( web3,chainSettings); + potatoStorageMetrics.CreateMetrics(); + potatoMarketplaceMetrics.CreateMetrics(); + PoolV3Client poolClient = new( web3,chainSettings,account); _poolMetrics.CreateMetrics(poolClient); } diff --git a/Frontend/Configuration/ChainSettings.cs b/Frontend/Configuration/ChainSettings.cs index 34d2cb64084218ebe62a8edc95a8fe82e05bcc67..c644f88160ff728fe4a9f652d32a42ec3585789c 100644 --- a/Frontend/Configuration/ChainSettings.cs +++ b/Frontend/Configuration/ChainSettings.cs @@ -6,4 +6,6 @@ public class ChainSettings public string WethTokenAddress { get; set; } public UniswapSettings Uniswap { get; set; } + public string PrivateKey { get; set; } + } \ No newline at end of file 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/PoolMetrics.cs b/Frontend/Metrics/PoolMetrics.cs index 98184592e301e62dd83eaf6e0d1704e73911c097..2c0ea46c21f51b04c8ad3a4a5e29cecd82c0f372 100644 --- a/Frontend/Metrics/PoolMetrics.cs +++ b/Frontend/Metrics/PoolMetrics.cs @@ -1,8 +1,4 @@ using System.Diagnostics.Metrics; -using System.Net.Http; -using System.Threading.RateLimiting; -using Frontend.Services; -using Newtonsoft.Json.Linq; namespace Frontend.Metrics; public class PoolMetrics{ 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/Pages/Index.cshtml.cs b/Frontend/Pages/Index.cshtml.cs index ebe2a8ba8e31012c7b4903fa31a3aaf8517d5b14..080720e6e6a8a6accec5441c98b86ad9af090380 100644 --- a/Frontend/Pages/Index.cshtml.cs +++ b/Frontend/Pages/Index.cshtml.cs @@ -15,7 +15,7 @@ public class IndexModel : PageModel public IndexModel(ILogger<IndexModel> logger, Web3 web3, ChainSettings chainSettings, Account account) { _logger = logger; - _poolV3Client = new PoolV3Client(web3, chainSettings); + _poolV3Client = new PoolV3Client(web3, chainSettings, account); _chainSettings = chainSettings; _account = account; } @@ -40,7 +40,7 @@ public class IndexModel : PageModel } if (amount > 0) { - var amountRecieved = _poolV3Client.SwapAsync(fromTokenAddress, toTokenAddress, amount, _account).Result; + var amountRecieved = _poolV3Client.SwapAsync(fromTokenAddress!, toTokenAddress!, amount).Result; } } } diff --git a/Frontend/PoolV3Client.cs b/Frontend/PoolV3Client.cs index 7889cea07448c12ad161085236c41d27f6faf23e..5d1cba47df8e418efab085fdd43384d442a4501e 100644 --- a/Frontend/PoolV3Client.cs +++ b/Frontend/PoolV3Client.cs @@ -23,7 +23,6 @@ public class PoolV3Client private string? _token0Address = null; private string? _token1Address = null; private uint? _poolFeeTier = null; - private bool _hasApproved = false; private TokenClient? _token0Client; private TokenClient? _token1Client; @@ -77,7 +76,7 @@ public class PoolV3Client public PoolV3Client(Web3 web3, ChainSettings chainSettings, Account account) { - _account=account; + _account = account; _pool = web3.Eth.GetContract( File.ReadAllText( Path.Combine( @@ -162,17 +161,17 @@ public class PoolV3Client { if (fromTokenAddress == Token0Address) { - if(amount>Web3.Convert.FromWei(await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address))) + if (amount > Web3.Convert.FromWei(await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address))) { await Token0Client.ApproveAsync(_chainSettings.Uniswap.SwapRouterV2Address, BigInteger.Pow(2, 256) - 1); - } + } } else { - if(amount>Web3.Convert.FromWei(await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address))) + if (amount > Web3.Convert.FromWei(await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address))) { await Token0Client.ApproveAsync(_chainSettings.Uniswap.SwapRouterV2Address, BigInteger.Pow(2, 256) - 1); - } + } } } @@ -198,28 +197,28 @@ public class PoolV3Client throw new Exception("Swap event not found in transaction receipt"); } -/// <summary> -/// This function should swap the tokens in the pool to create a desired quote -/// </summary> -/// <param name="desiredQuote">the ration between token1/token0</param> -/// <returns></returns> + /// <summary> + /// This function should swap the tokens in the pool to create a desired quote + /// </summary> + /// <param name="desiredQuote">the ration between token1/token0</param> + /// <returns></returns> public async Task<decimal> PerformSwapToCreatDesiredQuote(decimal desiredQuote) { var currentQuote = await GetPairRatio(); - + throw new NotImplementedException(); var amount = 0.0M; if (currentQuote < desiredQuote) { // we sell some of token1 to buy token0 amount = (desiredQuote - currentQuote) / desiredQuote; - throw new NotImplementedException(); + throw new NotImplementedException(); await SwapAsync(Token1Address, Token0Address, amount); } else { amount = 1; - throw new NotImplementedException(); - await SwapAsync(Token1Address, Token0Address, amount, new Account(_chainSettings.PrivateKey)); + throw new NotImplementedException(); + await SwapAsync(Token1Address, Token0Address, amount); } throw new NotImplementedException(); } diff --git a/Frontend/Program.cs b/Frontend/Program.cs index c6ea82adc17f254bd449aa458b7fe0a15384769a..de4846dfcab0ff3d866bf6e498af6dabfbb4a974 100644 --- a/Frontend/Program.cs +++ b/Frontend/Program.cs @@ -1,11 +1,15 @@ using System.Runtime.CompilerServices; using Frontend; +using Frontend.AssetStores; using Frontend.BackgroundServices; using Frontend.Configuration; +using Frontend.Marketplaces; +using Frontend.Marketplaces.Interfaces; using Frontend.Metrics; -using Frontend.Services; using Nethereum.Web3; using Nethereum.Web3.Accounts; +using Microsoft.EntityFrameworkCore; + using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using Org.BouncyCastle.Utilities; @@ -16,13 +20,15 @@ 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 var chainConfiguration = builder.Configuration.GetSection("Chain"); -var chainId = int.Parse(chainConfiguration["ChainId"] ?? "11155111"); // fallback to Sepolia var chainSettings = new ChainSettings(); chainConfiguration.Bind(chainSettings); @@ -30,10 +36,13 @@ chainConfiguration.Bind(chainSettings); 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()); Account account = new Account( accountPrivateKey, - chainId: chainId + chainId: chainSettings.ChainId ); builder.Services.AddSingleton(provider => chainSettings); @@ -51,6 +60,9 @@ builder.Services.AddSingleton<PoolMetrics>(); +builder.Services.AddDbContextFactory<PotatoStorage>(options => + options.UseNpgsql($"Host={postgresHost};Database={postgresDb};Username={postgresUser};Password={postgresPassword}")); + builder.Services.AddRazorPages(); builder.Services.AddOpenTelemetry() .WithMetrics(builder => @@ -63,6 +75,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: