-
Notifications
You must be signed in to change notification settings - Fork 59
Separate anchor fees from network fees in cost decomposition #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
145c6d7
d3215c4
53eb0d1
e749011
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,17 @@ | ||
| // CostDecomposition breaks the effective transfer cost into separately-reported | ||
| // components: FX loss, fees, slippage, and expected failure cost. | ||
| // components: FX loss, network fees, anchor fee, slippage, and expected | ||
| // failure cost. | ||
| // | ||
| // Currently, the verdict reports a single loss percentage against fair value. | ||
| // That number is useful but opaque. Showing the decomposition turns a single | ||
| // verdict into actionable information. | ||
| // | ||
| // Each component is computed and reported independently. Expected failure cost | ||
| // stays explicitly unknown until failure history exists. | ||
| // Each component is computed and reported independently. Network fees and | ||
| // anchor fees are reported separately because they have different sources: | ||
| // network fees are Stellar base-fee charges per operation, while anchor fees | ||
| // are the anchor's own charge for a conversion, obtainable via SEP-38 when | ||
| // the anchor publishes an ANCHOR_QUOTE_SERVER. Expected failure cost stays | ||
| // explicitly unknown until failure history exists. | ||
| package route | ||
|
|
||
| import ( | ||
|
|
@@ -18,7 +23,8 @@ type CostComponent string | |
|
|
||
| const ( | ||
| CostFXLoss CostComponent = "fx_loss" | ||
| CostFees CostComponent = "fees" | ||
| CostNetworkFees CostComponent = "network_fees" | ||
| CostAnchorFee CostComponent = "anchor_fee" | ||
| CostSlippage CostComponent = "slippage" | ||
| CostExpectedFailure CostComponent = "expected_failure" | ||
| ) | ||
|
|
@@ -40,7 +46,8 @@ type CostDecomposition struct { | |
|
|
||
| // Decompose splits a priced route's effective transfer cost into components. | ||
| func Decompose(q Quote, mid decimal.Decimal) CostDecomposition { | ||
| parts := make([]CostPart, 0, 4) | ||
| _ = mid // reserved: will carry the independent mid-market rate for future FX-loss-from-mid calculation | ||
| parts := make([]CostPart, 0, 5) | ||
|
|
||
| // FX loss: difference between effective rate and mid, as a percentage. | ||
| fxLossPct := q.LossPct | ||
|
|
@@ -52,20 +59,43 @@ func Decompose(q Quote, mid decimal.Decimal) CostDecomposition { | |
| Determined: true, | ||
| }) | ||
|
|
||
| // Fees: undetermined. A Stellar path payment charges a base fee per | ||
| // Network fees: undetermined. A Stellar path payment charges a base fee per | ||
| // operation, and a multi-hop path is more operations than a direct one, | ||
| // but Decompose sees only a Quote and has neither the path's operation | ||
| // count nor a currently-effective base fee. Naming the gap keeps the | ||
| // units honest: unavailable is unknown, not a default, and small is not | ||
| // zero. | ||
| parts = append(parts, CostPart{ | ||
| Component: CostFees, | ||
| Component: CostNetworkFees, | ||
| Amount: decimal.Zero, | ||
| Pct: decimal.Zero, | ||
| Determined: false, | ||
| Reason: "network fee not measured; determining it requires the path's operation count and the current Stellar base fee", | ||
| }) | ||
|
Comment on lines
68
to
74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Do not encode an unknown fee as Lines 68-74 and 91-97 return numeric zero values when the fee amount and percentage are unavailable. Use an optional decimal representation for unknown Prompt for AI Agents As per path instructions: “unknown must be reported as unknown, never defaulted, guessed or averaged away.” Also applies to: 91-97 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| // Anchor fee: the anchor's own charge for a conversion, obtainable via | ||
| // SEP-38 when the anchor publishes an ANCHOR_QUOTE_SERVER in its | ||
| // stellar.toml. A DEX-only route has no anchor involved, and an anchor | ||
| // that does not publish a quote server has no machine-readable rate — the | ||
| // absence is a fact about the anchor rather than a zero fee. | ||
| // | ||
| // When a KindAnchorSEP38 quote is available, its FeeInBuyAsset (already | ||
| // normalised for denomination by sep38.Quote) can be wired here. | ||
| anchorFeeReason := "anchor fee not available; the anchor does not publish an ANCHOR_QUOTE_SERVER, so its fee cannot be obtained programmatically" | ||
| if q.Kind == KindAnchorSEP38 { | ||
| // TODO: extract anchor fee from a sep38.Quote when a corridor with | ||
| // SEP-38 support is wired in. The sep38.Quote.FeeInBuyAsset field | ||
| // already carries the converted fee. | ||
| anchorFeeReason = "anchor fee available via SEP-38 but no corridor with a published quote server has been priced yet" | ||
|
Comment on lines
+84
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Report DEX anchor absence accurately.
Prompt for AI Agents 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| } | ||
| parts = append(parts, CostPart{ | ||
| Component: CostAnchorFee, | ||
| Amount: decimal.Zero, | ||
| Pct: decimal.Zero, | ||
| Determined: false, | ||
| Reason: anchorFeeReason, | ||
| }) | ||
|
|
||
| // Slippage: undetermined without a comparison across sizes. | ||
| parts = append(parts, CostPart{ | ||
| Component: CostSlippage, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Remove the duplicate
go-versionentry.actions/setup-godefinesgo-versionat Line 45 and Line 46.actionlintand YAMLlint reject this duplicate mapping, so the workflow cannot pass syntax validation. Remove the stale literal at Line 45 and keep${{ env.GO_VERSION }}.Proposed fix
with: - go-version: "1.22" go-version: ${{ env.GO_VERSION }}🧰 Tools
🪛 actionlint (1.7.12)
[error] 46-46: key "go-version" is duplicated in "with" section. previously defined at line:45,col:11. note that this key is case insensitive
(syntax-check)
🪛 YAMLlint (1.37.1)
[error] 46-46: duplication of key "go-version" in mapping
(key-duplicates)
🤖 Prompt for AI Agents
Source: Linters/SAST tools