Skip to content

Conversation

@louisinger
Copy link
Collaborator

@louisinger louisinger commented Dec 5, 2025

This PR is introducing the fee system specified by #639

ark-lib/arkfee package is exporting all the structs and types parsing and evaluating CEL programs.

Add new env variables letting to specify the fee model for intent input ARKD_INTENT_INPUT_FEE_PROGRAM , and intent output : ARKD_INTENT_OUTPUT_FEE_PROGRAM.

the CEL envs are speicifed in arkfee/README.md
Example of valid CEL programs:

inputType == 'note' || inputType == 'recoverable' ? 0.0 : amount*0.01 

outputType == 'onchain' ? 200.0 : 0.0

expiry - now() < double(duration('5m').getSeconds()) ? 0.0 : amount / 2.0

outputType == 'onchain' ? amount * 0.2 : 0.0

@Kukks @altafan please review

it closes #857
it closes #856

Summary by CodeRabbit

  • New Features

    • Added EstimateFee API/RPC to request dynamic fee estimates.
    • Introduced configurable fee estimator with CEL-based programs for offchain/onchain inputs and outputs.
  • Breaking Changes

    • Replaced single OnchainOutputFee with four program-based fee config fields.
    • Removed includeOffchain/includeOnchain flags from SDK Balance and CollaborativeExit calls.
  • Documentation

    • Added arkfee README with usage examples.
  • Tests

    • Added estimator unit tests and an end-to-end fee test.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Walkthrough

Adds a CEL-based dynamic fee estimator and integrates it into the service/config, replacing a static on-chain fee. Introduces an RPC POST /v1/batch/estimateFee, arkfee package with CEL environments/programs, a FeeManager port and ArkFee manager implementation, updated tests, and config/env variables to supply fee programs.

Changes

