Skip to content
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

Add fee calculation helpers #537

Open
wants to merge 1 commit into
base: 02-21-link_payer_to_gateway_envelopes
Choose a base branch
from

Conversation

neekolas
Copy link
Contributor

@neekolas neekolas commented Feb 21, 2025

TL;DR

  • Introduces a new fee calculation system with currency handling in picodollars (12 decimal precision).
  • Adds some interfaces for how we can get the current rates from the system. Hardcoded for now, but we'll move to something onchain.

Notes

Handling money in integers feels like the right way to do it, even if we have to convert from PicoDollars to MicroDollars (6 decimals) when we go onchain.

We will have to be careful about that conversion to make sure no one can get free messaging by abusing the rounding.

What changed?

  • Added a new currency package with PicoDollar type for precise financial calculations
  • Created a fee calculation system with components for message, storage, and congestion fees
  • Implemented a rates fetcher interface with a fixed-rate implementation
  • Added currency conversion utilities between dollars, picodollars, and microdollars
  • Included comprehensive test coverage for currency conversions

How to test?

  1. Run the currency package tests:
go test ./pkg/currency
  1. Verify currency conversions:
    • Test dollar to picodollar conversion
    • Validate string representation
    • Check microdollar conversion
  2. Test fee calculations with the fixed rate fetcher implementation

Why make this change?

To provide a precise and reliable fee calculation system that:

  • Handles currency with high precision (12 decimal places)
  • Supports different fee components (message, storage, congestion)
  • Allows for future rate adjustments through the rates fetcher interface
  • Prevents floating-point arithmetic errors in financial calculations

Summary by CodeRabbit

  • New Features

    • Introduced a high-precision currency module that supports conversions between standard monetary units and finer subdivisions.
    • Launched a fee calculation module that computes base and congestion fees based on message parameters and dynamic rate fetching, with robust error handling.
  • Tests

    • Added comprehensive tests to validate currency conversions and fee calculations, including checks for overflow and invalid input scenarios.

Copy link

coderabbitai bot commented Feb 21, 2025

Walkthrough

This pull request introduces advanced currency handling and fee calculation functionality. A new high-precision currency type, PicoDollar, is defined with functions to convert between dollars, picodollars, and microdollars along with string representation. In addition, fee calculation logic is implemented through a FeeCalculator that computes base and congestion fees using rate information fetched via an interface. Supporting components include a fixed rates fetcher, fee-related interfaces, and comprehensive tests to validate conversion accuracy, fee calculations, and error conditions.

Changes

File(s) Change Summary
pkg/currency/currency.go, pkg/currency/currency_test.go Introduced the PicoDollar type with functions: FromDollars, toDollarsTestOnly, ToMicroDollars, and String. Added tests for conversion accuracy and string formatting.
pkg/fees/calculator.go, pkg/fees/fixedRates.go, pkg/fees/interface.go Added fee calculation functionality via FeeCalculator for base and congestion fees with error handling. Defined fee interfaces (IRatesFetcher, IFeeCalculator) and a fixed rates fetcher along with the Rates struct.
pkg/fees/calculator_test.go Added tests to verify fee calculations, including base fee, congestion fee computations, invalid inputs, and overflow conditions.

Sequence Diagram(s)

sequenceDiagram
    participant C as Controller
    participant FC as FeeCalculator
    participant RF as RatesFetcher

    C->>FC: CalculateBaseFee(messageTime, messageSize, storageDuration)
    FC->>RF: GetRates(messageTime)
    RF-->>FC: Return Rates
    FC-->>C: Return base fee (PicoDollar)

    C->>FC: CalculateCongestionFee(messageTime, congestionUnits)
    FC->>RF: GetRates(messageTime)
    RF-->>FC: Return Rates
    FC-->>C: Return congestion fee (PicoDollar)
Loading

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 golangci-lint (1.62.2)

Error: can't load config: the Go language version (go1.23) used to build golangci-lint is lower than the targeted Go version (1.24)
Failed executing command with error: can't load config: the Go language version (go1.23) used to build golangci-lint is lower than the targeted Go version (1.24)


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec1c15 and 77b36c3.

📒 Files selected for processing (6)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/calculator_test.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • pkg/fees/fixedRates.go
  • pkg/fees/calculator.go
  • pkg/currency/currency_test.go
  • pkg/fees/calculator_test.go
  • pkg/fees/interface.go
  • pkg/currency/currency.go
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Build pre-baked anvil-xmtpd
  • GitHub Check: Test (Node)
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

