Skip to content

Conversation

@louisinger
Copy link
Contributor

@louisinger louisinger commented Dec 8, 2025

arkade-os/arkd#860

Summary by CodeRabbit

  • New Features

    • Added intent fee estimation API and client support; fee estimator used during coin selection and settlement for more accurate sizing.
  • Chores

    • Updated public data types and conversions to integrate the new fee estimator.
    • Dependency and client-generation updates to expose the new estimate-fee models.
    • Increased e2e test timing to account for processing.
  • Breaking Changes

    • Fee configuration representation and some public method signatures changed; integrations must be updated.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Walkthrough

Integrated arkfee-based fee estimation into settlement and coin-selection: added arkfee imports and converters, refactored selectFunds/CoinSelect/settle signatures to accept receivers and an arkfee.Estimator, and updated selection/output sizing to compute per-output and per-input fees and return adjusted outputs.

Changes

Cohort / File(s) Summary
Client settlement & coin-selection updates
client.go, client/...client.go
settle signature now accepts receivers []types.Receiver; public Settle delegates to private settle. selectFunds signature changed to accept outputs []types.Receiver and *arkfee.Estimator; coin-selection and SendOffChain flows now instantiate/pass an arkfee.Estimator and receive adjusted outputs.
Coin selection fee-aware logic
internal/utils/utils.go
CoinSelect refactored to accept outputs and feeEstimator; accumulates output amounts including estimated output fees, evaluates per-input onchain/offchain fees during selection, computes change including its estimated fee, handles dust with estimator-aware adjustments, and surfaces estimator errors.
Type conversions for arkfee
types/types.go, client/client.go
Replaced local IntentFeeInfo with arkfee.Config; added conversions Utxo.ToArkFeeInput(), Receiver.ToArkFeeOutput(), and TapscriptsVtxo.ToArkFeeInput() to produce arkfee input/output types.
Transport client & API additions
client/client.go, client/grpc/client.go, client/rest/client.go
TransportClient gains EstimateIntentFee(ctx, proof, message); gRPC/REST clients implement EstimateIntentFee; server fee parsing switched to populate arkfee.Config program fields for intent fees.
OpenAPI + REST client models & endpoint
api-spec/.../service.openapi.json, client/rest/service/*.go
Added POST /v1/batch/estimateFee endpoint and new request/response models (EstimateIntentFeeRequest/Response) and generated client methods/models (model_estimate_intent_fee_*.go, api_ark_service.go).
Store serialization/deserialization of fees
store/file/config_store.go, store/file/types.go
Persistence layer updated to store/load intent fee program fields into/from arkfee.Config (program strings) instead of prior numeric onchain/offchain fields.
Client helper additions
client/client.go
Added TapscriptsVtxo.ToArkFeeInput() helper to produce arkfee.OffchainInput for fee estimation.
Module dependencies
go.mod
Bumped ark-lib pre-release reference and added/updated indirect dependencies (CEL, antlr, googleapis genproto, stoewer).
Tests / timing tweak
test/e2e/transaction_test.go
Increased sleep from 1s → 3s in an e2e test branch that finalizes pending tx.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant Ark as arkClient
    participant FeeEst as arkfee.Estimator
    participant CoinSel as CoinSelect
    participant Types as Types (Utxo/Receiver/Vtxo)

    Note over Ark,FeeEst: Create arkfee.Estimator from server FeeInfo.IntentFees
    Caller->>Ark: settle(ctx, receivers[])
    Ark->>Types: Receiver.ToArkFeeOutput() / Utxo.ToArkFeeInput() / Vtxo.ToArkFeeInput()
    Ark->>FeeEst: NewEstimator(FeeInfo.IntentFees)
    Ark->>CoinSel: CoinSelect(boardingUtxos, vtxos, outputs[], feeEstimator)
    CoinSel->>FeeEst: EvalOutput(each output)
    FeeEst-->>CoinSel: output fee estimates
    CoinSel->>FeeEst: EvalOffchainInput / EvalOnchainInput(per input)
    FeeEst-->>CoinSel: input fee estimates
    CoinSel->>FeeEst: EvalOutput(change)
    FeeEst-->>CoinSel: change fee
    CoinSel-->>Ark: selected UTXOs, VTXOs, adjusted outputs[]
    Ark-->>Caller: proceed with adjusted receivers / tx id
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement arkfee package' clearly and concisely describes the main objective of the changeset: integrating the arkfee package throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

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

Caution

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

⚠️ Outside diff range comments (2)
internal/utils/utils.go (2)

63-78: Bug: break causes remaining boarding UTXOs to be lost.

When selectedAmount >= amount, the current boarding UTXO is appended to notSelectedBoarding, but then break exits the loop and all subsequent boarding UTXOs are lost. This affects the dust-handling logic at lines 126-136 which relies on notSelectedBoarding having available inputs.

Apply this diff to properly collect all unselected boarding UTXOs:

 for _, boardingUtxo := range boardingUtxos {
     if selectedAmount >= amount {
         notSelectedBoarding = append(notSelectedBoarding, boardingUtxo)
-        break
+        continue
     }
 
     selectedBoarding = append(selectedBoarding, boardingUtxo)

80-95: Bug: break causes remaining VTXOs to be lost.

Same issue as with boarding UTXOs. When selectedAmount >= amount, remaining VTXOs after the current one are not added to notSelected, which affects the dust-handling logic at lines 115-125.

Apply this diff to properly collect all unselected VTXOs:

 for _, vtxo := range vtxos {
     if selectedAmount >= amount {
         notSelected = append(notSelected, vtxo)
-        break
+        continue
     }
 
     selected = append(selected, vtxo)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8dc59a and fc9ca89.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • client.go (7 hunks)
  • client/client.go (2 hunks)
  • go.mod (5 hunks)
  • internal/utils/utils.go (5 hunks)
  • types/types.go (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T07:46:07.938Z
Learnt from: louisinger
Repo: arkade-os/go-sdk PR: 6
File: client.go:1194-1211
Timestamp: 2025-08-13T07:46:07.938Z
Learning: The listenForBoardingTxs() function in client.go is started from the Unlock method in base_client.go when WithTransactionFeed is enabled and UtxoMaxAmount is non-zero, not from the LoadArkClient methods.

Applied to files:

  • internal/utils/utils.go
  • client.go
🧬 Code graph analysis (3)
types/types.go (1)
store/sql/sqlc/queries/models.go (1)
  • Utxo (22-35)
internal/utils/utils.go (2)
types/types.go (2)
  • Receiver (260-263)
  • Utxo (229-240)
client/client.go (1)
  • TapscriptsVtxo (99-102)
client.go (3)
types/types.go (2)
  • Receiver (260-263)
  • Utxo (229-240)
client/client.go (1)
  • TapscriptsVtxo (99-102)
internal/utils/utils.go (1)
  • CoinSelect (28-143)
⏰ 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: Sdk integration tests
  • GitHub Check: Sdk unit tests
🔇 Additional comments (8)
types/types.go (2)

242-250: LGTM - Correct mapping for boarding UTXO to ArkFee input.

The conversion correctly uses SpendableAt for Expiry and sets Type to InputTypeBoarding, which aligns with the semantics of boarding UTXOs. The hardcoded Weight: 0 appears intentional given the same pattern in TapscriptsVtxo.ToArkFeeInput() in client/client.go.


265-274: LGTM - Clean output type detection.

The method correctly determines output type based on whether the receiver address is onchain, providing proper ArkFee output classification.

client/client.go (1)

104-112: LGTM - Proper differentiation from Utxo.ToArkFeeInput().

This correctly uses InputTypeVtxo and ExpiresAt for VTXOs, contrasting with types.Utxo.ToArkFeeInput() which uses InputTypeBoarding and SpendableAt for boarding UTXOs. The semantic distinction is appropriate.

client.go (4)

720-748: LGTM - Clean fee estimation integration.

The CollaborativeExit correctly fetches fee info from the server, creates the fee estimator, and passes it through the coin selection flow. The outputs returned from selectFunds properly include any change, which is then passed to joinBatchWithRetry.


2076-2094: Approve "send all" logic with fee deduction.

The logic correctly handles the case where a single receiver with amount ≤ 0 means "send all available funds". It iterates through all inputs, sums their values, and deducts the fee for each input, resulting in the maximum sendable amount.


2154-2165: LGTM - Fee estimator construction.

The fee estimator is correctly instantiated using the offchain input/output fee expressions from the server's fee info.


750-756: Verify whether Settle() with nil receivers is intentional or a bug.

Settle() calls settle(ctx, nil, opts...) passing nil receivers. If this results in selectFunds computing amount=0, the settlement functionality would break. Determine if buildSettlement explicitly handles the nil receivers case (e.g., collecting all available VTXOs to self), or if this is actually an unintended behavior that should send funds to self instead.

go.mod (1)

8-8: Verify the ark-lib commit contains the arkfee package with expected exports.

The ark-lib dependency at line 8 is updated to a specific commit from Dec 8, 2025. This codebase imports and uses arkfee from github.com/arkade-os/arkd/pkg/ark-lib/arkfee throughout (Estimator, Input, Output, InputTypeBoarding, InputTypeVtxo, OutputTypeVtxo, OutputTypeOnchain, EvalOutput). Confirm this commit includes all required arkfee exports before merging.

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

♻️ Duplicate comments (1)
client.go (1)

2160-2170: Verify server response structure to prevent nil pointer dereference.

Similar to line 727, this code accesses info.Fees.IntentFees without nil checks. Ensure consistent error handling across both CollaborativeExit and settle functions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc9ca89 and 15623a2.

📒 Files selected for processing (1)
  • client.go (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T07:46:07.938Z
Learnt from: louisinger
Repo: arkade-os/go-sdk PR: 6
File: client.go:1194-1211
Timestamp: 2025-08-13T07:46:07.938Z
Learning: The listenForBoardingTxs() function in client.go is started from the Unlock method in base_client.go when WithTransactionFeed is enabled and UtxoMaxAmount is non-zero, not from the LoadArkClient methods.

Applied to files:

  • client.go
⏰ 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). (1)
  • GitHub Check: Sdk unit tests
🔇 Additional comments (3)
client.go (3)

17-17: LGTM!

The arkfee package import supports the fee estimation integration described in the PR objectives.


317-319: No action needed. SendOffChain correctly passes nil for the feeEstimator parameter. This function handles offchain-to-offchain payments (vtxo transfers only) and explicitly excludes boarding utxos, making fee estimation unnecessary. Fee estimation is only required in CollaborativeExit and settle because they interact with onchain UTXOs, which SendOffChain does not.

Likely an incorrect or invalid review comment.


721-732: The code is safe from nil pointer dereference; no changes needed.

IntentFees is a struct value field (not a pointer) within types.FeeInfo, so it's always allocated when a FeeInfo is created. The parseFees function ensures IntentFeeInfo is always populated in the returned FeeInfo struct. Direct access to info.Fees.IntentFees.OffchainInput and OffchainOutput cannot cause a nil dereference.

Likely an incorrect or invalid review comment.

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

♻️ Duplicate comments (2)
client.go (1)

2032-2113: Sweep‑all path in selectFunds still double-counts input fees and can make CoinSelect impossible to satisfy.

In selectFunds:

  • When len(outputs) == 0, you create a single offchain receiver with Amount: 0.

  • When len(outputs) == 1 && outputs[0].Amount <= 0 (the sweep‑all case used by Settle()), you:

    outputs[0].Amount += utxo.Amount
    fees, _ := feeEstimator.EvalOnchainInput(...)
    outputs[0].Amount -= uint64(fees.ToSatoshis())
    
    outputs[0].Amount += vtxo.Amount
    fees, _ := feeEstimator.EvalOffchainInput(...)
    outputs[0].Amount -= uint64(fees.ToSatoshis())

    i.e. you pre‑subtract per‑input fees for all candidate boarding UTXOs and VTXOs.

  • You then call utils.CoinSelect(..., outputs, a.Dust, opts.WithExpirySorting, feeEstimator) where CoinSelect itself:

    • Adds output fees to amount, and
    • Adds input fees to amount again for each selected input.

The net effect for sweep‑all is:

  • outputs[0].Amount starts at total funds minus all candidate input fees.
  • CoinSelect then adds fees again for each selected input and also adds output fees.

If the estimator charges a positive fee for the output, you end up requiring:

sum(selected inputs) >= sum(all inputs) + outputFee

which is impossible when all available inputs are selected; CoinSelect will return "not enough funds to cover amount" even though the actual balance is sufficient. This is essentially the same issue previously flagged about “fee calculation before coin selection” in the sweep‑all path.

To fix this, you’ll want to avoid double‑charging input fees. Two possible directions:

  • Option A (recommended): Don’t mutate outputs[0].Amount based on per‑input fees at all. Let CoinSelect see the raw sweep output (e.g., zero or a nominal target), select inputs with feeEstimator fully applied, and then post‑compute the effective payout from the selected inputs and resulting changeAmount.
  • Option B: If you keep the pre‑sweep logic, call CoinSelect with feeEstimator == nil in this specific branch so that input fees are only accounted for once (in your pre‑sweep calculation), and adjust how you treat the output fee.

Either way, the current combination of pre‑subtracting per‑input fees and passing the same estimator into CoinSelect makes the sweep‑all case incorrect and, for realistic positive output fees, unsatisfiable.

internal/utils/utils.go (1)

110-118: Fix possible uint64 underflows when subtracting fee from change.

change is uint64, and you subtract fees from it in three places:

  • Change-output fee:
    change -= uint64(fees.ToSatoshis())
  • Extra offchain input when change < dust:
    change += notSelected[0].Amount
    change -= uint64(fees.ToSatoshis())
  • Extra boarding UTXO in the same branch:
    change += notSelectedBoarding[0].Amount
    change -= uint64(fees.ToSatoshis())

If fees.ToSatoshis() exceeds the current change (e.g., very small change or pathological fee config), these subtractions will underflow and wrap to a very large value, corrupting the selection result.

Guard the subtraction with a bounds check and decide on a safe behavior (clamp to zero, or return an explicit error). For example:

- change -= uint64(fees.ToSatoshis())
+ feeAmt := uint64(fees.ToSatoshis())
+ if feeAmt > change {
+     // Either:
+     // return nil, nil, 0, fmt.Errorf("change output fee %d exceeds change %d", feeAmt, change)
+     // or clamp:
+     change = 0
+ } else {
+     change -= feeAmt
+ }

Apply the same pattern in the two branches that adjust change after adding an extra input.

Also applies to: 120-131, 133-142

🧹 Nitpick comments (3)
types/types.go (2)

10-17: Public FeeInfo change to arkfee.Config is a breaking API surface change.

Switching FeeInfo.IntentFees to arkfee.Config is a sensible consolidation but breaks existing callers that referenced the old struct fields. Ensure this is accompanied by a documented SDK version bump and migration notes for downstream users (or a compatibility shim, if needed).

Also applies to: 55-58


235-239: Tighten ArkFee conversion helpers to avoid silent failures.

Utxo.ToArkFeeInput() and Receiver.ToArkFeeOutput() look correct in terms of mapping amounts and pkScript bytes, but ToArkFeeOutput() swallows ToTxOut errors and returns a zero-value arkfee.Output. If this helper is ever used with unvalidated receivers, fee estimation could silently operate on {Amount:0, Script:""}.

Consider either:

  • Returning (arkfee.Output, error) from ToArkFeeOutput(), or
  • At least logging/annotating the error at call sites where bad receivers should be impossible.

This will make future misuse easier to diagnose.

Also applies to: 254-263

store/file/config_store.go (1)

76-85: Config-store intent fee mapping matches new arkfee.Config; consider upgrade semantics.

Persisting intent_fees.offchain_input/output and onchain_input/output from data.Fees.IntentFees.{Intent*Program} is consistent with the new arkfee-based config and with the decode logic in store/file/types.go.

If older state.json files contain legacy values that are not valid arkfee programs, be prepared either to document that users must re-run initialization or to add a small migration layer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15623a2 and e6d35e5.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • client.go (7 hunks)
  • client/client.go (2 hunks)
  • client/grpc/client.go (2 hunks)
  • client/rest/client.go (2 hunks)
  • go.mod (5 hunks)
  • internal/utils/utils.go (5 hunks)
  • store/file/config_store.go (1 hunks)
  • store/file/types.go (2 hunks)
  • types/types.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/client.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T07:46:07.938Z
Learnt from: louisinger
Repo: arkade-os/go-sdk PR: 6
File: client.go:1194-1211
Timestamp: 2025-08-13T07:46:07.938Z
Learning: The listenForBoardingTxs() function in client.go is started from the Unlock method in base_client.go when WithTransactionFeed is enabled and UtxoMaxAmount is non-zero, not from the LoadArkClient methods.

Applied to files:

  • internal/utils/utils.go
  • client.go
🧬 Code graph analysis (7)
internal/utils/utils.go (3)
types/types.go (2)
  • Receiver (249-252)
  • Utxo (222-233)
client/client.go (1)
  • TapscriptsVtxo (99-102)
explorer/service.go (1)
  • Output (80-84)
types/types.go (2)
explorer/service.go (2)
  • Utxo (105-111)
  • Output (80-84)
store/sql/sqlc/queries/models.go (1)
  • Utxo (22-35)
client/grpc/client.go (3)
types/types.go (2)
  • FeeInfo (55-58)
  • Config (26-47)
api-spec/protobuf/gen/ark/v1/types.pb.go (3)
  • FeeInfo (569-575)
  • FeeInfo (588-588)
  • FeeInfo (603-605)
client/rest/service/model_fee_info.go (1)
  • FeeInfo (21-24)
store/file/types.go (1)
types/types.go (2)
  • Config (26-47)
  • OnchainOutput (305-312)
store/file/config_store.go (1)
types/types.go (1)
  • OnchainOutput (305-312)
client/rest/client.go (4)
types/types.go (2)
  • FeeInfo (55-58)
  • Config (26-47)
api-spec/protobuf/gen/ark/v1/types.pb.go (3)
  • FeeInfo (569-575)
  • FeeInfo (588-588)
  • FeeInfo (603-605)
client/rest/service/model_fee_info.go (1)
  • FeeInfo (21-24)
store/service.go (1)
  • Config (35-40)
client.go (4)
types/types.go (2)
  • Receiver (249-252)
  • Utxo (222-233)
types.go (1)
  • CoinSelectOptions (134-145)
client/client.go (1)
  • TapscriptsVtxo (99-102)
internal/utils/utils.go (1)
  • CoinSelect (28-149)
🔇 Additional comments (7)
go.mod (1)

3-4: Dependency bumps align with arkfee integration; verify toolchain & module graph.

The updated Go version and ark-lib/protobuf/CEL-related dependencies look consistent with the new arkfee-based fee handling. Please make sure your CI/build runs go mod tidy and compiles/tests this module with the configured Go toolchain so that any transitive incompatibilities surface early.

Also applies to: 8-9, 26-27, 31-37, 64-65, 109-110, 133-134

store/file/types.go (1)

8-12: ArkFee config reconstruction from file-store looks consistent.

decode() now builds types.FeeInfo with an arkfee.Config constructed from the stored intent_fees fields, which matches what config_store.AddData writes. This keeps file persistence aligned with the new fee model.

Just ensure arkfee’s Config parsing/validation accepts these string fields as-is (especially for environments upgrading from older configs).

Also applies to: 88-97, 150-158

client/grpc/client.go (1)

12-25: gRPC fee parsing → arkfee.Config wiring looks correct; handle empty-config case.

parseFees now:

  • Parses TxFeeRate from the string field, and
  • Maps fees.IntentFee.{OffchainInput,OffchainOutput,OnchainInput,OnchainOutput} into the four Intent*Program fields of arkfee.Config.

This is the expected shape for later arkfee.New(info.Fees.IntentFees) calls. When IntentFee is absent, the generated getters will yield empty strings; make sure arkfee.New either rejects that with a clear error (as you already propagate) or that the server always populates a valid config.

Also applies to: 515-541

client/rest/client.go (1)

13-20: REST fee parsing mirrors gRPC mapping into arkfee.Config.

parseFees correctly:

  • Parses TxFeeRate when present, and
  • Builds arkfee.Config from fees.IntentFee.{OffchainInput,OffchainOutput,OnchainInput,OnchainOutput}.

This keeps REST and gRPC in sync for fee configuration. As with the gRPC path, please confirm that ark_service.FeeInfo.GetIntentFee() behaves sanely when the field is absent, and that passing the resulting (possibly empty) config into arkfee.New is either supported or yields a clear error.

Also applies to: 530-552

internal/utils/utils.go (1)

28-35: Fee-aware CoinSelect integration is structurally sound.

Extending CoinSelect to accept outputs []types.Receiver and a *arkfee.Estimator and then:

  • Adding output fees up front,
  • Adding input fees as inputs are selected,

fits well with the new arkfee-based fee model and keeps call sites clean. Callers passing nil for feeEstimator also preserve the old behavior.

Also applies to: 40-57, 76-84, 95-101

client.go (2)

16-18: CollaborativeExit’s feeEstimator wiring is coherent with the new arkfee flow.

Fetching info := a.client.GetInfo(ctx), constructing feeEstimator := arkfee.New(info.Fees.IntentFees), and routing through selectFunds gives you a single source of truth for fee programs across CollaborativeExit and Settle. The pre-check on balance < amount is harmless, though note that selectFunds/arkfee will still be the ultimate arbiter once fees are factored in.

Also applies to: 677-745


2116-2179: Settle/receivers validation + feeEstimator integration look good; leverage selectFunds fix above.

The updated settle:

  • Validates each provided receiver as an Ark address for this signer and enforces receiver.Amount >= a.Dust.
  • Fetches info := a.client.GetInfo(ctx) and constructs feeEstimator := arkfee.New(info.Fees.IntentFees).
  • Calls selectFunds to obtain boarding UTXOs, vtxos, and final outputs, then passes those into joinBatchWithRetry.

Once the sweep‑all behavior in selectFunds is corrected, this flow will give you a single, arkfee-driven path for both explicit receivers and full-balance settlements without duplicating fee logic here.

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

♻️ Duplicate comments (3)
internal/utils/utils.go (2)

107-115: Potential underflow when subtracting change output fee.

If uint64(fees.ToSatoshis()) exceeds change, this will cause an unsigned integer underflow. Add a check to prevent this:

 if feeEstimator != nil {
-    fees, err := feeEstimator.EvalOffchainOutput(arkfee.Output{
+    fees, err := feeEstimator.EvalOffchainOutput(arkfee.Output{
         Amount: change,
     })
     if err != nil {
         return nil, nil, 0, err
     }
-    change -= uint64(fees.ToSatoshis())
+    feeAmount := uint64(fees.ToSatoshis())
+    if feeAmount > change {
+        return nil, nil, 0, fmt.Errorf("change output fee (%d) exceeds change amount (%d)", feeAmount, change)
+    }
+    change -= feeAmount
 }

117-143: Potential underflow when subtracting input fees from change.

Similar to the change output fee issue, subtracting input fees from change at lines 127 and 138 can underflow if the fee exceeds the change amount. Consider adding checks:

 if change < dust {
     if len(notSelected) > 0 {
         selected = append(selected, notSelected[0])
         change += notSelected[0].Amount

         if feeEstimator != nil {
             fees, err := feeEstimator.EvalOffchainInput(notSelected[0].ToArkFeeInput())
             if err != nil {
                 return nil, nil, 0, err
             }
-            change -= uint64(fees.ToSatoshis())
+            feeAmount := uint64(fees.ToSatoshis())
+            if feeAmount > change {
+                return nil, nil, 0, fmt.Errorf("input fee (%d) exceeds available change (%d)", feeAmount, change)
+            }
+            change -= feeAmount
         }
     } else if len(notSelectedBoarding) > 0 {
         selectedBoarding = append(selectedBoarding, notSelectedBoarding[0])
         change += notSelectedBoarding[0].Amount

         if feeEstimator != nil {
             fees, err := feeEstimator.EvalOnchainInput(notSelectedBoarding[0].ToArkFeeInput())
             if err != nil {
                 return nil, nil, 0, err
             }
-            change -= uint64(fees.ToSatoshis())
+            feeAmount := uint64(fees.ToSatoshis())
+            if feeAmount > change {
+                return nil, nil, 0, fmt.Errorf("boarding input fee (%d) exceeds available change (%d)", feeAmount, change)
+            }
+            change -= feeAmount
         }
     } else {
         change = 0
     }
 }
client.go (1)

2004-2035: Critical: Fee calculation occurs before coin selection in sweep-all case.

When sweeping all funds (amount <= 0), the code calculates fees for ALL boarding UTXOs and vtxos (lines 2011-2027) before calling CoinSelect at line 2030. However, CoinSelect may not select all inputs—it selects based on the target amount. This creates a mismatch:

  1. Fees are calculated for all available inputs
  2. CoinSelect selects only a subset of inputs to cover the target amount
  3. The actual fees paid will differ from the pre-calculated fees
  4. outputs[0].Amount will be incorrect

This breaks the sweep-all functionality.

Recommended fix:

For the sweep-all case, either:

  • Modify CoinSelect to have a sweep-all mode that selects all inputs when feeEstimator is provided and amount is computed from all inputs
  • Or refactor to call CoinSelect first (with a large enough target), then calculate the actual amount based on selected inputs and their fees
🧹 Nitpick comments (1)
client.go (1)

2047-2111: LGTM! The settle function refactoring is well-structured.

The function correctly:

  • Validates receiver addresses and amounts
  • Creates a fee estimator from server info
  • Uses selectFunds to perform fee-aware coin selection
  • Passes adjusted outputs to the batch joining logic
Optional: Update comment for clarity
-	// coinselect boarding utxos and vtxos
+	// Select inputs and compute adjusted outputs including fees and change
 	boardingUtxos, vtxos, outputs, err := a.selectFunds(

This better reflects that selectFunds does more than just coin selection—it also handles fee calculation and change output creation.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6d35e5 and b8fc31a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • client.go
  • client/grpc/client.go
  • go.mod
  • internal/utils/utils.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T07:46:07.938Z
Learnt from: louisinger
Repo: arkade-os/go-sdk PR: 6
File: client.go:1194-1211
Timestamp: 2025-08-13T07:46:07.938Z
Learning: The listenForBoardingTxs() function in client.go is started from the Unlock method in base_client.go when WithTransactionFeed is enabled and UtxoMaxAmount is non-zero, not from the LoadArkClient methods.

Applied to files:

  • internal/utils/utils.go
  • client.go
🧬 Code graph analysis (3)
client/grpc/client.go (3)
types/types.go (2)
  • FeeInfo (55-58)
  • Config (26-47)
client/rest/service/model_fee_info.go (1)
  • FeeInfo (21-24)
api-spec/protobuf/gen/ark/v1/types.pb.go (3)
  • FeeInfo (569-575)
  • FeeInfo (588-588)
  • FeeInfo (603-605)
internal/utils/utils.go (3)
types/types.go (2)
  • Receiver (249-252)
  • Utxo (222-233)
client/client.go (1)
  • TapscriptsVtxo (99-102)
explorer/service.go (1)
  • Output (80-84)
client.go (4)
types/types.go (2)
  • Receiver (249-252)
  • Utxo (222-233)
types.go (1)
  • CoinSelectOptions (134-145)
client/client.go (1)
  • TapscriptsVtxo (99-102)
internal/utils/utils.go (1)
  • CoinSelect (28-146)
⏰ 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: Sdk integration tests
  • GitHub Check: Sdk unit tests
🔇 Additional comments (8)
go.mod (2)

8-8: Ark-lib version update is appropriate for arkfee integration.

The ark-lib dependency update properly supports the arkfee fee estimation implementation used throughout the codebase (client.go, types.go, and related modules). The CEL-related dependencies appearing in go.mod are transitive dependencies from ark-lib, not direct additions to this project.


133-134: These are indirect dependencies (transitive, not directly imported in the codebase). No compatibility issues found—go mod verify passes successfully with no breaking imports or version conflicts.

internal/utils/utils.go (3)

28-54: LGTM! Output fee calculation logic is correct.

The function signature update and output fee calculation implementation look good:

  • Properly differentiates between onchain and offchain outputs
  • Correctly accumulates fees into the total amount required
  • Handles errors appropriately

67-82: LGTM! Boarding UTXO input fee handling is correct.

The onchain input fee calculation for boarding UTXOs is properly implemented:

  • Evaluates fee using EvalOnchainInput
  • Adds fee to the target amount, correctly accounting for the cost of using each input
  • Handles errors appropriately

84-99: LGTM! Vtxo input fee handling is correct.

The offchain input fee calculation for vtxos is properly implemented and consistent with the boarding UTXO handling.

client.go (2)

330-332: LGTM! SendOffChain correctly passes nil for feeEstimator.

For pure off-chain transfers, not using fee estimation appears intentional and maintains backward compatibility.


733-757: LGTM! CollaborativeExit fee estimation integration is correct.

The implementation properly:

  • Retrieves fee configuration from the server
  • Creates a fee estimator
  • Passes it to selectFunds for accurate coin selection
  • Uses the adjusted outputs for batch joining
client/grpc/client.go (1)

12-12: No action required. The arkfee library compatibility and field mapping in parseFees are correct and consistent with the arkfee.Config struct definition in the specified version.

@louisinger louisinger requested a review from altafan January 6, 2026 13:29
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: 1

🤖 Fix all issues with AI agents
In @go.mod:
- Line 32: go.mod still lists unused CEL-related indirect modules (cel.dev/expr,
antlr4-go/antlr, google/cel-go, stoewer/go-strcase); remove those module lines
from go.mod and run `go mod tidy` to purge transitive entries, then run the
provided verification commands (search for "cel.dev", "google/cel-go", "antlr",
"go-strcase", and CEL API patterns) to confirm no code imports or references
remain; update the PR with the cleaned go.mod/go.sum and mention that `go mod
tidy` was run.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8fc31a and 8b7ed53.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (1)
  • go.mod
⏰ 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: Sdk integration tests
  • GitHub Check: Sdk unit tests

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 (1)
test/e2e/transaction_test.go (1)

519-519: Consider polling with timeout instead of fixed sleep.

The increased sleep duration from 1 to 3 seconds suggests the test was experiencing timing-related flakiness. While this may improve reliability, a fixed sleep is brittle and can unnecessarily slow down tests in fast environments or still fail in slower CI pipelines.

A more robust approach would be to poll GetTransactionHistory with a reasonable timeout (e.g., 5 seconds) and a short retry interval (e.g., 100-200ms), returning as soon as the transaction appears.

♻️ Example polling implementation
-		time.Sleep(3 * time.Second)
-
-		history, err = restoredAlice.GetTransactionHistory(ctx)
-		require.NoError(t, err)
-		require.True(t, slices.ContainsFunc(history, func(tx types.Transaction) bool {
-			return tx.TransactionKey.String() == txid
-		}))
+		// Poll for the transaction to appear in history
+		require.Eventually(t, func() bool {
+			history, err := restoredAlice.GetTransactionHistory(ctx)
+			if err != nil {
+				return false
+			}
+			return slices.ContainsFunc(history, func(tx types.Transaction) bool {
+				return tx.TransactionKey.String() == txid
+			})
+		}, 5*time.Second, 200*time.Millisecond, "transaction should appear in history after auto-finalization")
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b7ed53 and 22f7563.

📒 Files selected for processing (1)
  • test/e2e/transaction_test.go
⏰ 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: Sdk unit tests
  • GitHub Check: Sdk integration tests

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: 2

🤖 Fix all issues with AI agents
In @client/grpc/client.go:
- Around line 219-231: The EstimateIntentFee method in grpcClient returns -1 on
error; change it to return the zero value for int64 (0) when an error occurs.
Locate func (a *grpcClient) EstimateIntentFee(ctx context.Context, proof,
message string) and replace the return statement in the error branch (currently
"return -1, err") with "return 0, err" so callers receive the idiomatic zero
value alongside the error.

In @client/rest/client.go:
- Around line 130-144: The function restClient.EstimateIntentFee currently
returns -1 on error; change the error return to return the zero value 0 instead
(i.e., return 0, err) so callers rely on the error rather than a sentinel value;
update the error branch in EstimateIntentFee to return 0 instead of -1 and leave
the successful return as resp.GetFee(), nil.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22f7563 and 802034a.

⛔ Files ignored due to path filters (2)
  • api-spec/protobuf/gen/ark/v1/service.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • api-spec/protobuf/gen/ark/v1/service_grpc.pb.go is excluded by !**/*.pb.go, !**/gen/**
📒 Files selected for processing (8)
  • api-spec/openapi/swagger/ark/v1/service.openapi.json
  • client/client.go
  • client/grpc/client.go
  • client/rest/client.go
  • client/rest/service/.openapi-generator/FILES
  • client/rest/service/api_ark_service.go
  • client/rest/service/model_estimate_intent_fee_request.go
  • client/rest/service/model_estimate_intent_fee_response.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T07:46:07.938Z
Learnt from: louisinger
Repo: arkade-os/go-sdk PR: 6
File: client.go:1194-1211
Timestamp: 2025-08-13T07:46:07.938Z
Learning: The listenForBoardingTxs() function in client.go is started from the Unlock method in base_client.go when WithTransactionFeed is enabled and UtxoMaxAmount is non-zero, not from the LoadArkClient methods.

Applied to files:

  • client/client.go
🧬 Code graph analysis (3)
client/rest/service/model_estimate_intent_fee_request.go (1)
api-spec/protobuf/gen/ark/v1/service.pb.go (3)
  • EstimateIntentFeeRequest (339-344)
  • EstimateIntentFeeRequest (357-357)
  • EstimateIntentFeeRequest (372-374)
client/rest/client.go (5)
client/rest/service/model_estimate_intent_fee_request.go (1)
  • EstimateIntentFeeRequest (21-23)
api-spec/protobuf/gen/ark/v1/service.pb.go (3)
  • EstimateIntentFeeRequest (339-344)
  • EstimateIntentFeeRequest (357-357)
  • EstimateIntentFeeRequest (372-374)
api-spec/protobuf/gen/ark/v1/types.pb.go (6)
  • Intent (441-447)
  • Intent (460-460)
  • Intent (475-477)
  • FeeInfo (569-575)
  • FeeInfo (588-588)
  • FeeInfo (603-605)
types/types.go (2)
  • FeeInfo (55-58)
  • Config (26-47)
client/rest/service/model_fee_info.go (1)
  • FeeInfo (21-24)
client/rest/service/model_estimate_intent_fee_response.go (1)
api-spec/protobuf/gen/ark/v1/service.pb.go (3)
  • EstimateIntentFeeResponse (383-388)
  • EstimateIntentFeeResponse (401-401)
  • EstimateIntentFeeResponse (416-418)
⏰ 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: Sdk unit tests
  • GitHub Check: Sdk integration tests
🔇 Additional comments (10)
api-spec/openapi/swagger/ark/v1/service.openapi.json (2)

90-130: LGTM!

The new EstimateIntentFee endpoint follows the established API patterns and includes proper request/response definitions with standard error handling.


633-651: LGTM!

The schema definitions are well-structured. The use of int64 for the fee field is appropriate for representing satoshi amounts.

client/rest/service/.openapi-generator/FILES (1)

12-13: LGTM!

The new model files align with the EstimateIntentFeeRequest and EstimateIntentFeeResponse schemas added to the OpenAPI specification.

client/client.go (2)

31-31: LGTM!

The new interface method follows the established pattern of other intent-related methods and appropriately returns int64 for the fee amount in satoshis.


105-118: Verify if Weight: 0 is correct—requires checking the arkfee package specification.

The ToArkFeeInput method sets Weight to a hardcoded 0. The Vtxo struct contains no weight field to source this value from, and there are no comments explaining whether this is intentional. This requires verification against the external arkfee package (from github.com/arkade-os/arkd/pkg/ark-lib/arkfee) to confirm:

  • Whether OffchainInput.Weight is actually used in fee calculations
  • If 0 is an acceptable/valid value for offchain input weight estimation
  • Whether weight should be dynamically calculated from input characteristics
client/grpc/client.go (1)

532-558: LGTM!

The refactoring correctly migrates from types.IntentFeeInfo to arkfee.Config, mapping the intent fee fields to their corresponding program properties. This aligns with the PR's goal of integrating arkfee-based fee estimation.

client/rest/client.go (1)

546-568: LGTM!

The parseFees implementation correctly migrates to arkfee.Config and is consistent with the gRPC client implementation, ensuring uniform fee handling across transport layers.

client/rest/service/model_estimate_intent_fee_request.go (1)

1-126: LGTM! Standard OpenAPI-generated model code.

This file introduces the EstimateIntentFeeRequest model with proper constructors, accessors, JSON serialization, and nullable wrapper support. The implementation follows established patterns and is consistent with the protobuf definitions.

client/rest/service/api_ark_service.go (1)

265-384: LGTM! Properly structured API endpoint implementation.

The new ArkServiceEstimateIntentFee endpoint follows the established patterns in this file, including proper request validation, error handling, and response decoding. The implementation is consistent with other ArkService endpoints.

client/rest/service/model_estimate_intent_fee_response.go (1)

1-126: LGTM! Standard OpenAPI-generated response model.

This file introduces the EstimateIntentFeeResponse model with a Fee field and complete accessor/serialization support. The implementation follows the same patterns as other generated models and properly handles optional fields.

@altafan altafan merged commit e1ab10a into master Jan 7, 2026
4 of 5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 8, 2026
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