Cohort / File(s) Summary
API Spec & Protobuf
api-spec/openapi/swagger/ark/v1/service.openapi.json, api-spec/protobuf/ark/v1/service.proto
Added POST /v1/batch/estimateFee (ArkService_EstimateFee) with EstimateFeeRequest (Intent) and EstimateFeeResponse (fee int64).
Core service & types
internal/core/application/service.go, internal/core/application/types.go, internal/core/application/utils.go, internal/core/ports/fee.go
Replaced static onchain fee with ports.FeeManager; added EstimateFee service method and IntentFeeInfo adjustments; removed computeIntentFees helper; added FeeManager interface.
Config & wiring
internal/config/config.go, docker-compose.regtest.yml, go.mod
Replaced OnchainOutputFee with four intent fee program config fields and fee manager initialization; updated env vars and go.mod for CEL-related deps.
Fee estimator implementation
pkg/ark-lib/arkfee/types.go, pkg/ark-lib/arkfee/estimator.go, pkg/ark-lib/arkfee/estimator_test.go, pkg/ark-lib/arkfee/README.md, pkg/ark-lib/arkfee/testdata/*
New arkfee package: types, Estimator (parsing/compiling CEL programs), evaluation methods, tests with embedded valid/invalid fixtures, and README.
CEL environments & helpers
pkg/ark-lib/arkfee/celenv/variables.go, pkg/ark-lib/arkfee/celenv/functions.go, pkg/ark-lib/arkfee/celenv/intent_*.go
Defined CEL variables, now() function, and pre-initialized CEL envs for each program type.
Fee manager integration
internal/infrastructure/feemanager/arkfee.go
Implements ArkFee-based FeeManager adapter translating internal types to arkfee types and calling the Estimator.
gRPC handler & parsing
internal/interface/grpc/handlers/arkservice.go, internal/interface/grpc/handlers/parser.go, internal/interface/grpc/permissions/permissions.go
Added EstimateFee RPC handler and parseEstimateFeeIntent; updated permissions and proto-to-proto conversions for fee fields.
Intent messages & proof
pkg/ark-lib/intent/message.go, pkg/ark-lib/intent/message_test.go, pkg/ark-lib/intent/testdata/message_fixtures.json, pkg/ark-lib/intent/proof.go
Added EstimateFeeMessage type and intent message fixtures; added Proof.Fees() method to compute fees from Proof.
CLI & SDK call sites
pkg/ark-cli/main.go, pkg/ark-cli/go.mod
Removed compute-expiry args from Balance/CollaborativeExit calls; updated module deps.
E2E tests & utils
internal/test/e2e/e2e_test.go, internal/test/e2e/utils_test.go
Updated tests to new SDK signatures; added TestFee and helpers to restart arkd with fee-program env vars.
Misc & deps
go.mod (root), pkg/ark-lib/go.mod, pkg/ark-cli/go.mod, pkg/errors/errors.go
Bumped/added CEL and proto-related dependencies; added INTENT_FEE_EVALUATION_FAILED error code.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant gRPC as gRPC Handler
    participant Parser as Intent Parser
    participant Service as Core Service
    participant FeeMgr as FeeManager (arkFeeManager)
    participant Estimator as arkfee.Estimator
    participant CEL as CEL Env/Evaluator

    Client->>gRPC: EstimateFee(EstimateFeeRequest)
    gRPC->>Parser: parseEstimateFeeIntent(intent)
    Parser-->>gRPC: (proof, EstimateFeeMessage)
    gRPC->>Service: EstimateFee(ctx, proof, message)
    Service->>FeeMgr: GetIntentFees(ctx, boardingInputs, vtxoInputs, onchainOutputs, offchainOutputs)
    FeeMgr->>Estimator: Eval(offchainInputs, onchainInputs, offchainOutputs, onchainOutputs)
    Estimator->>CEL: Compile/Evaluate programs with variables
    CEL-->>Estimator: FeeAmount per item
    Estimator->>FeeMgr: Total FeeAmount
    FeeMgr-->>Service: fee (satoshis)
    Service-->>gRPC: EstimateFeeResponse(fee)
    gRPC-->>Client: Response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Focus areas:
    • arkfee package: CEL parsing/compilation correctness, numeric typing and ToSatoshis conversion, and test fixture coverage.
    • FeeManager adapter: conversions between internal types and arkfee types (expiry/birth/time handling).
    • Config/service wiring: initialization order and error propagation when fee manager fails.
    • gRPC/parser: intent parsing and error mapping to gRPC codes.
    • E2E tests: container restart helper and env var propagation to ensure deterministic test runs.

Possibly related PRs

Suggested reviewers

  • Kukks
  • altafan

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes: removal of the 'includeOffchain' parameter from Balance and CollaborativeExit SDK methods, plus removal of 'computeExpiration' parameter from CLI commands. These changes are unrelated to the fee system requirements and appear to be API refactoring. Either remove the SDK/CLI API signature changes and revert to the original method signatures, or create a separate PR to address these API removals, as they fall outside the scope of introducing the fee system.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Introduce fee system using CEL formulas' clearly summarizes the main change by highlighting the introduction of a CEL-based fee system, which aligns with the substantial modifications across API specs, configuration, and infrastructure components.
Linked Issues check ✅ Passed The PR addresses both linked issues: #857 requires EstimateIntentFee RPC (implemented via EstimateFee endpoint in OpenAPI and gRPC specs, plus handler and service methods); #856 requires fee computation and verification (implemented via FeeManager integration with CEL-based fee estimation in service layer).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch arkfee

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@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: 3

🧹 Nitpick comments (9)
pkg/ark-cli/main.go (2)

543-546: Consider removing the unused computeExpiration variable.

The computeExpiration variable is read from the flag on Line 543 but is no longer passed to CollaborativeExit on Line 546. This suggests it may be unused in this code path.

Apply this diff to remove the unused variable from this path:

-	computeExpiration := ctx.Bool(enableExpiryCoinselectFlag.Name)
 	if len(onchainReceivers) > 0 {
 		txid, err := arkSdkClient.CollaborativeExit(
 			ctx.Context, onchainReceivers[0].To, onchainReceivers[0].Amount,

Note: If computeExpiration is still needed for the SendOffChain call on Line 554, the variable assignment should be moved after the if len(onchainReceivers) > 0 block.


199-199: Consider removing the unused expiryDetailsFlag from the balance command.

The expiryDetailsFlag is still listed in the balance command's flags, but the flag value is no longer passed to the Balance method (Line 382). This suggests the flag may no longer be needed.

Apply this diff if the flag is no longer used:

 	balanceCommand = cli.Command{
 		Name:  "balance",
 		Usage: "Shows onchain and offchain Ark wallet balance",
 		Action: func(ctx *cli.Context) error {
 			return balance(ctx)
 		},
-		Flags: []cli.Flag{expiryDetailsFlag},
 	}
internal/interface/grpc/handlers/parser.go (1)

51-67: Estimate-fee intent parsing correctly mirrors register flow

parseEstimateFeeIntent reuses parseIntentProofTx, validates message presence, and delegates JSON/type validation to EstimateFeeMessage.Decode, which keeps the parsing path consistent with other intent types. You might optionally specialize the error message to "invalid estimate-fee intent message" for easier debugging, but the current behavior is functionally fine.

pkg/ark-lib/arkfee/estimator_test.go (1)

76-86: Avoid strict float equality for weighted fee expectations

require.Equal on FeeAmount for values like 56.3 depends on exact float representation of the CEL evaluation matching the literal, which can be brittle.

Consider using a delta-based assertion instead, e.g.:

-					require.Equal(t, testCase.expected, result)
+					require.InDelta(t, float64(testCase.expected), float64(result), 1e-9)

and similarly for other non-integer expectations. This keeps the tests robust against minor floating‑point rounding differences while still tightly checking correctness.

Also applies to: 192-245

pkg/ark-lib/intent/message.go (1)

12-17: EstimateFeeMessage reuse of RegisterMessage structure with distinct type check looks sound

Defining EstimateFeeMessage on top of RegisterMessage and validating m.Type == IntentMessageTypeEstimateFee in Decode cleanly reuses the existing message shape while preventing cross-use of register payloads on the estimate-fee path. As with the other messages, callers just need to ensure BaseMessage.Type is set correctly before Encode.

Also applies to: 62-82

api-spec/openapi/swagger/ark/v1/service.openapi.json (1)

90-130: EstimateFee OpenAPI path and schemas align with the new RPC; consider documenting required intent type

The /v1/batch/estimateFee endpoint and its EstimateFeeRequest/EstimateFeeResponse schemas are consistent with the existing intent-based APIs and correctly describe the fee as an int64 in satoshis.

To make client usage clearer, consider extending the description to note that the embedded Intent’s message must carry {"type": "estimate-fee", ...} (matching IntentMessageTypeEstimateFee), even though the payload shape mirrors the register intent.

Also applies to: 633-651

pkg/ark-lib/arkfee/estimator.go (1)

79-99: Consider validating that individual fees are non-negative.

The Eval function aggregates fees from all inputs and outputs. If a CEL program returns a negative value, it could result in unexpected behavior (fees reducing the total or going negative). Depending on business requirements, you may want to add validation.

If negative fees should not be allowed:

 func (e *Estimator) Eval(inputs []Input, outputs []Output) (FeeAmount, error) {
 	fee := FeeAmount(0)
 
 	for _, input := range inputs {
 		inputFee, err := e.EvalInput(input)
 		if err != nil {
 			return 0, err
 		}
+		if inputFee < 0 {
+			return 0, fmt.Errorf("negative input fee not allowed: %v", inputFee)
+		}
 		fee += inputFee
 	}
 
 	for _, output := range outputs {
 		outputFee, err := e.EvalOutput(output)
 		if err != nil {
 			return 0, err
 		}
+		if outputFee < 0 {
+			return 0, fmt.Errorf("negative output fee not allowed: %v", outputFee)
+		}
 		fee += outputFee
 	}
 
 	return fee, nil
 }
pkg/ark-lib/arkfee/types.go (1)

10-14: Consider adding validation for FeeAmount edge cases.

The ToSatoshis() method directly casts math.Ceil(float64(f)) to int64 without checking for:

  1. Negative fee amounts (which would be invalid)
  2. Values exceeding math.MaxInt64 (causing overflow)
 func (f FeeAmount) ToSatoshis() int64 {
+	if f < 0 {
+		return 0
+	}
+	ceiling := math.Ceil(float64(f))
+	if ceiling > float64(math.MaxInt64) {
+		return math.MaxInt64
+	}
-	return int64(math.Ceil(float64(f)))
+	return int64(ceiling)
 }
internal/core/application/service.go (1)

1570-1574: Address the Weight calculation TODOs.

The Weight field is hardcoded to 0 in multiple places with TODO comments. This field represents the "weighted liquidity lockup ratio" and is part of the fee calculation inputs. Leaving it at 0 may result in incorrect fee estimates if the CEL programs reference this variable.

Do you want me to help implement the Weight calculation, or should this be tracked in a separate issue? The logic would depend on how you define the liquidity lockup ratio (e.g., based on vtxo age, amount, or exit delay).

Also applies to: 1692-1698

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c28ac48 and cb2bbda.

⛔ Files ignored due to path filters (7)
  • api-spec/protobuf/gen/ark/v1/service.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • api-spec/protobuf/gen/ark/v1/service.pb.rgw.go is excluded by !**/gen/**
  • api-spec/protobuf/gen/ark/v1/service_grpc.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • go.sum is excluded by !**/*.sum
  • pkg/ark-cli/go.sum is excluded by !**/*.sum
  • pkg/ark-lib/go.sum is excluded by !**/*.sum
  • pkg/errors/go.sum is excluded by !**/*.sum
📒 Files selected for processing (25)
  • api-spec/openapi/swagger/ark/v1/service.openapi.json (2 hunks)
  • api-spec/protobuf/ark/v1/service.proto (2 hunks)
  • envs/arkd.light.env (1 hunks)
  • go.mod (7 hunks)
  • internal/config/config.go (9 hunks)
  • internal/core/application/service.go (12 hunks)
  • internal/core/application/types.go (1 hunks)
  • internal/interface/grpc/handlers/arkservice.go (1 hunks)
  • internal/interface/grpc/handlers/parser.go (1 hunks)
  • internal/test/e2e/e2e_test.go (18 hunks)
  • pkg/ark-cli/go.mod (4 hunks)
  • pkg/ark-cli/main.go (4 hunks)
  • pkg/ark-lib/arkfee/celenv/functions.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/intent_input.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/intent_output.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/variables.go (1 hunks)
  • pkg/ark-lib/arkfee/estimator.go (1 hunks)
  • pkg/ark-lib/arkfee/estimator_test.go (1 hunks)
  • pkg/ark-lib/arkfee/types.go (1 hunks)
  • pkg/ark-lib/go.mod (2 hunks)
  • pkg/ark-lib/intent/message.go (2 hunks)
  • pkg/ark-lib/intent/message_test.go (1 hunks)
  • pkg/ark-lib/intent/testdata/message_fixtures.json (1 hunks)
  • pkg/errors/errors.go (1 hunks)
  • pkg/errors/go.mod (0 hunks)
💤 Files with no reviewable changes (1)
  • pkg/errors/go.mod
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T10:58:41.042Z
Learnt from: louisinger
Repo: arkade-os/arkd PR: 691
File: internal/core/application/service.go:557-562
Timestamp: 2025-08-19T10:58:41.042Z
Learning: In the arkd SubmitOffchainTx method, using the checkpoint PSBT input's tapscript (forfeit path) for the VtxoInput.Tapscript field is the correct behavior, not a bug as initially thought. The system correctly handles the relationship between checkpoint inputs and Ark transaction inputs.

Applied to files:

  • internal/core/application/service.go
🧬 Code graph analysis (7)
pkg/ark-lib/intent/message_test.go (1)
pkg/ark-lib/intent/message.go (2)
  • IntentMessageTypeEstimateFee (16-16)
  • EstimateFeeMessage (62-62)
internal/interface/grpc/handlers/arkservice.go (2)
api-spec/protobuf/gen/ark/v1/service.pb.go (6)
  • EstimateFeeRequest (339-344)
  • EstimateFeeRequest (357-357)
  • EstimateFeeRequest (372-374)
  • EstimateFeeResponse (383-388)
  • EstimateFeeResponse (401-401)
  • EstimateFeeResponse (416-418)
pkg/errors/errors.go (1)
  • Error (39-46)
internal/core/application/types.go (3)
pkg/ark-lib/intent/proof.go (1)
  • Proof (39-41)
pkg/ark-lib/intent/message.go (1)
  • EstimateFeeMessage (62-62)
pkg/errors/errors.go (1)
  • Error (39-46)
pkg/ark-cli/main.go (2)
internal/core/application/admin.go (1)
  • Balance (484-487)
api-spec/protobuf/gen/ark/v1/wallet.pb.go (3)
  • Balance (653-659)
  • Balance (672-672)
  • Balance (687-689)
pkg/ark-lib/arkfee/estimator.go (3)
pkg/ark-lib/arkfee/celenv/intent_input.go (1)
  • IntentInputEnv (7-7)
pkg/ark-lib/arkfee/celenv/intent_output.go (1)
  • IntentOutputEnv (7-7)
pkg/ark-lib/arkfee/types.go (4)
  • Input (25-31)
  • FeeAmount (10-10)
  • Output (55-58)
  • OutputType (48-48)
internal/interface/grpc/handlers/parser.go (5)
internal/infrastructure/db/postgres/sqlc/queries/models.go (1)
  • Intent (33-38)
internal/infrastructure/db/sqlite/sqlc/queries/models.go (1)
  • Intent (31-36)
api-spec/protobuf/gen/ark/v1/types.pb.go (3)
  • Intent (441-447)
  • Intent (460-460)
  • Intent (475-477)
pkg/ark-lib/intent/proof.go (1)
  • Proof (39-41)
pkg/ark-lib/intent/message.go (1)
  • EstimateFeeMessage (62-62)
pkg/ark-lib/arkfee/estimator_test.go (2)
pkg/ark-lib/arkfee/types.go (5)
  • Input (25-31)
  • FeeAmount (10-10)
  • InputTypeRecoverable (19-19)
  • Output (55-58)
  • OutputTypeOnchain (52-52)
pkg/ark-lib/arkfee/estimator.go (1)
  • New (22-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and Scan
  • GitHub Check: integration tests
  • GitHub Check: unit tests
🔇 Additional comments (33)
pkg/errors/errors.go (1)

337-337: LGTM!

The new error code follows the established pattern and uses an appropriate gRPC code (Internal) for fee evaluation failures.

api-spec/protobuf/ark/v1/service.proto (2)

27-35: LGTM!

The new EstimateFee RPC endpoint follows the established patterns in the service definition and provides clear documentation.


186-191: LGTM!

The EstimateFee message definitions are well-structured and consistent with other request/response pairs in the file. The use of int64 for the fee field is appropriate for representing satoshi amounts.

pkg/ark-cli/main.go (3)

207-214: LGTM!

The formatting change improves readability by breaking the flags into multiple lines.


382-382: LGTM!

The Balance method signature has been simplified to remove the compute-expiry parameter, aligning with the SDK changes.


424-426: LGTM!

The CollaborativeExit method signature has been simplified to remove the compute-expiry parameter.

envs/arkd.light.env (1)

14-15: LGTM!

The CEL fee program expressions are syntactically correct and demonstrate a practical fee structure:

  • Input fees: waive fees for notes and recoverable inputs, charge 1% otherwise
  • Output fees: charge a flat 200 sats for on-chain outputs, waive for off-chain
pkg/ark-lib/intent/testdata/message_fixtures.json (1)

40-50: LGTM!

The new test fixture for the estimate-fee message type is well-structured and follows the existing pattern. The fields mirror those of the register message, which is appropriate since fee estimation requires the same information.

pkg/ark-lib/intent/message_test.go (1)

74-78: LGTM!

The new case for IntentMessageTypeEstimateFee follows the established pattern and correctly handles the decoding of estimate fee messages.

pkg/ark-lib/arkfee/celenv/intent_output.go (1)

1-22: LGTM!

The CEL environment setup for intent output evaluation is correctly structured and follows best practices. The panic in init() is appropriate as it catches configuration errors early during startup.

internal/core/application/types.go (1)

22-31: EstimateFee API shape is consistent with existing Service methods

The new EstimateFee(ctx, proof, message) (int64, errors.Error) mirrors the existing intent APIs and cleanly exposes fee estimation without side effects. No issues from an interface/usage standpoint.

pkg/ark-lib/arkfee/celenv/intent_input.go (1)

1-25: IntentInputEnv initialization matches expected CEL env pattern

Global IntentInputEnv setup with input variables and helper functions in init() is straightforward and consistent with how a shared CEL environment should be configured. Panicking on misconfiguration at startup is reasonable here.

internal/interface/grpc/handlers/arkservice.go (1)

104-118: EstimateFee handler wiring is consistent and minimal

The handler cleanly validates the request via parseEstimateFeeIntent, maps parse errors to InvalidArgument, and delegates fee computation to svc.EstimateFee, returning the raw fee in the response. This matches the existing patterns for other intent-based RPCs.

pkg/ark-lib/arkfee/estimator_test.go (1)

11-266: Good coverage of estimator behaviors across input, output, and aggregate flows

The fixture-based tests for EvalInput, EvalOutput, and Eval exercise the key CEL programs (expiry-based, type-based, weight-based, and fixed-fee) and verify both zero-fee and non-zero-fee scenarios. The structure makes it easy to extend with more cases later.

pkg/ark-cli/go.mod (1)

9-15: All dependency updates verified—go mod tidy and CLI build are clean

The bumped ark-lib/go-sdk versions and new CEL-related/genproto/protobuf dependencies are correct and well-resolved:

  • go mod tidy completes without changes
  • CLI builds successfully against all updated versions
  • Module graph is consistent and all transitive dependencies resolve cleanly

The changes are solid as-is.

internal/test/e2e/e2e_test.go (2)

82-82: LGTM - Consistent API signature updates.

The Balance() calls are updated throughout to match the new simplified API signature. The removal of the boolean parameter aligns with the SDK changes mentioned in the AI summary.

Also applies to: 90-90, 135-135, 140-140, 179-179, 186-186


379-379: LGTM - CollaborativeExit API updates.

The CollaborativeExit() calls are consistently updated to the new 3-parameter signature, removing the on-chain-inputs boolean control. These changes align with the broader API surface updates in this PR.

Also applies to: 424-424, 463-463, 1901-1901

pkg/ark-lib/go.mod (1)

18-19: LGTM - CEL dependencies added for arkfee package.

The new dependencies support the CEL-based fee estimation system:

  • cel.dev/expr and github.com/google/cel-go for CEL expression evaluation
  • antlr4-go/antlr as an indirect dependency for CEL parsing
  • stoewer/go-strcase for string case conversion in CEL
  • Updated protobuf/genproto dependencies required by CEL

Also applies to: 28-28, 33-36, 42-42

pkg/ark-lib/arkfee/celenv/functions.go (2)

12-21: LGTM - now() function implementation.

Clean implementation returning Unix timestamp in seconds as a double, suitable for time-based fee calculations in CEL expressions.


24-53: LGTM - clamp_double and clamp_double_int_int overloads.

The implementations correctly handle type conversion and clamping logic. Error handling for conversion failures is appropriate.

Also applies to: 83-116

pkg/ark-lib/arkfee/estimator.go (3)

16-42: LGTM - Estimator construction and program parsing.

The constructor correctly handles empty programs gracefully and validates CEL expressions at initialization time rather than at evaluation time, which is the right approach for fail-fast behavior.


44-60: LGTM - EvalInput implementation.

The nil-program guard clause returning zero fee is appropriate for optional fee programs. Type conversion from CEL result to float64 is correct.


115-126: LGTM - CEL program parsing with type validation.

Enforcing cel.DoubleType as the return type at parse time ensures consistent fee values across all programs.

internal/config/config.go (5)

127-130: LGTM - New fee configuration fields.

The addition of IntentInputFeeProgram, IntentOutputFeeProgram, and the private fee estimator field follows the existing config patterns. The estimator is appropriately kept private as it's an implementation detail.


213-214: LGTM - Environment variable configuration for fee programs.

The new ARKD_INTENT_INPUT_FEE_PROGRAM and ARKD_INTENT_OUTPUT_FEE_PROGRAM environment variables follow the existing naming conventions. Default empty strings allow the system to run without fees configured.

Also applies to: 251-252, 292-293, 404-405


648-654: LGTM - Fee estimator initialization.

The feeEstimator() method correctly creates the estimator from config values and wraps errors with context. This follows the same pattern as other service initialization methods in this file.


571-573: LGTM - Fee estimator validation placement.

Initializing the fee estimator early in the validation chain is correct - it ensures invalid CEL programs are caught at startup rather than at runtime.


841-841: LGTM - Fee estimator passed to application service.

The estimator is correctly wired into the application layer, replacing the previous OnchainOutputFee approach mentioned in the summary.

pkg/ark-lib/arkfee/celenv/variables.go (1)

1-23: LGTM!

The CEL variable definitions are well-structured with:

  • Clear constant names that match the variable identifiers
  • Appropriate type choices (DoubleType for numeric values, StringType for enum-like types)
  • Helpful documentation strings for each variable
internal/core/application/service.go (4)

73-73: LGTM!

The integration of the feeEstimator field into the service struct and constructor is clean and follows the existing pattern for dependency injection.

Also applies to: 119-119, 242-242


2060-2063: LGTM!

The GetInfo method now correctly exposes the CEL fee programs instead of a static fee value, enabling clients to understand the fee model.


2290-2392: LGTM! EstimateFee method correctly provides fee estimates.

The EstimateFee method appropriately:

  • Validates time ranges
  • Looks up vtxos to determine input types (boarding vs vtxo/recoverable/note)
  • Constructs arkFeeInputs and arkFeeOutputs
  • Evaluates fees using the estimator

The method intentionally skips signature verification and detailed validation since it's providing an estimate, not registering an actual intent. The logic correctly mirrors the fee calculation in RegisterIntent.


1869-1895: The computeIntentFees helper function is properly implemented.

The function is defined in internal/core/application/utils.go (line 456) and correctly extracts fees from the intent proof by calculating sumOfInputs - sumOfOutputs. It validates that each input has a witness UTXO and returns an error if the sum of inputs is less than the sum of outputs, which prevents negative fees.

Copy link
Contributor

@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: 1

🧹 Nitpick comments (3)
pkg/ark-lib/arkfee/README.md (3)

63-96: Document error handling and edge cases in the Usage section.

The usage examples show the happy path but omit error handling guidance. Please document:

  • What happens when a CEL program is malformed or invalid?
  • How should callers handle err returns from EvalInput, EvalOutput, and Eval?
  • Are there any constraints on program complexity, variable values, or maximum amounts?

131-131: Add details about FeeAmount type and ToSatoshis() conversion.

The return type section mentions converting FeeAmount to satoshis using ToSatoshis(), but doesn't document:

  • The structure or fields of the FeeAmount type
  • Whether ToSatoshis() performs rounding, truncation, or other transformations
  • Whether the result is an integer or floating-point value

24-25: Clarify conditional availability of expiry and birth variables.

These variables are marked as "only available if input has an expiry/birth time." Please clarify:

  • What value or error is returned if a program references these variables when they're absent?
  • Should programs defensively check for their presence before use?
  • Are there common patterns or best practices for handling optional variables in CEL?
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb2bbda and bc1d059.

📒 Files selected for processing (1)
  • pkg/ark-lib/arkfee/README.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
pkg/ark-lib/arkfee/README.md

[grammar] ~124-~124: Ensure spelling is correct
Context: ...? 0.0 : 200.0 **Percentage fee for onchain outputs:**cel outputType == 'onchai...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: unit tests
  • GitHub Check: integration tests

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/test/e2e/e2e_test.go (1)

758-779: refill uses for range int(delta), which is invalid and doesn’t do what you want

for range int(delta) cannot range over an int and will not compile; even if it did, it wouldn’t clearly express “call faucet N times”. Given delta is a positive float64 difference in BTC units, you likely want to loop int(delta) times and call nigiri faucet on each iteration.

Apply something like this to make the intent explicit and compilable:

-    for range int(delta) {
-      if _, err := runCommand("nigiri", "faucet", address.Address); err != nil {
-        return err
-      }
-    }
+    for i := 0; i < int(delta); i++ {
+      if _, err := runCommand("nigiri", "faucet", address.Address); err != nil {
+        return err
+      }
+    }

If delta should instead represent BTC amount per faucet call rather than “count”, consider renaming or adjusting the math accordingly, but the current for range int(delta) is definitely wrong.

🧹 Nitpick comments (3)
internal/test/e2e/utils_test.go (2)

137-198: Consider de-duplicating runCommandWithEnv with runCommand

The logic in runCommandWithEnv is almost identical to runCommand, differing only in how the environment is set on exec.Cmd. This works, but it duplicates a fairly involved pattern (pipes, goroutines, error aggregation).

You could reduce maintenance surface by extracting a common helper (e.g. runPreparedCommand(cmd *exec.Cmd)) and have both runCommand and runCommandWithEnv call it, or by adding an optional env overlay parameter to runCommand itself.


576-612: restartArkdWithNewConfig may be flaky and leaves arkd in modified state

The helper does what the new fee tests need, but two things are worth tightening:

  1. Readiness waiting: it relies on fixed time.Sleep calls after docker container stop and docker compose up, then immediately POSTs /wallet/unlock. On slower machines or under load this can easily race. Reusing the existing waitUntilReady logic (or polling /status) after unlock would make this much more robust.

  2. Global config side effect: it permanently restarts arkd with the provided fee programs and never restores the previous configuration, so any tests running after the caller will see the modified fee model. That’s OK as long as TestFee is last and future tests are aware, but it’s fragile. Consider either:

    • accepting a “restore” env and calling restartArkdWithNewConfig again in a defer at the call site, or
    • providing a symmetric helper that restarts with default (empty) fee programs.
internal/test/e2e/e2e_test.go (1)

3821-3982: TestFee exercise is solid, but consider config restoration and broader coverage

The test does a good job of:

  • Restarting arkd with a specific input-fee program.
  • Verifying that two consecutive batch settlements apply a 1% fee each time (21000 → 20790 → 20582).
  • Asserting that offchain balances and onchain states move as expected.

A few improvements to consider:

  1. Restore arkd config after the test: As written, restartArkdWithNewConfig leaves the CEL fee programs enabled for any tests that might run later. To keep the e2e environment deterministic, it would be safer to restore defaults in a defer, for example:
 func TestFee(t *testing.T) {
-  env := arkdEnv{
+  env := arkdEnv{
     intentInputFeeProgram:  "inputType == 'note' || inputType == 'recoverable' ? 0.0 : amount*0.01",
     intentOutputFeeProgram: "outputType == 'onchain' ? 200.0 : 0.0",
   }
 
-  err := restartArkdWithNewConfig(env)
+  err := restartArkdWithNewConfig(env)
   require.NoError(t, err)
+
+  // Optionally restore defaults (empty programs) after this test.
+  t.Cleanup(func() {
+    _ = restartArkdWithNewConfig(arkdEnv{})
+  })
  1. Test the “free for note/recoverable” and output-fee branches: Right now you only cover the percentage fee on boarding/refresh inputs. Adding a small subtest that:
    • onboards via note and checks that no input fee is charged, and/or
    • performs a collaborative exit and checks that the configured 200-sat onchain output fee is applied,
      would give full coverage of the program shown in the PR description.

These are not blockers for the current PR but would harden the tests against future changes to the fee-evaluation logic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26e06f6 and 39f508f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • docker-compose.regtest.yml (1 hunks)
  • go.mod (7 hunks)
  • internal/test/e2e/e2e_test.go (19 hunks)
  • internal/test/e2e/utils_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go.mod
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-08T11:50:25.008Z
Learnt from: altafan
Repo: arkade-os/arkd PR: 733
File: cmd/arkd/commands.go:601-606
Timestamp: 2025-10-08T11:50:25.008Z
Learning: In the market hour configuration (cmd/arkd/commands.go and related files), when roundMinParticipantsCount or roundMaxParticipantsCount is set to 0, the system uses default values from environment variables ARKD_ROUN_MIN/MAX_PARTICIPANTS_COUNT. Therefore, it's acceptable that these values cannot be explicitly cleared back to 0 after being set to a non-zero value.

Applied to files:

  • docker-compose.regtest.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: integration tests
  • GitHub Check: unit tests
  • GitHub Check: Build and Scan
🔇 Additional comments (2)
docker-compose.regtest.yml (1)

95-96: New fee-program env vars are wired correctly into arkd service

ARKD_INTENT_OUTPUT_FEE_PROGRAM and ARKD_INTENT_INPUT_FEE_PROGRAM are exposed as docker-compose envs with sane empty defaults and match the names used by the e2e restart helper. This is consistent with the CEL fee-model design and keeps existing ARKD_ONCHAIN_OUTPUT_FEE behavior intact.

internal/test/e2e/e2e_test.go (1)

82-89: Balance/CollaborativeExit call-site updates are consistent with the new SDK API

All updated usages of Balance(...) and CollaborativeExit(...) across the e2e suite consistently follow the new signatures and preserve the original intent of the assertions (checking offchain/onchain split and exit amounts). No legacy callers with the old boolean flag signatures remain in the codebase.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (3)
pkg/ark-cli/go.mod (1)

17-17: Update CEL dependencies to latest stable versions (duplicate feedback).

This PR continues to use cel.dev/expr v0.24.0 and antlr4-go v4.13.0, which were flagged in the past review as outdated. Please update to:

  • cel.dev/expr v0.24.0v0.26.1 (latest stable as of Dec 2025)
  • antlr4-go v4.13.0v4.13.1 (latest stable)

Apply updates across all go.mod files (both pkg/ark-cli/go.mod and root go.mod).

Also applies to: 21-21

go.mod (1)

72-72: Update CEL dependencies to latest stable versions (duplicate feedback).

The root go.mod continues to use outdated CEL dependencies that were flagged in the previous review. Please update:

  • cel.dev/expr v0.24.0v0.26.1 (latest stable as of Dec 2025)
  • antlr4-go v4.13.0v4.13.1 (latest stable)

Since this is the root module, update it first, then ensure pkg/ark-cli/go.mod reflects the same versions.

Also applies to: 81-81

pkg/ark-lib/arkfee/README.md (1)

21-28: Clarify the weight variable semantics (range and effect).

Current text “Weighted liquidity lockup ratio of a vtxo” is still ambiguous. It would help to specify:

  • Expected range (e.g. 0.0–1.0 or arbitrary >=0 scalar),
  • Whether it’s unitless and how it typically scales fees (e.g. multiplier vs. share in a weighted average),
  • A short numeric example showing its impact in a sample program.

This will make it easier for operators to design correct fee formulas.

🧹 Nitpick comments (3)
internal/core/application/service.go (2)

1485-1700: RegisterIntent fee evaluation flow is sound; consider a few robustness tweaks.

Positives:

  • Input classification into boarding vs. (vtxo / recoverable / note) and output classification into vtxo vs. onchain map cleanly onto the documented CEL inputType/outputType variants.
  • Using FeeAmount.ToSatoshis() and enforcing fees < expectedFeesSats before accepting an intent provides a clear, conservative threshold (rounding up expected fees).

A few refinements to consider:

  1. Input type precedence for notes vs. recoverables
inputType := arkfee.InputTypeVtxo
if vtxo.Swept {
    inputType = arkfee.InputTypeRecoverable
} else if vtxo.IsNote() {
    inputType = arkfee.InputTypeNote
}

This assumes Swept and IsNote() are mutually exclusive. If a note can also be swept, it will be classified as recoverable instead of note. If that’s not intended, invert the order or use an explicit switch to make precedence clear.

  1. Non‑negative value assumptions when casting to uint64

Both for boarding inputs and outputs you do:

Amount: uint64(psbtInput.WitnessUtxo.Value),
...
Amount: amount // where amount := uint64(output.Value)

If a malformed PSBT ever carries a negative Value, this will silently wrap to a huge uint64. You already reject malformed proofs elsewhere, but adding explicit guards here (e.g. if Value < 0 return INVALID_PSBT_INPUT) would make the fee path more self‑contained and safer.

  1. Weight is always zero for now

You currently append:

Weight: 0, // TODO compute weight

for both boarding and vtxo inputs. That’s fine as a first step, but worth documenting explicitly (in code or README) that weight is not yet meaningful so operators don’t rely on it in programs prematurely.

  1. Optional: expose per‑input/output breakdown in metadata

INTENT_INSUFFICIENT_FEE’s metadata type has InputExpectedFees / OutputExpectedFees maps in addition to totals. If you later extend Estimator to return per‑element fees, populating those maps here would make debugging under‑funded intents easier.

Overall, the integration is correct; these points are mainly about edge‑case clarity and future‑proofing.

Also applies to: 1745-1845, 1869-1895


2290-2392: EstimateFee mirrors RegisterIntent well; consider DRYing and tightening validation.

The new EstimateFee method correctly:

  • Reuses the same ValidAt / ExpireAt checks as RegisterIntent.
  • Builds arkfee.Input/arkfee.Output with the same boarding vs vtxo vs recoverable vs note and onchain vs offchain classifications.
  • Calls s.feeEstimator.Eval and returns the ceiling in satoshis.

A few suggestions:

  1. Share input/output construction with RegisterIntent

The input/output → arkfee.Input/arkfee.Output logic is now duplicated between RegisterIntent and EstimateFee. Extracting helpers (e.g. buildArkFeeInputs / buildArkFeeOutputs) would reduce drift risk when adding new input/output types or fields.

  1. Same negative‑value guard as registration path

Here too you cast output.Value and WitnessUtxo.Value to uint64. Adding simple Value < 0 checks before the cast (returning an INVALID_INTENT_PSBT/INVALID_PSBT_INPUT error) would protect the estimator in case a client supplies a malformed PSBT.

  1. Clarify behavior for unknown inputs

When GetVtxos returns no rows, you treat the input as boarding and still charge via InputTypeBoarding. That’s reasonable for estimation, but a brief comment would help future readers understand that this is “best effort” estimation without requiring the vtxo to exist yet.

Functionally, the method is consistent with the registration flow.

internal/config/config.go (1)

26-27: Config wiring for CEL fee programs is coherent.

  • New IntentInputFeeProgram / IntentOutputFeeProgram fields, env keys, and defaults are consistent, and with the ARKD_ prefix viper will pick up ARKD_INTENT_INPUT_FEE_PROGRAM / ARKD_INTENT_OUTPUT_FEE_PROGRAM as intended.
  • LoadConfig correctly plumbs these into the Config instance, so they are available before service initialization.

One small robustness improvement: you may want to strings.TrimSpace the program strings before passing them downstream, to avoid surprising parse errors from accidental leading/trailing whitespace in env vars.

Also applies to: 118-132, 159-217, 219-255, 294-296, 344-409

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39f508f and 1d3ea12.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • pkg/ark-cli/go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • go.mod (7 hunks)
  • internal/config/config.go (9 hunks)
  • internal/core/application/service.go (13 hunks)
  • pkg/ark-cli/go.mod (4 hunks)
  • pkg/ark-lib/arkfee/README.md (1 hunks)
  • pkg/ark-lib/arkfee/celenv/functions.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/intent_input.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/intent_output.go (1 hunks)
  • pkg/ark-lib/arkfee/types.go (1 hunks)
  • pkg/ark-lib/go.mod (2 hunks)
  • pkg/errors/go.mod (0 hunks)
💤 Files with no reviewable changes (1)
  • pkg/errors/go.mod
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/ark-lib/arkfee/celenv/intent_input.go
  • pkg/ark-lib/go.mod
  • pkg/ark-lib/arkfee/celenv/functions.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T10:58:41.042Z
Learnt from: louisinger
Repo: arkade-os/arkd PR: 691
File: internal/core/application/service.go:557-562
Timestamp: 2025-08-19T10:58:41.042Z
Learning: In the arkd SubmitOffchainTx method, using the checkpoint PSBT input's tapscript (forfeit path) for the VtxoInput.Tapscript field is the correct behavior, not a bug as initially thought. The system correctly handles the relationship between checkpoint inputs and Ark transaction inputs.

Applied to files:

  • internal/core/application/service.go
🧬 Code graph analysis (3)
pkg/ark-lib/arkfee/types.go (1)
pkg/ark-lib/arkfee/celenv/variables.go (6)
  • AmountVariableName (8-8)
  • InputTypeVariableName (12-12)
  • WeightVariableName (11-11)
  • ExpiryVariableName (9-9)
  • BirthVariableName (10-10)
  • OutputTypeVariableName (13-13)
internal/core/application/service.go (10)
pkg/ark-lib/arkfee/estimator.go (2)
  • Estimator (16-19)
  • New (22-42)
pkg/ark-lib/arkfee/types.go (7)
  • Input (25-31)
  • InputTypeVtxo (20-20)
  • InputTypeRecoverable (19-19)
  • InputTypeNote (22-22)
  • Output (55-58)
  • OutputTypeVtxo (51-51)
  • OutputTypeOnchain (52-52)
internal/core/ports/tx_builder.go (1)
  • Input (25-28)
pkg/ark-lib/intent/proof.go (3)
  • Input (44-48)
  • New (134-158)
  • Proof (39-41)
api-spec/protobuf/gen/ark/v1/types.pb.go (6)
  • Input (76-82)
  • Input (95-95)
  • Input (110-112)
  • Outpoint (24-30)
  • Outpoint (43-43)
  • Outpoint (58-60)
internal/core/application/admin.go (1)
  • Receiver (519-523)
pkg/errors/errors.go (7)
  • INVALID_INTENT_PROOF (292-296)
  • InvalidIntentProofMetadata (173-176)
  • INTENT_INSUFFICIENT_FEE (325-329)
  • IntentInsufficientFeeMetadata (195-200)
  • INVALID_INTENT_TIMERANGE (279-283)
  • IntentTimeRangeMetadata (163-167)
  • PsbtMetadata (111-113)
pkg/ark-lib/intent/message.go (1)
  • EstimateFeeMessage (62-62)
internal/core/application/types.go (2)
  • Outpoint (180-180)
  • VOut (134-134)
internal/core/domain/vtxo.go (1)
  • Outpoint (15-18)
internal/config/config.go (1)
pkg/ark-lib/arkfee/estimator.go (2)
  • Estimator (16-19)
  • New (22-42)
🪛 LanguageTool
pkg/ark-lib/arkfee/README.md

[duplication] ~8-~8: Possible typo: you repeated a word.
Context: ...nput Program**: Evaluated for each input in an intent - Intent Output Program: Evaluated ...

(ENGLISH_WORD_REPEAT_RULE)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit tests
  • GitHub Check: Build and Scan
  • GitHub Check: integration tests
🔇 Additional comments (5)
go.mod (1)

221-222: Verify protobuf and genproto version alignment.

The protobuf and genproto version updates use recent pseudo-versions (2025-12-02). Confirm that these versions are compatible with the CEL dependencies and that no API breakage occurs during compilation and testing.

Also applies to: 252-252, 272-272

pkg/ark-lib/arkfee/celenv/intent_output.go (1)

1-21: IntentOutputEnv wiring looks consistent with input env.

Env construction with amount, outputType, and nowFunction matches the expected CEL surface and failing fast via panic on init is appropriate here.

pkg/ark-lib/arkfee/types.go (1)

10-65: FeeAmount and CEL arg marshalling look correct.

  • FeeAmount.ToSatoshis() using math.Ceil is appropriate to avoid under‑charging when programs return fractional fees.
  • Input/Output toArgs correctly expose amount, inputType/outputType, weight, and optional timestamps using the shared celenv variable names.

No behavioral issues spotted here.

internal/core/application/service.go (1)

2431-2447: New cache cleanup in startRound looks correct.

The added cleanup:

  • Deletes tree signing sessions for the previous round ID.
  • Deletes cached boarding input signatures keyed by the previous CommitmentTxid.

This is gated on existingRound != nil and empty‑string checks, so it’s safe and should prevent stale data from leaking into subsequent rounds.

internal/config/config.go (1)

574-576: feeEstimator lifecycle and error surfacing look good; align with service‑side nil handling.

  • Validate() now calls c.feeEstimator() early, so invalid CEL programs fail fast with a clear “failed to create fee estimator” error before any DB/wallet wiring.
  • feeEstimator() simply delegates to arkfee.New(c.IntentInputFeeProgram, c.IntentOutputFeeProgram) and stores the result in c.fee, which is then injected into application.NewService in appService().

Once you enforce a non‑nil estimator in NewService (as suggested in service.go), this configuration path guarantees c.fee is always initialized for normal node startup, and misconfigured fee programs are rejected at config‑validation time.

No functional issues here; just ensure any direct uses of application.NewService outside the config path are updated to pass a valid estimator as well.

Also applies to: 651-657, 831-845

Copy link
Contributor

@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: 1

♻️ Duplicate comments (1)
pkg/ark-lib/arkfee/README.md (1)

29-29: Clarify the weight variable documentation.

The description "Weighted liquidity lockup ratio of a vtxo" remains vague. Per prior feedback, please specify the valid range (e.g., 0.0–1.0), explain how it modifies fee calculations, describe edge cases (e.g., weight=0 means no contribution, weight=1.0 means full contribution), and include a concrete example showing how a vtxo with weight 0.25 affects a fee computation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d3ea12 and 9bb0951.

📒 Files selected for processing (1)
  • pkg/ark-lib/arkfee/README.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
pkg/ark-lib/arkfee/README.md

[duplication] ~11-~11: Possible typo: you repeated a word.
Context: ...nput Program**: Evaluated for each input in an intent - Intent Output Program: Evaluated ...

(ENGLISH_WORD_REPEAT_RULE)

🪛 markdownlint-cli2 (0.18.1)
pkg/ark-lib/arkfee/README.md

6-6: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and Scan
  • GitHub Check: unit tests
  • GitHub Check: integration tests

Copy link
Contributor

@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: 3

🧹 Nitpick comments (1)
pkg/ark-lib/arkfee/estimator.go (1)

23-33: Consider safer type assertion.

While the type assertion on line 32 is protected by the DoubleType check in parse(), it could still panic if there's an unexpected bug. Consider using a type assertion with ok check for defensive programming.

Apply this diff for safer error handling:

 func (p *program) Eval(args map[string]any) (FeeAmount, error) {
 	result, _, err := p.Program.Eval(args)
 	if err != nil {
 		return 0, err
 	}
 	native, err := result.ConvertToNative(reflect.TypeOf(float64(0)))
 	if err != nil {
 		return 0, err
 	}
-	return FeeAmount(native.(float64)), nil
+	f64, ok := native.(float64)
+	if !ok {
+		return 0, fmt.Errorf("unexpected type %T, expected float64", native)
+	}
+	return FeeAmount(f64), nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bb0951 and 1fad876.

📒 Files selected for processing (9)
  • pkg/ark-lib/arkfee/celenv/intent_offchain_input.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/intent_onchain_input.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/intent_output.go (1 hunks)
  • pkg/ark-lib/arkfee/celenv/variables.go (1 hunks)
  • pkg/ark-lib/arkfee/estimator.go (1 hunks)
  • pkg/ark-lib/arkfee/estimator_test.go (1 hunks)
  • pkg/ark-lib/arkfee/testdata/invalid.json (1 hunks)
  • pkg/ark-lib/arkfee/testdata/valid.json (1 hunks)
  • pkg/ark-lib/arkfee/types.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/ark-lib/arkfee/celenv/variables.go
  • pkg/ark-lib/arkfee/types.go
🧰 Additional context used
🧬 Code graph analysis (2)
pkg/ark-lib/arkfee/estimator_test.go (3)
pkg/ark-lib/arkfee/estimator.go (2)
  • Config (11-16)
  • New (44-73)
internal/config/config.go (1)
  • Config (61-145)
pkg/ark-lib/arkfee/types.go (5)
  • OffchainInput (24-30)
  • FeeAmount (10-10)
  • OnchainInput (47-49)
  • Output (57-60)
  • VtxoType (16-16)
pkg/ark-lib/arkfee/estimator.go (5)
pkg/ark-lib/arkfee/types.go (4)
  • FeeAmount (10-10)
  • OffchainInput (24-30)
  • OnchainInput (47-49)
  • Output (57-60)
internal/config/config.go (1)
  • Config (61-145)
pkg/ark-lib/arkfee/celenv/intent_offchain_input.go (1)
  • IntentOffchainInputEnv (7-7)
pkg/ark-lib/arkfee/celenv/intent_onchain_input.go (1)
  • IntentOnchainInputEnv (7-7)
pkg/ark-lib/arkfee/celenv/intent_output.go (1)
  • IntentOutputEnv (7-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and Scan
  • GitHub Check: integration tests
  • GitHub Check: unit tests
🔇 Additional comments (11)
pkg/ark-lib/arkfee/celenv/intent_onchain_input.go (1)

9-20: LGTM!

The initialization pattern is appropriate. Panicking in init() for critical environment setup failures is acceptable since the application cannot function without valid CEL environments.

pkg/ark-lib/arkfee/celenv/intent_output.go (1)

9-20: LGTM!

Consistent initialization pattern with appropriate variables for output evaluation (amount, outputScript, now function).

pkg/ark-lib/arkfee/celenv/intent_offchain_input.go (1)

9-23: LGTM!

The offchain input environment appropriately includes temporal fields (expiry, birth) and input metadata (type, weight) needed for time-based and type-conditional fee calculations.

pkg/ark-lib/arkfee/testdata/invalid.json (1)

1-112: LGTM!

Comprehensive negative test coverage including syntax errors, type mismatches, undefined variables, and cross-environment variable usage. This will help ensure robust error handling during CEL program compilation.

pkg/ark-lib/arkfee/estimator_test.go (4)

13-30: LGTM!

Comprehensive testing of invalid configurations with proper error assertions.


32-165: LGTM!

Thorough test coverage for individual evaluation methods, including edge cases where no program is configured.


167-210: LGTM!

Well-structured end-to-end evaluation tests exercising all four program types together.


212-345: LGTM!

Clean test data structures and conversion utilities using Go's embed feature for test fixtures.

pkg/ark-lib/arkfee/estimator.go (3)

43-73: LGTM!

The constructor properly handles optional programs by only parsing non-empty configuration strings, with appropriate error propagation.


75-109: LGTM!

Individual evaluation methods correctly handle the case where programs are not configured (returning zero fees) and properly delegate to the program wrapper.


145-160: LGTM!

The parse helper properly validates that CEL programs return double type, which provides type safety for the fee calculations and protects the type assertion in program.Eval.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (2)
pkg/ark-lib/arkfee/README.md (1)

6-6: Fix bare URL in markdown.

Use proper markdown link syntax instead of a bare URL.

-https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions
+[CEL language specification](https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions)
internal/core/application/service.go (1)

73-75: Still need a guard against nil feeEstimator in service construction.

service now dereferences s.feeEstimator in multiple places (RegisterIntent, EstimateFee, GetInfo) assuming it is non‑nil, but NewService still accepts feeEstimator *arkfee.Estimator without enforcing this invariant. Tests or alternative wiring that bypass config.Validate() can still pass nil and panic at runtime.

Recommend enforcing non‑nil in NewService and optionally asserting inside call sites:

 func NewService(
@@
-    feeEstimator *arkfee.Estimator,
+    feeEstimator *arkfee.Estimator,
 ) (Service, error) {
+    if feeEstimator == nil {
+        return nil, fmt.Errorf("fee estimator must not be nil")
+    }
@@
-        vtxoNoCsvValidationCutoffTime: vtxoNoCsvValidationCutoffTime,
-        feeEstimator:                  feeEstimator,
+        vtxoNoCsvValidationCutoffTime: vtxoNoCsvValidationCutoffTime,
+        feeEstimator:                  feeEstimator,

Also applies to: 119-120, 199-243

🧹 Nitpick comments (9)
pkg/ark-lib/arkfee/README.md (1)

31-31: Clarify the weight variable documentation.

The description "Weighted liquidity lockup ratio of a vtxo" lacks critical context. Expand this to specify the valid range (e.g., 0.0–1.0?), how it modifies the fee calculation, and its behavioral semantics (e.g., 0 = no lockup → no contribution, 1 = fully locked → full contribution). A short example showing how a vtxo with weight 0.25 affects a fee computation would be helpful.

internal/core/application/types.go (1)

95-105: Changing IntentFeeInfo on‑chain fields to string is consistent with exposing CEL programs.

Switching OnchainInput/OnchainOutput to string matches how these values are later populated from arkfee.Estimator.Config and serialized via gRPC. Just ensure all callers expect a program string rather than a numeric fee value.

If any downstream consumers still treat these fields as numeric amounts, they should be updated or renamed to avoid confusion (e.g., OnchainInputProgram).

internal/core/application/service.go (3)

1440-1714: Fee accounting in RegisterIntent is consistent but could use minor robustness.

Positives:

  • You accumulate expectedFees from:
    • On‑chain boarding inputs via EvalOnchainInput.
    • Off‑chain inputs (existing vtxos) via EvalOffchainInput with VtxoType{Vtxo,Recoverable,Note}.
    • Outputs via EvalOnchainOutput / EvalOffchainOutput.
  • You then compute actual proof fees via proof.Fees() and enforce fees >= expectedFees.ToSatoshis(), returning a structured INTENT_INSUFFICIENT_FEE on failure.
  • Missing WitnessUtxo on any proof input will be caught early by Proof.Fees().

Two minor suggestions:

  1. Guard against nil feeEstimator just in case. Even with constructor checks, an optional defensive:
if s.feeEstimator == nil {
    return "", errors.INTERNAL_ERROR.New("fee estimator unavailable")
}

near the top of RegisterIntent would turn a panic into a clear error.

  1. Consider logging or exposing per‑component fee breakdown. Right now only the total is enforced; if you later need debugging, returning or logging the per‑category fees (on/off‑chain, in/out) can be very helpful.

Also applies to: 1898-1913


1768-1871: Output fee evaluation matches message semantics and existing amount checks.

  • You build a reusable arkfee.Output from each TxOut and branch on slices.Contains(message.OnchainOutputIndexes, outputIndex) to choose between EvalOnchainOutput and EvalOffchainOutput.
  • Existing amount and script validations (UTXO/VTXO min/max, address extraction, sub‑dust checks) remain intact and occur before fee accumulation, so invalid outputs are rejected before fees matter.
  • Adding output to error metadata makes mis‑configured CEL programs diagnosable.

This is a clean integration of the estimator into the existing registration flow.

If you find yourself needing this split in multiple places, consider a small helper that returns (arkfee.Output, isOnchain bool) to DRY up the logic between RegisterIntent and EstimateFee.


2310-2422: EstimateFee implementation is consistent with RegisterIntent and side‑effect free.

  • Time‑range checks (ValidAt/ExpireAt) mirror RegisterIntent, so callers see the same validity semantics.
  • Inputs:
    • For each proof input, you look up a vtxo; if found, you build an OffchainInput with type Vtxo/Recoverable/Note.
    • If no vtxo is found or there’s an error, you fall back to an OnchainInput using WitnessUtxo.Value.
  • Outputs:
    • You classify outputs as on‑chain vs off‑chain with the same OnchainOutputIndexes set used in registration.
  • You then call s.feeEstimator.Eval(...) with the four slices and return the total in satoshis.

This matches the registration‑time accounting but without DB/cache mutations and looks safe as an estimate‑only path.

You might eventually want to reuse the input/output collection logic between RegisterIntent and EstimateFee (e.g. helper that returns the four arkfee slices plus the raw receivers), to avoid subtle drift if one path is updated in the future.

internal/config/config.go (2)

442-618: feeEstimator initialization in Validate surfaces CEL errors early.

  • Validate now calls c.feeEstimator() before constructing other services, so malformed CEL programs cause startup to fail fast with a clear "failed to create fee estimator" error.
  • feeEstimator() builds the estimator directly from c.Intent*Program fields, relying on arkfee.New to only parse non‑empty programs, which matches the intent of “optional fee rules”.

One minor suggestion: log the offending program(s) at debug level when arkfee.New fails, to ease diagnosis in production.

You can verify behavior with a small CEL typo in one of the INTENT_*_FEE_PROGRAM env vars and ensure Validate returns a clear error mentioning the estimator creation.

Also applies to: 661-672


820-861: Passing c.fee into NewService is correct but assumes prior initialization.

appService() forwards c.fee to application.NewService, which matches the new constructor signature. Given that Validate() already calls c.feeEstimator(), this is fine for normal startup.

As a defensive improvement, consider:

  • Returning an explicit error if c.fee is still nil here (e.g., if appService() is called without Validate() in some tests).
internal/test/e2e/e2e_test.go (2)

3909-3911: Clarify fee calculation with inline comments.

The hardcoded expected values (20790, 20582) are correct but would benefit from inline calculation comments to make the test more maintainable and self-documenting.

Apply this diff to add clarity:

 	// 21000 - 1% of 21000 = 20790
+	// Formula: 21000 * 0.99 = 20790
 	require.Equal(t, 20790, int(aliceFirstVtxo.Amount))
 	require.Equal(t, 20790, int(bobFirstVtxo.Amount))
 
 	// ...
 
 	// 20790 - 1% of 20790 = 20582
+	// Formula: 20790 * 0.99 = 20582.1, truncated to 20582
 	require.Equal(t, 20582, int(aliceSecondVtxo.Amount))
 	require.Equal(t, 20582, int(bobSecondVtxo.Amount))

Note: The second calculation shows truncation (20790 * 0.99 = 20582.1 → 20582), which is expected behavior for satoshi amounts but worth documenting.

Also applies to: 3963-3965


3824-3831: Document the fee program semantics for test maintainability.

The CEL fee formulas are central to the test but their semantics could be clearer for future maintainers.

Consider adding a comment block explaining the fee structure:

+	// Fee structure being tested:
+	// - Offchain inputs: Free for 'note' or 'recoverable' types, otherwise 1% of amount
+	// - Onchain inputs: 1% of amount
+	// - Offchain outputs: Free
+	// - Onchain outputs: Flat 200 satoshis
 	env := arkdEnv{
-		// for input: free in case of recoverable or note, 1% of the amount otherwise
-		// for output: 200 satoshis for onchain output, 0 for vtxo output
 		intentOffchainInputFeeProgram:  "inputType == 'note' || inputType == 'recoverable' ? 0.0 : amount*0.01",
 		intentOnchainInputFeeProgram:   "0.01 * amount",
 		intentOffchainOutputFeeProgram: "0.0",
 		intentOnchainOutputFeeProgram:  "200.0",
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fad876 and ddbf370.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • pkg/ark-cli/go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • docker-compose.regtest.yml (1 hunks)
  • go.mod (7 hunks)
  • internal/config/config.go (9 hunks)
  • internal/core/application/service.go (14 hunks)
  • internal/core/application/types.go (2 hunks)
  • internal/core/application/utils.go (0 hunks)
  • internal/interface/grpc/handlers/parser.go (2 hunks)
  • internal/test/e2e/e2e_test.go (19 hunks)
  • internal/test/e2e/utils_test.go (3 hunks)
  • pkg/ark-cli/go.mod (4 hunks)
  • pkg/ark-lib/arkfee/README.md (1 hunks)
  • pkg/ark-lib/intent/proof.go (1 hunks)
💤 Files with no reviewable changes (1)
  • internal/core/application/utils.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/test/e2e/utils_test.go
  • go.mod
  • docker-compose.regtest.yml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T10:58:41.042Z
Learnt from: louisinger
Repo: arkade-os/arkd PR: 691
File: internal/core/application/service.go:557-562
Timestamp: 2025-08-19T10:58:41.042Z
Learning: In the arkd SubmitOffchainTx method, using the checkpoint PSBT input's tapscript (forfeit path) for the VtxoInput.Tapscript field is the correct behavior, not a bug as initially thought. The system correctly handles the relationship between checkpoint inputs and Ark transaction inputs.

Applied to files:

  • internal/core/application/service.go
🧬 Code graph analysis (2)
internal/interface/grpc/handlers/parser.go (3)
api-spec/protobuf/gen/ark/v1/types.pb.go (3)
  • Intent (441-447)
  • Intent (460-460)
  • Intent (475-477)
pkg/ark-lib/intent/message.go (1)
  • EstimateFeeMessage (62-62)
pkg/ark-lib/arkfee/types.go (1)
  • OnchainInput (47-49)
internal/config/config.go (1)
pkg/ark-lib/arkfee/estimator.go (3)
  • Estimator (35-41)
  • Config (11-16)
  • New (44-73)
🪛 LanguageTool
pkg/ark-lib/arkfee/README.md

[grammar] ~24-~24: Ensure spelling is correct
Context: ...ntOffchainInputEnv Used for evaluating offchain input (vtxo) fee calculations. Availabl...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~36-~36: Ensure spelling is correct
Context: ...entOnchainInputEnv Used for evaluating onchain input (boarding) fee calculations. Avai...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
pkg/ark-lib/arkfee/README.md

6-6: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit tests
  • GitHub Check: integration tests
  • GitHub Check: Build and Scan
🔇 Additional comments (14)
pkg/ark-lib/arkfee/README.md (1)

64-117: LGTM!

The documentation is comprehensive, well-structured, and provides clear usage patterns with concrete examples across all four program categories. The CEL environment documentation, example programs, and code snippets make it easy for users to understand and implement custom fee logic.

pkg/ark-cli/go.mod (3)

10-11: Verify updated ark-lib and go-sdk versions contain expected CEL implementation.

The direct dependencies have been updated to newer pseudo-versions. Please confirm that these versions include the fee estimation logic and CEL parsing/evaluation changes that should accompany the new CEL-based fee system.


92-95: Verify proto-related dependency versions are aligned.

The protobuf-related indirect dependencies have been updated to newer versions. Confirm these versions are compatible with Go 1.25.5 and properly aligned with the gRPC API changes introduced by the fee system (EstimateFee RPC).


17-17: CEL dependencies are secure with no known vulnerabilities, though two can be updated to newer versions.

All four CEL-related dependencies are stable and free of public security advisories:

  • cel.dev/expr v0.24.0 — secure, no CVEs
  • google/cel-go v0.26.1 — latest released version, secure
  • antlr4-go/antlr/v4 v4.13.0 — secure; newer patch v4.13.2 available (non-critical)
  • stoewer/go-strcase v1.2.0 — secure; newer v1.3.x versions available (non-critical)

The versions in use are safe to ship. Consider upgrading antlr and go-strcase to their latest patches in a future update for currency, but no immediate action is required.

internal/interface/grpc/handlers/parser.go (2)

51-67: parseEstimateFeeIntent correctly mirrors register-intent parsing.

The new helper follows the same validation and decode pattern as parseRegisterIntent, including error text and nil checks, which keeps parsing behavior consistent across intent types. Looks good.


291-303: FeeInfo toProto uses raw on‑chain fee programs consistently.

Passing OnchainInput and OnchainOutput through as strings (like the off‑chain fields) matches the new IntentFeeInfo types and seems consistent with exposing CEL programs rather than numeric fees. No issues here.

internal/core/application/types.go (1)

22-31: Service interface extension for EstimateFee looks coherent.

The new EstimateFee method matches the implementation signature in service.go (context, intent.Proof, intent.EstimateFeeMessage, returns int64, errors.Error), so interface and concrete type stay aligned.

pkg/ark-lib/intent/proof.go (1)

160-179: Proof.Fees implementation is logically sound and defensive.

  • Uses all PSBT inputs (including index 0) but safely, since the first input’s witness UTXO is set to value 0 in buildToSignTx.
  • Requires WitnessUtxo on every input and reports which index is missing, which is helpful when proofs are malformed.
  • Sums unsigned‑tx outputs and rejects negative fees (sumInputs < sumOutputs), preventing nonsensical results.

This aligns with how proofs are constructed in this package and should give a reliable fee figure for RegisterIntent.

internal/core/application/service.go (2)

2062-2085: GetInfo now exposes CEL fee programs correctly.

Populating ServiceInfo.Fees.IntentFees.* from s.feeEstimator.Config.Intent*Program fields is coherent with the config/arkfee wiring and gives clients introspection into the active fee model. As long as feeEstimator is guaranteed non‑nil (see earlier comment), this is straightforward.


2461-2477: Round cleanup additions help prevent stale cache entries.

The extra cleanup of tree signing sessions and boarding‑input signatures when restarting rounds:

  • Deletes tree signing sessions keyed by existingRound.Id.
  • Clears boarding input signatures keyed by existingRound.CommitmentTxid.

This should reduce the risk of old data leaking into new rounds. Behavior looks correct given the surrounding reset logic.

internal/config/config.go (2)

26-27: Config wire‑up for arkfee estimator looks structurally correct.

  • New Config fields (IntentOffchainInputProgram, IntentOnchainInputProgram, etc.) plus fee *arkfee.Estimator align with arkfee.Config.
  • Env variables ARKD_INTENT_*_FEE_PROGRAM map cleanly to those fields, and you’ve introduced matching constants.
  • EnablePprof remains orthogonal and doesn’t interfere.

This is a sensible way to surface CEL programs via environment.

Also applies to: 126-147, 217-221


223-261: Defaults and LoadConfig wiring preserve backward compatibility.

  • Fee‑program defaults are empty strings, so in the absence of env overrides the estimator will compile with “no extra fees” rather than failing.
  • LoadConfig pulls the fee‑program env vars into the new Config fields while leaving existing behavior intact.
  • EnablePprof continues to be read from its env var with a false default.

No behavioral regressions apparent here.

Also applies to: 295-305, 352-419

internal/test/e2e/e2e_test.go (2)

82-82: LGTM! API signature updates are consistent.

The Balance() call signature changes throughout the test suite are consistent with the updated SDK API that removed the boolean parameter. All call sites properly updated.

Also applies to: 90-90, 135-135, 140-140, 179-179, 186-186, 202-202, 229-229, 255-255, 269-269, 289-289, 313-313, 345-345, 363-363, 368-368, 385-385, 391-391, 407-407, 413-413, 429-429, 435-435, 2058-2058, 2702-2702


379-379: LGTM! API signature updates are consistent.

The CollaborativeExit() call signature changes are consistent with the updated SDK API that removed the boolean parameter.

Also applies to: 424-424, 463-463, 1901-1901

Copy link
Contributor

@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/ark-lib/arkfee/estimator.go (3)

23-32: Avoid potential panic on ConvertToNative result

native.(float64) will panic if CEL ever returns a non‑float64 despite the static DoubleType check. It’s low‑probability but easy to harden by checking the type assertion and including the program text in the error for easier debugging.

Example refactor:

 func (p *program) Eval(args map[string]any) (FeeAmount, error) {
-	result, _, err := p.Program.Eval(args)
-	if err != nil {
-		return 0, err
-	}
-	native, err := result.ConvertToNative(reflect.TypeOf(float64(0)))
-	if err != nil {
-		return 0, err
-	}
-	return FeeAmount(native.(float64)), nil
+	result, _, err := p.Program.Eval(args)
+	if err != nil {
+		return 0, fmt.Errorf("evaluate fee program %q: %w", p.txt, err)
+	}
+	native, err := result.ConvertToNative(reflect.TypeOf(float64(0)))
+	if err != nil {
+		return 0, fmt.Errorf("convert result of fee program %q to float64: %w", p.txt, err)
+	}
+	f, ok := native.(float64)
+	if !ok {
+		return 0, fmt.Errorf("fee program %q returned non-float64 result of type %T", p.txt, native)
+	}
+	return FeeAmount(f), nil
 }

43-73: Avoid returning a partially initialized Estimator on error

Because New uses named returns, a compile/parse failure returns a non‑nil Estimator alongside a non‑nil err. That’s fine if all call sites reliably ignore the estimator when err != nil, but it’s easy to make this safer/clearer by returning nil on failure and avoiding named returns.

For example:

-func New(config Config) (estimator *Estimator, err error) {
-	estimator = &Estimator{Config: config}
+func New(config Config) (*Estimator, error) {
+	estimator := &Estimator{Config: config}
@@
-		if err != nil {
-			return
-		}
+		if err != nil {
+			return nil, err
+		}
@@
-		if err != nil {
-			return
-		}
+		if err != nil {
+			return nil, err
+		}
@@
-		if err != nil {
-			return
-		}
+		if err != nil {
+			return nil, err
+		}
@@
-		if err != nil {
-			return
-		}
+		if err != nil {
+			return nil, err
+		}
 	}
-
-	return
+	return estimator, nil
 }

75-109: Fix minor typos in exported method comments

The comments for the Eval* methods have a small typo (“evalutes”) and are user‑facing on the exported API. Consider correcting them for polish, e.g.:

-// EvalOffchainInput evalutes the fee for a given vtxo input
+// EvalOffchainInput evaluates the fee for a given offchain (vtxo) input
@@
-// EvalOnchainInput evalutes the fee for a given boarding input
+// EvalOnchainInput evaluates the fee for a given onchain (boarding) input
@@
-// EvalOffchainOutput evalutes the fee for a given vtxo output
+// EvalOffchainOutput evaluates the fee for a given offchain (vtxo) output
@@
-// EvalOnchainOutput evalutes the fee for a given collaborative exit output
+// EvalOnchainOutput evaluates the fee for a given collaborative exit (onchain) output
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddbf370 and d3ed8ce.

📒 Files selected for processing (2)
  • pkg/ark-lib/arkfee/estimator.go (1 hunks)
  • pkg/ark-lib/arkfee/testdata/valid.json (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T10:58:41.042Z
Learnt from: louisinger
Repo: arkade-os/arkd PR: 691
File: internal/core/application/service.go:557-562
Timestamp: 2025-08-19T10:58:41.042Z
Learning: In the arkd SubmitOffchainTx method, using the checkpoint PSBT input's tapscript (forfeit path) for the VtxoInput.Tapscript field is the correct behavior, not a bug as initially thought. The system correctly handles the relationship between checkpoint inputs and Ark transaction inputs.

Applied to files:

  • pkg/ark-lib/arkfee/testdata/valid.json
🧬 Code graph analysis (1)
pkg/ark-lib/arkfee/estimator.go (4)
pkg/ark-lib/arkfee/types.go (4)
  • FeeAmount (10-10)
  • OffchainInput (24-30)
  • OnchainInput (47-49)
  • Output (57-60)
pkg/ark-lib/arkfee/celenv/intent_offchain_input.go (1)
  • IntentOffchainInputEnv (7-7)
pkg/ark-lib/arkfee/celenv/intent_onchain_input.go (1)
  • IntentOnchainInputEnv (7-7)
pkg/ark-lib/arkfee/celenv/intent_output.go (1)
  • IntentOutputEnv (7-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit tests
  • GitHub Check: integration tests
  • GitHub Check: Build and Scan
🔇 Additional comments (6)
pkg/ark-lib/arkfee/estimator.go (2)

111-150: Aggregate Eval now correctly accounts for all four intent dimensions

The aggregate Eval method now iterates over offchain/onchain inputs and outputs, delegating to the per‑item evaluators and propagating errors. This fixes the previously noted omission of onchain outputs and aligns with the test expectations in testdata/valid.json (e.g., mixed and onchain‑only cases).

Logic and error handling here look solid.


153-168: Type enforcement in parse is appropriate

Validating ast.OutputType() == cel.DoubleType before constructing the program is a good guardrail and matches the FeeAmount semantics. This should prevent misconfigured fee programs (e.g., boolean/string) from slipping into runtime evaluation.

Implementation looks good as‑is.

pkg/ark-lib/arkfee/testdata/valid.json (4)

2-83: Offchain input fixtures cover time, type, and weight dimensions well

The evalOffchainInput group nicely exercises expiry‑based conditions, input type branching, and weighted percentage fees. The expected values (e.g., 5000 vs 0, 56.3, 100) match the CEL programs’ logic.


84-125: Onchain input fixtures validate both fixed and proportional fees

The evalOnchainInput cases correctly validate constant and amount * 0.01 programs, with expectations (100, 500) matching the formulas. This gives good coverage of typical onchain input fee models.


126-203: Output fixtures symmetrically cover offchain/onchain scenarios

The evalOffchainOutput and evalOnchainOutput sections mirror each other with fixed, script‑size‑based, and percentage fees. The script length example ("0020deadbeefeeeeeeffff" → 22 bytes → 0.22 fee) looks correct and ensures size(script) handling is exercised.


204-281: Aggregate eval expectations correctly include all fee components

The aggregate eval cases now match the estimator’s full behavior:

  • “fixed fee” mixed case: 100 (offchain in) + 50 (onchain in) + 100 (offchain out) + 150 (onchain out) = 400.
  • “free for vtxo input”: only one charge from the non‑empty script output → 100.
  • “onchain only”: 1% of 10000 = 100 plus 15% of 20000 = 3000 → 3100.

These expectations are consistent with the corrected estimator (including onchain outputs).

Copy link
Contributor

@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 (1)
internal/config/config.go (1)

216-221: Fee program env var names vs PR/docs — ensure they’re aligned and fully documented

Here you expose four env keys:

  • INTENT_OFFCHAIN_INPUT_FEE_PROGRAM
  • INTENT_ONCHAIN_INPUT_FEE_PROGRAM
  • INTENT_OFFCHAIN_OUTPUT_FEE_PROGRAM
  • INTENT_ONCHAIN_OUTPUT_FEE_PROGRAM

which become ARKD_... env vars via the prefix, plus the new ENABLE_PPROF and the cutoff date key.

The PR objectives / description mention only ARKD_INTENT_INPUT_FEE_PROGRAM and ARKD_INTENT_OUTPUT_FEE_PROGRAM. If the final design is to have four separate knobs (offchain/onchain × input/output), it would be good to:

  • Update any README / config docs to list all four env vars explicitly and drop/clarify the older two‑var naming, and
  • Maybe add a short comment here or in the README explaining the mapping (offchain vs onchain) so operators don’t have to infer it from code.

I can help draft the doc / README snippet if you’d like.

Also applies to: 295-297, 405-407

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3ed8ce and 3e441b5.

📒 Files selected for processing (1)
  • internal/config/config.go (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and Scan
  • GitHub Check: integration tests
  • GitHub Check: unit tests
🔇 Additional comments (2)
internal/config/config.go (2)

126-127: EnablePprof config wiring looks consistent

The EnablePprof field, env key, default, and LoadConfig mapping all follow the existing config patterns (env‑backed bool with a sensible default). Assuming the rest of the codebase actually consumes Config.EnablePprof, this wiring looks correct and non‑controversial.

Also applies to: 221-221, 256-256, 296-296, 406-406


26-27: Fee programs are optional and safely handled when unset

The integration is actually more robust than the original concern suggested. arkfee.New explicitly guards each program field with if len(config.IntentXxxProgram) > 0 before parsing, so empty programs are gracefully skipped without error. This is documented in the arkfee code comment: "parses the intent input and output programs if not empty."

Since there are no viper.SetDefault calls for the four INTENT_*_FEE_PROGRAM env vars, unset programs default to empty strings, which arkfee.New handles by leaving those estimator fields as nil. This is the intended design—all four programs are optional, not required. No breaking change or startup failure will occur if operators don't configure them.

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/core/application/service.go (1)

1726-1820: Fix incorrect output categorization logic.

Line 1819 appends every output to offchainOutputs, including those already categorized as onchain outputs on line 1789. This means onchain outputs are included in both onchainOutputs and offchainOutputs, which will result in incorrect fee calculation.

Apply this diff to fix the logic:

 	for outputIndex, output := range proof.UnsignedTx.TxOut {
 		amount := uint64(output.Value)
 		rcv := domain.Receiver{
 			Amount: amount,
 		}

 		isOnchainOutput := slices.Contains(message.OnchainOutputIndexes, outputIndex)
 		if isOnchainOutput {
 			// ... validation and setting rcv.OnchainAddress ...
 			onchainOutputs = append(onchainOutputs, *output)
 		} else {
 			// ... validation and setting rcv.PubKey ...
+			offchainOutputs = append(offchainOutputs, *output)
 		}

 		receivers = append(receivers, rcv)
-		offchainOutputs = append(offchainOutputs, *output)
 	}
🧹 Nitpick comments (2)
internal/infrastructure/feemanager/arkfee.go (2)

18-25: Consider storing the estimator as a pointer to avoid copying.

The constructor dereferences the estimator pointer (line 24: Estimator: *estimator), which embeds the entire estimator by value. If arkfee.Estimator is large or contains non-copyable fields (mutexes, channels), this could lead to inefficiency or subtle bugs.

Consider changing the struct to embed a pointer:

 type arkFeeManager struct {
-	arkfee.Estimator
+	*arkfee.Estimator
 }

 func NewArkFeeManager(config arkfee.Config) (ports.FeeManager, error) {
 	estimator, err := arkfee.New(config)
 	if err != nil {
 		return nil, err
 	}

-	return arkFeeManager{Estimator: *estimator}, nil
+	return &arkFeeManager{Estimator: estimator}, nil
 }

60-72: Eliminate duplicate code.

The functions toArkFeeOffchainOutput and toArkFeeOnchainOutput are identical. This duplication can be removed by using a single helper function.

Apply this refactor:

-func toArkFeeOffchainOutput(output wire.TxOut) arkfee.Output {
+func toArkFeeOutput(output wire.TxOut) arkfee.Output {
 	return arkfee.Output{
 		Amount: uint64(output.Value),
 		Script: hex.EncodeToString(output.PkScript),
 	}
 }

-func toArkFeeOnchainOutput(output wire.TxOut) arkfee.Output {
-	return arkfee.Output{
-		Amount: uint64(output.Value),
-		Script: hex.EncodeToString(output.PkScript),
-	}
-}

Then update the calls in GetIntentFees:

 	for _, output := range offchainOutputs {
-		arkfeeOffchainOutputs = append(arkfeeOffchainOutputs, toArkFeeOffchainOutput(output))
+		arkfeeOffchainOutputs = append(arkfeeOffchainOutputs, toArkFeeOutput(output))
 	}

 	arkfeeOnchainOutputs := make([]arkfee.Output, 0, len(onchainOutputs))
 	for _, output := range onchainOutputs {
-		arkfeeOnchainOutputs = append(arkfeeOnchainOutputs, toArkFeeOnchainOutput(output))
+		arkfeeOnchainOutputs = append(arkfeeOnchainOutputs, toArkFeeOutput(output))
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e441b5 and 435ac55.

📒 Files selected for processing (5)
  • internal/config/config.go (9 hunks)
  • internal/core/application/service.go (11 hunks)
  • internal/core/ports/fee.go (1 hunks)
  • internal/infrastructure/feemanager/arkfee.go (1 hunks)
  • pkg/ark-lib/arkfee/estimator.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/ark-lib/arkfee/estimator.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/core/application/service.go (5)
internal/core/ports/fee.go (1)
  • FeeManager (10-16)
internal/core/application/types.go (1)
  • IntentFeeInfo (100-105)
pkg/errors/errors.go (4)
  • INVALID_INTENT_PROOF (292-296)
  • InvalidIntentProofMetadata (173-176)
  • INTENT_INSUFFICIENT_FEE (325-329)
  • IntentInsufficientFeeMetadata (195-200)
pkg/ark-lib/intent/proof.go (2)
  • New (134-158)
  • Proof (39-41)
internal/core/domain/offchain_tx.go (1)
  • Tx (30-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and Scan
  • GitHub Check: unit tests
  • GitHub Check: integration tests
🔇 Additional comments (11)
internal/core/ports/fee.go (1)

10-16: LGTM!

The FeeManager interface is well-designed with a clear contract for computing fees based on boarding inputs, VTXO inputs, and onchain/offchain outputs.

internal/infrastructure/feemanager/arkfee.go (1)

27-58: LGTM!

The GetIntentFees implementation correctly converts internal types to ArkFee types and invokes the estimator. Error handling is properly propagated.

internal/core/application/service.go (5)

71-73: LGTM!

The service struct now uses feeManager and intentFeeInfo instead of a static fee value, enabling dynamic fee calculation based on CEL formulas.


1851-1884: Fee validation logic is well-structured.

The dynamic fee calculation using feeManager.GetIntentFees properly replaces the static fee check. Error handling and metadata are appropriately detailed.

Note: This logic depends on the correct output categorization flagged in the previous comment (lines 1726-1820).


2269-2361: LGTM!

The new EstimateFee method provides a clean interface for fee estimation without intent registration. Time validation, input/output categorization, and error handling are all correctly implemented.


2041-2043: LGTM!

The GetInfo method now correctly exposes the CEL fee programs via intentFeeInfo, allowing clients to understand the fee structure.


2400-2417: Good defensive coding.

The cleanup logic now includes proper nil/empty checks before deleting tree signing sessions and boarding input signatures, preventing potential cache operations with invalid keys.

internal/config/config.go (4)

127-134: LGTM!

The Config struct now includes the four Intent fee program fields and a fee manager, enabling dynamic CEL-based fee configuration.


654-665: LGTM!

The feeManager() method correctly constructs an ArkFeeManager using the CEL program configuration and provides clear error handling.


577-579: LGTM!

Validating the fee manager during config validation ensures that any CEL syntax errors or configuration issues are caught early.


839-857: LGTM!

The application service is correctly instantiated with the fee manager and fee info, completing the integration of CEL-based dynamic fee estimation.

@altafan altafan merged commit 1c225d8 into master Dec 15, 2025
6 checks passed
@bitcoin-coder-bob bitcoin-coder-bob mentioned this pull request Dec 18, 2025
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.

Add EstimateIntentFee RPC Compute and Verify intent fee on RegisterIntent

3 participants