This was referenced Feb 21, 2025
Copy link
Contributor Author

neekolas commented Feb 21, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
pkg/fees/fixedRates.go (1)

11-13: Add input validation for rates parameter.

Consider adding validation to ensure the rates parameter is not nil to prevent potential nil pointer dereferences.

 func NewFixedRatesFetcher(rates *Rates) *FixedRatesFetcher {
+	if rates == nil {
+		panic("rates cannot be nil")
+	}
 	return &FixedRatesFetcher{rates: rates}
 }
pkg/currency/currency_test.go (3)

9-16: Add more test cases for currency conversion.

Consider adding test cases for:

  • Edge cases (0, very large numbers)
  • Negative numbers
  • Numbers with more decimal places
 func TestConversion(t *testing.T) {
+	testCases := []struct {
+		name     string
+		dollars  float64
+		expected PicoDollar
+	}{
+		{"regular case", 1.25, 1250000000000},
+		{"zero", 0, 0},
+		{"large number", 1000000.50, 1000000500000000000},
+		{"negative", -1.25, -1250000000000},
+		{"many decimals", 1.23456789, 1234567890000},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			initial, err := FromDollars(tc.dollars)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, initial)
+
+			converted := initial.toDollars()
+			require.Equal(t, tc.dollars, converted)
+		})
+	}
 }

18-22: Add more test cases for string representation.

Consider adding test cases for:

  • Zero
  • Negative numbers
  • Numbers with trailing zeros
  • Large numbers
 func TestString(t *testing.T) {
+	testCases := []struct {
+		name     string
+		dollars  float64
+		expected string
+	}{
+		{"regular case", 1.25, "1.25"},
+		{"zero", 0, "0.00"},
+		{"negative", -1.25, "-1.25"},
+		{"trailing zeros", 1.50, "1.50"},
+		{"large number", 1000000.25, "1000000.25"},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			initial, err := FromDollars(tc.dollars)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, initial.String())
+		})
+	}
 }

24-28: Add more test cases for microdollars conversion.

Consider adding test cases for:

  • Zero
  • Negative numbers
  • Numbers that might cause overflow
  • Numbers with more decimal places
 func TestToMicroDollars(t *testing.T) {
+	testCases := []struct {
+		name     string
+		dollars  float64
+		expected int64
+	}{
+		{"regular case", 1.25, 1250000},
+		{"zero", 0, 0},
+		{"negative", -1.25, -1250000},
+		{"large number", 1000000.50, 1000000500000},
+		{"many decimals", 1.23456789, 1234568}, // rounds to nearest microdollar
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			initial, err := FromDollars(tc.dollars)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, initial.ToMicroDollars())
+		})
+	}
 }
pkg/fees/calculator.go (1)

9-15: Add input validation for ratesFetcher parameter.

Consider adding validation to ensure the ratesFetcher parameter is not nil to prevent potential nil pointer dereferences.

 func NewFeeCalculator(ratesFetcher IRatesFetcher) *FeeCalculator {
+	if ratesFetcher == nil {
+		panic("ratesFetcher cannot be nil")
+	}
 	return &FeeCalculator{ratesFetcher: ratesFetcher}
 }
pkg/fees/interface.go (1)

9-15: Add validation constraints in comments.

Consider documenting the expected constraints for each field:

  • Valid ranges (e.g., non-negative values)
  • Units of measurement
  • Any other business rules
 // Rates containt the cost for each fee component at a given message time.
 // Values in the rates struct are denominated in USD PicoDollars
 type Rates struct {
-	MessageFee    currency.PicoDollar // The flat per-message fee
-	StorageFee    currency.PicoDollar // The fee per byte-day of storage
-	CongestionFee currency.PicoDollar // The fee per unit of congestion
+	MessageFee    currency.PicoDollar // The flat per-message fee (must be non-negative)
+	StorageFee    currency.PicoDollar // The fee per byte-day of storage (must be non-negative)
+	CongestionFee currency.PicoDollar // The fee per unit of congestion (must be non-negative)
 }
pkg/currency/currency.go (3)

9-14: Add documentation for the PicoDollarsPerDollar constant.

While the type is well-documented, the constant lacks documentation explaining its purpose and significance.

Add a comment above the constant:

 // Picodollar is a type to represent currency with 12 decimal precision
 type PicoDollar int64
 
