From 8779c4b237a636f9213b73c12e22e575bae47a49 Mon Sep 17 00:00:00 2001 From: Ruben Date: Wed, 6 May 2026 18:29:36 +0400 Subject: [PATCH 1/2] Set Plorex solver API key in station-config.json Co-Authored-By: Claude Opus 4.7 (1M context) --- csharp/src/StationAPI/station-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/src/StationAPI/station-config.json b/csharp/src/StationAPI/station-config.json index 08e1c953..28922cae 100644 --- a/csharp/src/StationAPI/station-config.json +++ b/csharp/src/StationAPI/station-config.json @@ -235,7 +235,7 @@ "grpcEndpoint": "https://train-solver-api.lb.layerswap.io", "webhookSecret": "station-webhook-secret-1", "adminApiUrl": "https://train-solver-admin.lb.layerswap.io", - "apiKey": null + "apiKey": "tsk_test_jw9ku1zA2ETneTV-_Rm7BveBLOR_8fGXNkKR6bxbUBM" } ], "faucet": { From 3f0a653e5318bf352890ee544671729cfff071ff Mon Sep 17 00:00:00 2001 From: Ruben Date: Wed, 6 May 2026 18:35:13 +0400 Subject: [PATCH 2/2] Switch faucet from AdminAPI HTTP to Gateway gRPC ExecutionService The Gateway gRPC API now exposes ExecutionService.SubmitTransaction, which is the auth-scoped equivalent of AdminAPI's custom-transaction. StationAPI's FaucetService now goes through the same gRPC channel as quotes/orders, picking up the X-Train-API-Key header automatically via ApiKeyClientInterceptor. Drops the AdminApiUrl config field and the solver-admin HttpClient. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../StationAPI/Configuration/StationConfig.cs | 1 - csharp/src/StationAPI/Program.cs | 4 -- .../src/StationAPI/Services/FaucetService.cs | 68 +++++++++---------- .../Services/SolverClientFactory.cs | 18 ++++- csharp/src/StationAPI/station-config.json | 1 - 5 files changed, 48 insertions(+), 44 deletions(-) diff --git a/csharp/src/StationAPI/Configuration/StationConfig.cs b/csharp/src/StationAPI/Configuration/StationConfig.cs index c94629fb..34439ed3 100644 --- a/csharp/src/StationAPI/Configuration/StationConfig.cs +++ b/csharp/src/StationAPI/Configuration/StationConfig.cs @@ -62,7 +62,6 @@ public class SolverConfig public string? LogoUrl { get; set; } public string GrpcEndpoint { get; set; } = null!; public string? WebhookSecret { get; set; } - public string? AdminApiUrl { get; set; } /// /// Bearer token for the solver's Gateway gRPC API. Format: tsk_test_* or diff --git a/csharp/src/StationAPI/Program.cs b/csharp/src/StationAPI/Program.cs index d2c80bc4..e21f2828 100644 --- a/csharp/src/StationAPI/Program.cs +++ b/csharp/src/StationAPI/Program.cs @@ -78,10 +78,6 @@ builder.Services.AddMemoryCache(); builder.Services.AddSingleton(); builder.Services.AddTransient(); -builder.Services.AddHttpClient("solver-admin", client => -{ - client.Timeout = TimeSpan.FromSeconds(15); -}); // Binance price feed HttpClient builder.Services.AddHttpClient("binance", client => diff --git a/csharp/src/StationAPI/Services/FaucetService.cs b/csharp/src/StationAPI/Services/FaucetService.cs index ae945563..957390e5 100644 --- a/csharp/src/StationAPI/Services/FaucetService.cs +++ b/csharp/src/StationAPI/Services/FaucetService.cs @@ -1,6 +1,6 @@ -using System.Net.Http.Json; using System.Numerics; -using System.Text.Json; +using Grpc.Core; +using Train.Solver.Proto; using Train.Solver.StationAPI.Configuration; using Train.Solver.StationAPI.Helpers; @@ -11,16 +11,16 @@ public class FaucetService : IFaucetService private const string EvmNativeAddress = "0x0000000000000000000000000000000000000000"; private readonly IStationConfigProvider _configProvider; - private readonly IHttpClientFactory _httpClientFactory; + private readonly ISolverClientFactory _clientFactory; private readonly ILogger _logger; public FaucetService( IStationConfigProvider configProvider, - IHttpClientFactory httpClientFactory, + ISolverClientFactory clientFactory, ILogger logger) { _configProvider = configProvider; - _httpClientFactory = httpClientFactory; + _clientFactory = clientFactory; _logger = logger; } @@ -36,10 +36,10 @@ public FaucetService( public async Task ClaimAsync(FaucetItemConfig item, string recipientAddress, CancellationToken ct) { - var solver = _configProvider.Current.Solvers.FirstOrDefault(s => !string.IsNullOrEmpty(s.AdminApiUrl)); - if (solver is null || string.IsNullOrEmpty(solver.AdminApiUrl)) + var solver = _configProvider.Current.Solvers.FirstOrDefault(); + if (solver is null) { - return new FaucetClaimResult(false, null, "No solver with admin API configured"); + return new FaucetClaimResult(false, null, "No solver configured"); } if (!BigInteger.TryParse(item.Amount, out var amount) || amount.Sign <= 0) @@ -57,46 +57,44 @@ public async Task ClaimAsync(FaucetItemConfig item, string re return new FaucetClaimResult(false, null, ex.Message); } + var client = _clientFactory.GetExecutionClient(solver.Id); + if (client is null) + { + return new FaucetClaimResult(false, null, "Solver execution client unavailable"); + } + var correlationId = $"faucet-{Guid.NewGuid()}"; var networkIdentifier = item.SolverNetworkIdentifier ?? item.Caip2Id; - var payload = new + var request = new SubmitExecutionRequest { - networkSlug = networkIdentifier, - walletAddress = item.MinterAddress, - toAddress = item.TokenContract, - callData, - amount = "0", - tokenContract = EvmNativeAddress, - correlationId + NetworkSlug = networkIdentifier, + WalletAddress = item.MinterAddress, + ToAddress = item.TokenContract, + CallData = callData, + Amount = "0", + TokenContract = EvmNativeAddress, + CorrelationId = correlationId, }; - var http = _httpClientFactory.CreateClient("solver-admin"); - var url = solver.AdminApiUrl.TrimEnd('/') + "/api/custom-transaction"; - try { - using var response = await http.PostAsJsonAsync(url, payload, ct); - if (!response.IsSuccessStatusCode) - { - var body = await response.Content.ReadAsStringAsync(ct); - _logger.LogWarning("Faucet claim rejected by solver: {Status} {Body}", (int)response.StatusCode, body); - return new FaucetClaimResult(false, null, $"Solver responded {(int)response.StatusCode}"); - } - - using var stream = await response.Content.ReadAsStreamAsync(ct); - using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: ct); - if (doc.RootElement.TryGetProperty("accepted", out var accepted) && accepted.ValueKind == JsonValueKind.False) + var response = await client.SubmitTransactionAsync(request, cancellationToken: ct); + if (!response.Accepted) { - return new FaucetClaimResult(false, null, "Solver did not accept the transaction"); + var error = string.IsNullOrEmpty(response.ErrorMessage) + ? "Solver did not accept the transaction" + : response.ErrorMessage; + _logger.LogWarning("Faucet claim rejected by solver: {Error}", error); + return new FaucetClaimResult(false, null, error); } - return new FaucetClaimResult(true, correlationId, null); + return new FaucetClaimResult(true, response.CorrelationId, null); } - catch (HttpRequestException ex) + catch (RpcException ex) { - _logger.LogError(ex, "Failed to call solver admin API for faucet claim"); - return new FaucetClaimResult(false, null, "Failed to reach solver"); + _logger.LogError(ex, "Faucet claim gRPC call failed"); + return new FaucetClaimResult(false, null, ex.Status.Detail ?? "Failed to reach solver"); } } } diff --git a/csharp/src/StationAPI/Services/SolverClientFactory.cs b/csharp/src/StationAPI/Services/SolverClientFactory.cs index ea3b00ad..8ed59907 100644 --- a/csharp/src/StationAPI/Services/SolverClientFactory.cs +++ b/csharp/src/StationAPI/Services/SolverClientFactory.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Grpc.Core; using Grpc.Core.Interceptors; using Grpc.Net.Client; using Grpc.Net.Client.Web; @@ -10,6 +11,7 @@ namespace Train.Solver.StationAPI.Services; public interface ISolverClientFactory { SolverService.SolverServiceClient? GetClient(string solverId); + ExecutionService.ExecutionServiceClient? GetExecutionClient(string solverId); } public class SolverClientFactory : ISolverClientFactory, IDisposable @@ -23,6 +25,18 @@ public SolverClientFactory(IStationConfigProvider configProvider) } public SolverService.SolverServiceClient? GetClient(string solverId) + { + var invoker = GetInvoker(solverId); + return invoker is null ? null : new SolverService.SolverServiceClient(invoker); + } + + public ExecutionService.ExecutionServiceClient? GetExecutionClient(string solverId) + { + var invoker = GetInvoker(solverId); + return invoker is null ? null : new ExecutionService.ExecutionServiceClient(invoker); + } + + private CallInvoker? GetInvoker(string solverId) { var solver = _configProvider.Current.Solvers.FirstOrDefault(s => s.Id == solverId); if (solver is null) return null; @@ -35,11 +49,9 @@ public SolverClientFactory(IStationConfigProvider configProvider) // Channels are cached per endpoint, but the API key is per solver — wrap the // invoker so each client carries its own auth header. CallInvoker is cheap. - var invoker = string.IsNullOrEmpty(solver.ApiKey) + return string.IsNullOrEmpty(solver.ApiKey) ? channel.CreateCallInvoker() : channel.Intercept(new ApiKeyClientInterceptor(solver.ApiKey)); - - return new SolverService.SolverServiceClient(invoker); } public void Dispose() diff --git a/csharp/src/StationAPI/station-config.json b/csharp/src/StationAPI/station-config.json index 28922cae..6985b2e4 100644 --- a/csharp/src/StationAPI/station-config.json +++ b/csharp/src/StationAPI/station-config.json @@ -234,7 +234,6 @@ "logoUrl": "https://raw.githubusercontent.com/TrainProtocol/.github/main/assets/logo.png", "grpcEndpoint": "https://train-solver-api.lb.layerswap.io", "webhookSecret": "station-webhook-secret-1", - "adminApiUrl": "https://train-solver-admin.lb.layerswap.io", "apiKey": "tsk_test_jw9ku1zA2ETneTV-_Rm7BveBLOR_8fGXNkKR6bxbUBM" } ],