From aa433a8fb227610c4e17bb20c55345df7aab192a Mon Sep 17 00:00:00 2001 From: Aren Date: Wed, 22 Apr 2026 19:11:12 +0400 Subject: [PATCH] Add rate field to GetQuoteResponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the gross pre-fee exchange rate from IRateProvider on the public quote response — matches Layerswap's SwapQuoteModel.Rate semantics (raw provider rate, not receive/amount). For SameAsset routes this is 1.0; for TokenPrice it is srcPriceUsd / dstPriceUsd. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/rules/fee-and-quotes.md | 3 +++ csharp/src/Infrastructure/Quote/QuoteService.cs | 1 + csharp/src/Shared.Models/Models/QuoteWithSolverDto.cs | 2 ++ csharp/src/Shared.Proto/SolverProtoMapper.cs | 2 ++ protos/solver.proto | 1 + 5 files changed, 9 insertions(+) diff --git a/.claude/rules/fee-and-quotes.md b/.claude/rules/fee-and-quotes.md index c399ba7e..211dcc47 100644 --- a/.claude/rules/fee-and-quotes.md +++ b/.claude/rules/fee-and-quotes.md @@ -4,6 +4,9 @@ globs: csharp/** # Fee & Quote System +## Rate Field on GetQuoteResponse +`GetQuoteResponse.rate` (proto `string`, domain `QuoteWithSolverDto.Rate` as `decimal`) is the **gross, pre-fee** exchange rate returned by `IRateProvider.GetRateAsync(route)` — matches Layerswap semantics (`SwapQuoteModel.Rate` populated from the product of per-hop `route.Rate` values in `ViewModelMapper.MapQuoteResponseAsync`, where each `route.Rate` is the raw provider rate). For `SameAssetRateProvider` this is `1m`; for `TokenPriceRateProvider` it is `srcPriceUsd / dstPriceUsd`. Purely informational — the solver's fee and receive-amount math already uses this value internally; it's just surfaced now. Mapped to proto as invariant-culture `"G"` in `SolverProtoMapper.ToGetQuoteResponse`. + ## Quote Fee Calculation `QuoteService` (in `Infrastructure/Quote/QuoteService.cs`) computes total fees for a quote: diff --git a/csharp/src/Infrastructure/Quote/QuoteService.cs b/csharp/src/Infrastructure/Quote/QuoteService.cs index 2ff6c2ed..16786112 100644 --- a/csharp/src/Infrastructure/Quote/QuoteService.cs +++ b/csharp/src/Infrastructure/Quote/QuoteService.cs @@ -210,6 +210,7 @@ await CalculateTotalFeeAsync(route, amount, rewardAmountInDestNative: rewardAmou { Amount = amount, ReceiveAmount = receiveAmount, + Rate = swapRate, TotalFee = totalFee, TotalServiceFee = totalServiceFee, TotalExpenseFee = totalExpenseFee, diff --git a/csharp/src/Shared.Models/Models/QuoteWithSolverDto.cs b/csharp/src/Shared.Models/Models/QuoteWithSolverDto.cs index 4bd5d669..36061e87 100644 --- a/csharp/src/Shared.Models/Models/QuoteWithSolverDto.cs +++ b/csharp/src/Shared.Models/Models/QuoteWithSolverDto.cs @@ -13,6 +13,8 @@ public class QuoteWithSolverDto : QuoteDto public BigInteger Amount { get; set; } + public decimal Rate { get; set; } + public BigInteger RewardAmount { get; set; } public string Signature { get; set; } = string.Empty; diff --git a/csharp/src/Shared.Proto/SolverProtoMapper.cs b/csharp/src/Shared.Proto/SolverProtoMapper.cs index ba90991e..6d35a656 100644 --- a/csharp/src/Shared.Proto/SolverProtoMapper.cs +++ b/csharp/src/Shared.Proto/SolverProtoMapper.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Google.Protobuf.WellKnownTypes; using Train.Solver.Shared.Models.Events; using DomainModels = Train.Solver.Shared.Models; @@ -53,6 +54,7 @@ public static class SolverProtoMapper TotalExpenseFee = quote.TotalExpenseFee.ToString(), TotalRewardFee = quote.TotalRewardFee.ToString(), Amount = quote.Amount.ToString(), + Rate = quote.Rate.ToString("G", CultureInfo.InvariantCulture), }; #endregion diff --git a/protos/solver.proto b/protos/solver.proto index 6e05c34e..c634c935 100644 --- a/protos/solver.proto +++ b/protos/solver.proto @@ -69,6 +69,7 @@ message GetQuoteResponse { string total_expense_fee = 11; // BigInteger as string (source token units) string total_reward_fee = 12; // BigInteger as string (source token units) string amount = 13; // BigInteger as string (source amount — useful for reverse quotes) + string rate = 14; // Gross pre-fee exchange rate from IRateProvider (decimal): destination per 1 source token } message RewardInfo {