+// PicoDollarsPerDollar represents the number of picodollars in one dollar (1e12)
 const (
 	PicoDollarsPerDollar = 1e12
 )

26-29: Consider making ToDollars method exported.

The method name toDollars starts with a lowercase letter, making it unexported, while other conversion methods like ToMicroDollars are exported. This inconsistency might make the API less intuitive.

Consider making the method exported for consistency:

-func (p PicoDollar) toDollars() float64 {
+func (p PicoDollar) ToDollars() float64 {
 	return float64(p) / PicoDollarsPerDollar
 }

31-34: Extract magic number and improve documentation.

The method uses a magic number and could benefit from more detailed documentation explaining the conversion ratio.

Consider these improvements:

+const (
+	MicroDollarsPerDollar = 1e6
+)
+
+// ToMicroDollars converts PicoDollars to MicroDollars.
+// The conversion ratio is:
+// 1 Dollar = 1e12 PicoDollars = 1e6 MicroDollars
 func (p PicoDollar) ToMicroDollars() int64 {
-	return int64(p / 1e6)
+	return int64(p / (PicoDollarsPerDollar / MicroDollarsPerDollar))
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 635853b and 3a7522d.

📒 Files selected for processing (5)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Test (Node)
  • GitHub Check: Build pre-baked anvil-xmtpd
🔇 Additional comments (5)
pkg/fees/fixedRates.go (2)

5-9: LGTM! Clear documentation and focused design.

The comments effectively explain the purpose and use case of the FixedRatesFetcher struct.


15-17: LGTM! Clean implementation of the IRatesFetcher interface.

The method correctly implements the interface contract and follows Go conventions for unused parameters.

pkg/fees/interface.go (1)

17-21: LGTM! Well-documented and focused interface design.

The interface follows Go conventions and its purpose is clearly documented.

pkg/currency/currency.go (2)

1-7: LGTM! Clean package structure and imports.

The package name is appropriate, and imports are well-organized with no unnecessary dependencies.


9-38:

❓ Verification inconclusive

Verify precision requirements for currency handling.

The implementation uses 12 decimal places of precision (picodollars). Please verify if this level of precision aligns with the system requirements and industry standards for currency handling.


🌐 Web query:

What is the standard precision for handling cryptocurrency transactions?

💡 Result:

Cryptocurrency transaction precision varies significantly across different digital assets and fiat currencies, with standards shaped by both technical protocols and practical usability considerations. Here's a detailed breakdown:

Bitcoin (BTC) Precision

  • Maximum precision: 8 decimal places (1 satoshi = 0.00000001 BTC) [10]
  • Internal calculations: Up to 10 decimal places in APIs, though only 8 are usable on-chain [2]
  • Display standards: Often rounded to 5 decimal places for user interfaces [2]

Ethereum (ETH) and ERC-20 Tokens

  • Standard precision: 18 decimal places (1 wei = 0.000000000000000001 ETH)
  • Gwei subunit: Commonly used for gas fees (1 gwei = 0.000000001 ETH) [10]

Fiat Currency Handling

  • USD calculations:
    • Internal API precision: 4 decimal places
    • Display standard: 2 decimal places [2]
  • EUR/GBP: Typically follow similar patterns to USD

Token Variability

  • General range: Most tokens use 8–18 decimal places depending on their protocol design [8]
  • Examples:
    • Stablecoins like USDT: Often mirror fiat standards (2 displayed decimals)
    • High-supply tokens (e.g., SHIB): May use full 18-decimal precision

Implementation Considerations

  1. APIs vs On-Chain:

    • Exchanges like Kraken perform internal calculations at higher precision than displayed (e.g., BTC at 10 decimals internally vs 5 displayed) [2]
    • Blockchain networks enforce protocol-defined decimal limits
  2. Rounding Practices:

    • Transactions exceeding display decimals get truncated/rounded (e.g., $1.45406823 → $1.4541 in APIs) [2]
    • Mismatches require alignment between local calculations and API specifications

This tiered approach balances technical accuracy with human readability while maintaining consistency across wallets, exchanges, and blockchain networks.

Citations:


Confirm the chosen precision for PicoDollar conversion aligns with system and industry requirements.

  • The file uses 12 decimal places (PicoDollars) for representing currency.
  • Web sources indicate that cryptocurrency precisions vary widely (e.g., BTC uses 8 decimals and ETH uses 18 decimals), while fiat currencies often use far fewer decimals.
  • Please verify that the 12-decimal scheme meets your intended precision for both internal calculations and external displays, and consider whether any adjustments or additional tests (e.g., for rounding accuracy and overflow safety) are necessary.

@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch 2 times, most recently from 029ba8f to 8832f00 Compare February 21, 2025 22:53
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
pkg/fees/calculator.go (3)

18-31: Solid input validation.

Checking for negative messageSize and storageDurationDays is good practice for error prevention. Consider clarifying how zero values should be treated in domain-specific documentation (if zero storage days are valid).


38-49: Effective overflow checks—consider modularizing.

The overflow checks for storage fees are sensible. As an optional improvement, extracting these checks into a helper function could reduce repetition and improve clarity. If fee values can grow significantly, exploring big.Int may provide safer arithmetic.


68-73: Congestion fee overflow checks—centralize logic.

Similar overflow handling is repeated here. Refactoring shared multiplication checks into a separate helper method could simplify updates if the logic evolves.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 029ba8f and 8832f00.

📒 Files selected for processing (5)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/currency/currency_test.go
  • pkg/currency/currency.go
  • pkg/fees/interface.go
  • pkg/fees/fixedRates.go
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Test (Node)
  • GitHub Check: Build pre-baked anvil-xmtpd
🔇 Additional comments (5)
pkg/fees/calculator.go (5)

1-9: Package and imports look good.

The package name and imports are clear and minimal, aligning with Go conventions.


10-16: Well-structured constructor.

Returning a pointer to FeeCalculator is appropriate. The code is concise and straightforward.


33-36: Good error handling on rate fetch.

Returning the error immediately ensures clarity. You might consider wrapping the error to provide more context, but this is optional.


52-61: Validate congestion range.

Restricting congestion to 0–100 seems reasonable in most scenarios. Double-check domain requirements to ensure this range meets all future needs.

Would you like me to scan the codebase for calls to CalculateCongestionFee to confirm no assumptions exceed 100%?


63-66: Consistent rate fetching.

This mirrors the approach in CalculateBaseFee, promoting consistency and preventing silent failures.

@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch 2 times, most recently from 41d4e40 to d9eb111 Compare February 21, 2025 23:06
@neekolas neekolas marked this pull request as ready for review February 24, 2025 15:47
@neekolas neekolas requested a review from a team as a code owner February 24, 2025 15:47
@neekolas neekolas force-pushed the 02-21-link_payer_to_gateway_envelopes branch 2 times, most recently from 7b21dab to eb0b89a Compare February 24, 2025 15:49
@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from d9eb111 to 58be2dd Compare February 24, 2025 15:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
pkg/fees/calculator_test.go (5)

12-26: Add documentation for test rates.

Consider adding comments to explain what these rates represent and how they were chosen for testing purposes. This will help other developers understand the test scenarios better.

 const (
+	// Fixed rates in picodollars for testing purposes
+	// These values are chosen to make test calculations easy to verify
 	RATE_MESSAGE_FEE    = 100
 	RATE_STORAGE_FEE    = 50
 	RATE_CONGESTION_FEE = 200
 )

28-40: Enhance test coverage with table-driven tests.

Consider converting this to a table-driven test to cover more scenarios:

  • Zero message size
  • Zero storage duration
  • Maximum valid values
 func TestCalculateBaseFee(t *testing.T) {
-	calculator := setupCalculator()
-
-	messageTime := time.Now()
-	messageSize := int64(100)
-	storageDurationDays := int64(1)
-
-	baseFee, err := calculator.CalculateBaseFee(messageTime, messageSize, storageDurationDays)
-	require.NoError(t, err)
-
-	expectedFee := RATE_MESSAGE_FEE + (RATE_STORAGE_FEE * messageSize * storageDurationDays)
-	require.Equal(t, currency.PicoDollar(expectedFee), baseFee)
+	tests := []struct {
+		name               string
+		messageSize        int64
+		storageDuration    int64
+		expectedFee        currency.PicoDollar
+		expectError        bool
+	}{
+		{
+			name:            "normal case",
+			messageSize:     100,
+			storageDuration: 1,
+			expectedFee:     currency.PicoDollar(RATE_MESSAGE_FEE + (RATE_STORAGE_FEE * 100 * 1)),
+		},
+		{
+			name:            "zero message size",
+			messageSize:     0,
+			storageDuration: 1,
+			expectedFee:     currency.PicoDollar(RATE_MESSAGE_FEE),
+		},
+		{
+			name:            "zero storage duration",
+			messageSize:     100,
+			storageDuration: 0,
+			expectedFee:     currency.PicoDollar(RATE_MESSAGE_FEE),
+		},
+	}
+
+	calculator := setupCalculator()
+	messageTime := time.Now()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			baseFee, err := calculator.CalculateBaseFee(messageTime, tt.messageSize, tt.storageDuration)
+			if tt.expectError {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tt.expectedFee, baseFee)
+		})
+	}
 }

42-53: Enhance test coverage with table-driven tests.

Similar to the base fee test, consider converting this to a table-driven test to cover more scenarios:

  • 0% congestion
  • 100% congestion
  • Edge cases around the valid range
 func TestCalculateCongestionFee(t *testing.T) {
-	calculator := setupCalculator()
-
-	messageTime := time.Now()
-	congestionPercent := int64(50)
-
-	congestionFee, err := calculator.CalculateCongestionFee(messageTime, congestionPercent)
-	require.NoError(t, err)
-
-	expectedFee := RATE_CONGESTION_FEE * congestionPercent
-	require.Equal(t, currency.PicoDollar(expectedFee), congestionFee)
+	tests := []struct {
+		name             string
+		congestionPercent int64
+		expectedFee      currency.PicoDollar
+		expectError      bool
+	}{
+		{
+			name:             "50% congestion",
+			congestionPercent: 50,
+			expectedFee:      currency.PicoDollar(RATE_CONGESTION_FEE * 50),
+		},
+		{
+			name:             "no congestion",
+			congestionPercent: 0,
+			expectedFee:      0,
+		},
+		{
+			name:             "full congestion",
+			congestionPercent: 100,
+			expectedFee:      currency.PicoDollar(RATE_CONGESTION_FEE * 100),
+		},
+	}
+
+	calculator := setupCalculator()
+	messageTime := time.Now()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			congestionFee, err := calculator.CalculateCongestionFee(messageTime, tt.congestionPercent)
+			if tt.expectError {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tt.expectedFee, congestionFee)
+		})
+	}
 }

55-71: Add overflow test for congestion fee calculation.

The overflow test only covers base fee calculation. Consider adding test cases for congestion fee overflow and more specific overflow scenarios.

 func TestOverflow(t *testing.T) {
-	calculator := setupCalculator()
-
-	messageTime := time.Now()
-
-	// Test overflow in CalculateBaseFee
-	messageSize := math.MaxInt64
-	storageDurationDays := math.MaxInt64
-
-	_, err := calculator.CalculateBaseFee(
-		messageTime,
-		int64(messageSize),
-		int64(storageDurationDays),
-	)
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "storage fee calculation overflow")
+	tests := []struct {
+		name        string
+		testFunc    func(*FeeCalculator) error
+		errorString string
+	}{
+		{
+			name: "base fee storage overflow",
+			testFunc: func(c *FeeCalculator) error {
+				_, err := c.CalculateBaseFee(
+					time.Now(),
+					math.MaxInt64,
+					math.MaxInt64,
+				)
+				return err
+			},
+			errorString: "storage fee calculation overflow",
+		},
+		{
+			name: "congestion fee overflow",
+			testFunc: func(c *FeeCalculator) error {
+				_, err := c.CalculateCongestionFee(
+					time.Now(),
+					100, // Even valid congestion percent can overflow with large fee
+				)
+				return err
+			},
+			errorString: "congestion fee calculation overflow",
+		},
+	}
+
+	calculator := setupCalculator()
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.testFunc(calculator)
+			require.Error(t, err)
+			require.Contains(t, err.Error(), tt.errorString)
+		})
+	}
 }

73-85: Convert to table-driven test for consistency.

The test covers essential cases but could be more consistent with other tests by using a table-driven approach.

 func TestInvalidCongestionPercent(t *testing.T) {
-	calculator := setupCalculator()
-
-	messageTime := time.Now()
-
-	_, err := calculator.CalculateCongestionFee(messageTime, int64(101))
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "congestionPercent must be between 0 and 100")
-
-	_, err = calculator.CalculateCongestionFee(messageTime, int64(-1))
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "congestionPercent must be between 0 and 100")
+	tests := []struct {
+		name             string
+		congestionPercent int64
+	}{
+		{
+			name:             "above maximum",
+			congestionPercent: 101,
+		},
+		{
+			name:             "below minimum",
+			congestionPercent: -1,
+		},
+	}
+
+	calculator := setupCalculator()
+	messageTime := time.Now()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := calculator.CalculateCongestionFee(messageTime, tt.congestionPercent)
+			require.Error(t, err)
+			require.Contains(t, err.Error(), "congestionPercent must be between 0 and 100")
+		})
+	}
 }
pkg/fees/calculator.go (1)

10-16: Add nil check in constructor.

Consider adding a nil check for the ratesFetcher parameter to prevent potential panics.

 func NewFeeCalculator(ratesFetcher IRatesFetcher) *FeeCalculator {
+	if ratesFetcher == nil {
+		panic("ratesFetcher cannot be nil")
+	}
 	return &FeeCalculator{ratesFetcher: ratesFetcher}
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8832f00 and d9eb111.

📒 Files selected for processing (6)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/calculator_test.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/fees/fixedRates.go
  • pkg/currency/currency_test.go
  • pkg/currency/currency.go
  • pkg/fees/interface.go
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Graphite / mergeability_check
🔇 Additional comments (2)
pkg/fees/calculator.go (2)

18-50: Well-implemented fee calculation with proper safeguards!

The implementation:

  • Uses picodollars for precise calculations
  • Has thorough input validation
  • Includes overflow checks for large numbers
  • Provides clear error messages

52-73: Well-implemented congestion fee calculation!

The implementation:

  • Properly validates congestion percentage range
  • Includes overflow checks
  • Uses picodollars for precise calculations

@mkysel
Copy link
Collaborator

mkysel commented Feb 24, 2025

I will wait for the downstack PRs to resolve. These graphite layers are hard for me to review.

@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from 58be2dd to 0dc26f5 Compare February 24, 2025 16:10
@neekolas
Copy link
Contributor Author

@mkysel I think you were looking while it was restacking. The diff should be small and incremental again now

Comment on lines +20 to +21
messageSize int64,
storageDurationDays int64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can probably get away with uint32 for both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, but then we immediately have to convert to multiply against the int64 picodollars


func (c *FeeCalculator) CalculateCongestionFee(
messageTime time.Time,
congestionPercent int64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can get away with uint8 here

@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch 2 times, most recently from f318974 to 46352ee Compare February 24, 2025 16:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
pkg/fees/calculator.go (3)

20-21: Document the rationale for using int64.

While uint32 could be used for these parameters, using int64 aligns with the PicoDollar type and avoids additional type conversions during calculations. Consider adding a comment explaining this design decision.

 func (c *FeeCalculator) CalculateBaseFee(
 	messageTime time.Time,
+	// Using int64 to align with PicoDollar type and avoid conversions
 	messageSize int64,
 	storageDurationDays int64,

54-54: Consider using uint8 for congestionPercent.

Since congestionPercent is validated to be between 0 and 100, using uint8 would be more type-safe and better express the intent.


68-68: Document the percentage calculation.

The congestion fee calculation multiplies directly by the percentage without dividing by 100. If this is intentional (i.e., if CongestionFee is pre-scaled), please add a comment to clarify this design decision.

+	// CongestionFee is pre-scaled, so we multiply directly by the percentage (0-100)
 	result := rates.CongestionFee * currency.PicoDollar(congestionPercent)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9eb111 and f318974.

📒 Files selected for processing (6)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/calculator_test.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/fees/fixedRates.go
  • pkg/currency/currency.go
  • pkg/currency/currency_test.go
  • pkg/fees/calculator_test.go
  • pkg/fees/interface.go
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Test (Node)
  • GitHub Check: Build pre-baked anvil-xmtpd
🔇 Additional comments (2)
pkg/fees/calculator.go (2)

10-16: LGTM! Clean implementation with proper dependency injection.

The struct definition and constructor follow Go best practices, making the code maintainable and testable.


18-50: LGTM! Robust implementation with proper validation and overflow protection.

The implementation successfully addresses previous review feedback by including input validation and overflow protection. The calculation logic is sound and handles edge cases appropriately.

@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from 46352ee to 13eca3f Compare February 24, 2025 17:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
pkg/fees/interface.go (3)

9-11: Fix typo in documentation.

There's a typo in the comment: "containt" should be "contains".

-// Rates containt the cost for each fee component at a given message time.
+// Rates contains the cost for each fee component at a given message time.

23-23: Add interface documentation.

The interface lacks documentation explaining its purpose and methods. Consider adding documentation that describes:

  • The purpose of the interface
  • When to use each method
  • The meaning of the parameters
  • The expected behavior when errors occur
+// IFeeCalculator calculates various types of fees for messages.
+// It uses high-precision PicoDollar for all fee calculations to avoid floating-point errors.
 type IFeeCalculator interface {

29-33: Improve parameter naming for clarity.

The parameter name congestionUnits is not self-explanatory. Consider using a more descriptive name that indicates what these units represent and how they affect the fee calculation.

 	CalculateCongestionFee(
 		messageTime time.Time,
-		congestionUnits int64,
+		congestionPercent int64,
 	) (currency.PicoDollar, error)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f318974 and 13eca3f.

📒 Files selected for processing (6)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/calculator_test.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/fees/fixedRates.go
  • pkg/currency/currency_test.go
  • pkg/fees/calculator_test.go
  • pkg/currency/currency.go
  • pkg/fees/calculator.go
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Build pre-baked anvil-xmtpd
  • GitHub Check: Test (Node)
🔇 Additional comments (2)
pkg/fees/interface.go (2)

17-21: Well-designed interface with clear documentation!

The interface follows good design principles:

  • Single responsibility
  • Clear documentation explaining the rationale
  • Proper error handling

24-28: Fix parameter type inconsistency with implementation.

The interface defines messageSize and storageDurationDays as int64, but the implementation uses int. This mismatch could cause issues.

@neekolas neekolas force-pushed the 02-21-link_payer_to_gateway_envelopes branch from eb0b89a to ee5308f Compare February 25, 2025 00:31
@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from 13eca3f to ce9b947 Compare February 25, 2025 00:31
@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from ce9b947 to 1ec1c15 Compare February 25, 2025 19:42
@neekolas neekolas requested a review from a team February 25, 2025 19:45
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
pkg/testutils/envelopes/envelopes.go (1)

107-107: Consider consistent usage of TargetOriginator across envelope creation functions.
At line 76, TargetOriginator is assigned via new(uint32), whereas here it is assigned by value (nodeID). This inconsistency might lead to confusion in future maintenance or usage of these envelopes.

pkg/fees/calculator.go (2)

10-16: Add doc comments for clarity.
Consider adding brief doc comments for both the struct and constructor to enhance code readability and maintainability.


52-77: Align the error message with the variable name.
The variable is named congestionUnits, but the error message refers to it as congestionPercent. Update the error message to avoid confusion:

- "congestionPercent must be between 0 and 100, got %d"
+ "congestionUnits must be between 0 and 100, got %d"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13eca3f and 1ec1c15.

📒 Files selected for processing (7)
  • pkg/currency/currency.go (1 hunks)
  • pkg/currency/currency_test.go (1 hunks)
  • pkg/fees/calculator.go (1 hunks)
  • pkg/fees/calculator_test.go (1 hunks)
  • pkg/fees/fixedRates.go (1 hunks)
  • pkg/fees/interface.go (1 hunks)
  • pkg/testutils/envelopes/envelopes.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/fees/fixedRates.go
  • pkg/fees/calculator_test.go
  • pkg/fees/interface.go
  • pkg/currency/currency.go
  • pkg/currency/currency_test.go
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Graphite / mergeability_check
  • GitHub Check: Test (Node)
  • GitHub Check: Build pre-baked anvil-xmtpd
🔇 Additional comments (2)
pkg/fees/calculator.go (2)

1-9: Introduced package and imports look fine.
No issues or improvements needed here.


18-50: Base fee calculation appears robust.
The checks for negative inputs and overflow are appropriate, and returning an error when overflow is detected is a sensible approach.

@neekolas neekolas force-pushed the 02-21-link_payer_to_gateway_envelopes branch from ee5308f to 8d666c7 Compare February 25, 2025 20:21
@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from 1ec1c15 to b727641 Compare February 25, 2025 20:21
Comment on lines +58 to +59
"congestionPercent must be between 0 and 100, got %d",
congestionUnits,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider renaming congestionUnits to congestionPercent everywhere, so the expected input is clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You and @mkysel will have to hash that one out. His suggestion was to switch from percent to units, since we don't divide it by 100

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha :) yeah! I guess the text of the error message should match the name of the variable

@neekolas neekolas force-pushed the 02-21-add_fee_calculation_helpers branch from b727641 to 77b36c3 Compare February 26, 2025 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants