Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.

Conversation

@garry-sharp
Copy link
Contributor

@garry-sharp garry-sharp commented Aug 19, 2025

Summary by CodeRabbit

  • New Features

    • Dry Run mode added to fee configuration for safe simulations.
    • Public config wrapper enables explicit raw chain ID input and stricter chain ID initialization.
  • Behavior Changes

    • Validation-only API introduced for runtime policy validation of proposed transactions; legacy DB-backed signing flows deprecated.
    • Deprecated signing hooks exposed as placeholders for compatibility.
  • Refactor

    • Automated fee loading and fee-run assembly with controlled concurrency.
    • Transaction processing reworked into a concurrent, policy-driven proposal-and-signing orchestrator with improved logging.

Copilot AI review requested due to automatic review settings August 19, 2025 06:29
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a DryRun flag and a FeeConfigFileWrapper for chain_id unmarshalling; introduces LoadFees with semaphore-limited concurrency to fetch/attach pending fees; implements HandleTransactions with concurrent run processing, propose/execute flows and signing orchestration; adds/changes public APIs (ValidateProposedTransactions, ProposeTransactions/SigningComplete stubs).

Changes

Cohort / File(s) Summary
Config restructuring
plugin/fees/config.go
Added DryRun bool to FeeConfig; introduced FeeConfigFileWrapper with ChainIdRaw uint64 for unmarshalling; WithFileConfig uses the wrapper and sets ChainId (*big.Int) via SetUint64; added WithDryRun; stricter ChainId validation and explicit defaults.
Validation API
plugin/fees/fees.go
Added public ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error that validates transactions via recipe evaluation (base64 decode + engine.Evaluate); removed prior LoadFees/HandleTransactions from this file.
Public helper stubs
plugin/fees/helper.go
Added exported stubs ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.PluginKeysignRequest, error) and SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, policy vtypes.PluginPolicy) error returning not-implemented errors; adjusted imports.
Fee loading pipeline
plugin/fees/load.go
Implemented LoadFees(ctx context.Context, task *asynq.Task) error with semaphore-limited concurrency and executeFeeLoading to fetch pending fees from verifier, validate totals, create/attach fees to draft FeeRuns, and persist via repo calls with error wrapping and logging.
Transaction processing & proposals
plugin/fees/transaction.go
Implemented HandleTransactions(ctx, task) plus executeFeeTransaction and internal proposeTransactions: fetch runs, validate run/policy, resolve vault/addresses, construct ERC20 (USDC) transfer proposals, build keysign requests, and initiate signing (initSign) with concurrency control; removed older SigningComplete/ValidateProposedTransactions implementations in this path; added richer logging and error wrapping.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Scheduler as Job Scheduler
  participant FP as FeePlugin
  participant Repo as Repo/DB
  participant Verifier as Verifier API

  Scheduler->>FP: LoadFees(ctx, task)
  activate FP
  FP->>Repo: GetAllFeePolicies()
  loop per policy (semaphore-limited)
    FP->>Verifier: GetPublicKeysFees(policy.PublicKey)
    alt fees pending
      FP->>Repo: GetPendingFeeRun(policy.ID)
      alt no draft run
        FP->>Repo: CreateFeeRun(Draft)
      end
      FP->>Repo: CreateFee(fee) for each uncollected
    else no fees
      Note over FP: skip
    end
  end
  deactivate FP
Loading
sequenceDiagram
  autonumber
  actor Scheduler as Job Scheduler
  participant FP as FeePlugin
  participant Repo as Repo/DB
  participant Vault as Vault/Treasury
  participant Sign as Sign Service

  Scheduler->>FP: HandleTransactions(ctx, task)
  activate FP
  FP->>Repo: GetAllFeeRuns()
  loop each eligible run (semaphore-limited)
    FP->>Repo: GetPolicy(run.PolicyID)
    FP->>Vault: Resolve sender addr, token (USDC), treasury addr
    FP->>FP: proposeTransactions(policy, run)
    alt proposals ready
      loop each request
        FP->>Sign: initSign(request)
      end
    else error
      Note over FP: Log and handle per-run
    end
  end
  deactivate FP
Loading
sequenceDiagram
  autonumber
  participant Caller as Caller
  participant FP as FeePlugin
  participant Engine as Recipe Engine

  Caller->>FP: ValidateProposedTransactions(policy, txs)
  activate FP
  FP->>FP: ValidatePluginPolicy(policy)
  loop each tx
    FP->>FP: base64 decode tx.payload
    FP->>Engine: Evaluate(recipe, chain, txBytes)
    Engine-->>FP: result / error
  end
  FP-->>Caller: success / error
  deactivate FP
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I nibble configs with a tiny hop,
chain_id tucked in a wrapper crop.
Semaphores hum while drafts align,
USDC carrots travel down the line.
I thump, I sign — the backlog's fine. 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fee-code-location-move

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the fee collection code by reorganizing it across multiple files for better modularity and maintainability. The main purpose is to separate concerns by moving transaction handling logic, fee loading logic, and deprecated methods into dedicated files.

  • Moves transaction handling logic from the main fees.go file to transaction.go
  • Extracts fee loading functionality into a new load.go file
  • Relocates deprecated methods to helper.go
  • Updates configuration handling to support dry run mode and improves chain ID validation

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
plugin/fees/transaction.go Contains refactored transaction handling logic with new HandleTransactions and executeFeeTransaction methods
plugin/fees/load.go New file containing fee loading functionality extracted from fees.go
plugin/fees/helper.go Updated with deprecated method implementations moved from transaction.go
plugin/fees/fees.go Simplified main file with transaction validation logic and reduced scope
plugin/fees/config.go Enhanced configuration with dry run support and improved chain ID handling

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

fp.logger.WithFields(logrus.Fields{
"publicKey": feePolicy.PublicKey,
}).Info("Getting vault")
vaultFileName := vcommon.GetVaultBackupFilename(feePolicy.PublicKey, vtypes.PluginVultisigFees_feee.String())
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

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

There's a typo in the enum value PluginVultisigFees_feee - it should likely be PluginVultisigFees_fee (with three 'e's instead of four).

Suggested change
vaultFileName := vcommon.GetVaultBackupFilename(feePolicy.PublicKey, vtypes.PluginVultisigFees_feee.String())
vaultFileName := vcommon.GetVaultBackupFilename(feePolicy.PublicKey, vtypes.PluginVultisigFees_fee.String())

Copilot uses AI. Check for mistakes.
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 (4)
plugin/fees/config.go (1)

39-42: Consider documenting the wrapper structure's purpose.

The FeeConfigFileWrapper serves as an intermediate structure to handle the raw chain_id unmarshalling. Consider adding a comment to explain why this wrapper pattern is necessary (i.e., to separate the raw uint64 chain_id value from the final *big.Int representation).

+// FeeConfigFileWrapper is used to unmarshal the config file, handling the raw chain_id
+// as uint64 before converting it to *big.Int in the main FeeConfig struct
 type FeeConfigFileWrapper struct {
 	FeeConfig  `mapstructure:",squash"`
 	ChainIdRaw uint64 `mapstructure:"chain_id"`
 }
plugin/fees/load.go (1)

40-51: Potential goroutine leak on context cancellation.

The goroutine launched in the errgroup doesn't check for context cancellation before acquiring the semaphore. If the context is cancelled while waiting to acquire, the goroutine might continue running unnecessarily.

Consider checking context before acquiring:

 eg.Go(func() error {
 	defer wg.Done()
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+	}
 	if err := sem.Acquire(ctx, 1); err != nil {
 		return fmt.Errorf("failed to acquire semaphore: %w", err)
 	}
plugin/fees/transaction.go (2)

262-265: Consider using the configured resource from policy.

The resource name is hardcoded as "ethereum.erc20.transfer" but should ideally be validated against a constant or configuration, especially given the learning that fees are only collected in USDC.

Consider defining this as a constant:

+const FeeTransferResource = "ethereum.erc20.transfer"
+
 resourceName := "ethereum.erc20.transfer"
 if rule.Resource != resourceName {
 	return nil, fmt.Errorf("rule resource expected to be %s", resourceName)
 }

296-302: Amount conversion may cause overflow for large values.

Converting run.TotalAmount (int) to int64 could potentially overflow for very large fee amounts. Consider validating the range or using big.Int throughout.

-amount := run.TotalAmount
+amount := big.NewInt(0).SetInt64(int64(run.TotalAmount))
+if amount.Cmp(big.NewInt(0)) <= 0 {
+	return nil, fmt.Errorf("invalid fee amount: %d", run.TotalAmount)
+}

 tx, err := fp.eth.MakeAnyTransfer(ctx,
 	gcommon.HexToAddress(ethAddress),
 	gcommon.HexToAddress(recipient),
 	gcommon.HexToAddress(usdc.Address),
-	big.NewInt(int64(amount)))
+	amount)
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d80752a and 4f28dd4.

📒 Files selected for processing (5)
  • plugin/fees/config.go (6 hunks)
  • plugin/fees/fees.go (2 hunks)
  • plugin/fees/helper.go (3 hunks)
  • plugin/fees/load.go (1 hunks)
  • plugin/fees/transaction.go (2 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.
Learnt from: webpiratt
PR: vultisig/plugin#141
File: plugin/payroll/transaction.go:208-267
Timestamp: 2025-07-24T19:34:47.441Z
Learning: In the vultisig/plugin codebase, duplicate transaction prevention for the payroll plugin has been moved to the verifier side, so the local `IsAlreadyProposed` check in plugin/payroll/transaction.go is no longer needed. The validation flow has changed and is now handled by the verifier rather than within the plugin itself.
📚 Learning: 2025-07-31T15:43:42.631Z
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.

Applied to files:

  • plugin/fees/helper.go
  • plugin/fees/load.go
  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-07-24T19:34:47.441Z
Learnt from: webpiratt
PR: vultisig/plugin#141
File: plugin/payroll/transaction.go:208-267
Timestamp: 2025-07-24T19:34:47.441Z
Learning: In the vultisig/plugin codebase, duplicate transaction prevention for the payroll plugin has been moved to the verifier side, so the local `IsAlreadyProposed` check in plugin/payroll/transaction.go is no longer needed. The validation flow has changed and is now handled by the verifier rather than within the plugin itself.

Applied to files:

  • plugin/fees/helper.go
  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:47:47.927Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/policy.go:46-47
Timestamp: 2025-07-04T10:47:47.927Z
Learning: For the fee plugin in plugin/fees/policy.go, the resource validation and recipe specification are intentionally configured to accept only USDC transfers ("ethereum.usdc.transfer"), not general ERC20 transfers ("ethereum.erc20.transfer"), as fees are only collected in USDC.

Applied to files:

  • plugin/fees/load.go
  • plugin/fees/fees.go
  • plugin/fees/config.go
  • plugin/fees/transaction.go
📚 Learning: 2025-05-07T08:23:45.882Z
Learnt from: RaghavSood
PR: vultisig/plugin#36
File: api/server.go:21-33
Timestamp: 2025-05-07T08:23:45.882Z
Learning: The import path `github.com/vultisig/verifier/plugin` refers to an external dependency that provides the plugin interface, and should not be changed to `github.com/vultisig/plugin/plugin` as these are distinct packages with different purposes.

Applied to files:

  • plugin/fees/fees.go
🧬 Code Graph Analysis (4)
plugin/fees/helper.go (2)
plugin/fees/fees.go (1)
  • FeePlugin (33-48)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
plugin/fees/load.go (2)
plugin/fees/fees.go (1)
  • FeePlugin (33-48)
internal/types/fees.go (1)
  • FeeRunStateDraft (14-14)
plugin/fees/fees.go (1)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
plugin/fees/transaction.go (4)
plugin/fees/fees.go (1)
  • FeePlugin (33-48)
internal/types/fees.go (2)
  • FeeRunStateDraft (14-14)
  • FeeRun (29-39)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
common/vault.go (1)
  • GetVaultFromPolicy (12-24)
🔇 Additional comments (7)
plugin/fees/config.go (1)

169-171: Good validation improvement for ChainId.

The validation now correctly checks for both nil and zero values for ChainId, ensuring it's properly initialized.

plugin/fees/helper.go (2)

121-124: Intentional placeholder aligns with fee plugin v2 architecture.

The ProposeTransactions method returning "not implemented" is intentional, as confirmed by the learning about the fees plugin v2's run-based architecture. The actual transaction proposal logic is in the private proposeTransactions method that accepts a types.FeeRun parameter.


126-129: SigningComplete deprecation aligns with new architecture.

The SigningComplete method is correctly marked as deprecated with "not implemented" since the new architecture handles signing through initSign and post-transaction flow, as mentioned in the comment.

plugin/fees/load.go (1)

79-87: Good validation of fee amounts.

The code properly validates that the sum of uncollected fees matches the FeesPendingCollection amount, preventing discrepancies in fee accounting.

plugin/fees/fees.go (1)

109-142: Well-structured policy-based validation flow.

The ValidateProposedTransactions method properly validates both the policy and each transaction using the recipe engine. The sequential validation approach ensures all transactions comply with the policy constraints.

plugin/fees/transaction.go (2)

41-106: Robust concurrent transaction handling with proper mutex control.

The HandleTransactions method correctly serializes operations with a mutex and processes fee runs concurrently with proper semaphore-based concurrency control. The lifecycle logging will help with debugging and monitoring.


210-213: Critical error handling for transaction state recording.

Excellent error handling here. The comment correctly highlights the importance of recording the transaction state to prevent internal state inconsistency after broadcast.

@garry-sharp garry-sharp force-pushed the fee-code-location-move branch 2 times, most recently from 30700c0 to 4caaa4e Compare August 20, 2025 23:01
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

Caution

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

⚠️ Outside diff range comments (1)
plugin/fees/fees.go (1)

115-148: Engine validation is decoding the wrong field; use RawMessage hex, not Message (hash).

keysigned Message is the keccak sign-hash (base64) while RawMessage carries the unsigned tx payload (hex). Passing the hash to the recipe engine will cause malformed/unevaluable input. Also, prefer %w for error wrapping.

Apply this diff:

@@
 func (fp *FeePlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error {
   // First validate the plugin policy itself
   err := fp.ValidatePluginPolicy(policy)
   if err != nil {
-    return fmt.Errorf("failed to validate plugin policy: %v", err)
+    return fmt.Errorf("failed to validate plugin policy: %w", err)
   }
@@
   recipe, err := policy.GetRecipe()
   if err != nil {
-    return fmt.Errorf("failed to get recipe from policy: %v", err)
+    return fmt.Errorf("failed to get recipe from policy: %w", err)
   }
@@
-  for _, tx := range txs {
-    for _, keysignMessage := range tx.Messages {
-      txBytes, err := base64.StdEncoding.DecodeString(keysignMessage.Message)
+  for _, tx := range txs {
+    for _, keysignMessage := range tx.Messages {
+      // Validate the actual unsigned transaction bytes
+      txBytes, err := hexutil.Decode(keysignMessage.RawMessage)
       if err != nil {
-        return fmt.Errorf("failed to decode transaction: %w", err)
+        return fmt.Errorf("failed to decode raw transaction: %w", err)
       }
 
       // Evaluate if the transaction is allowed by the policy
       _, err = eng.Evaluate(recipe, vgcommon.Chain(keysignMessage.Chain), txBytes)
       if err != nil {
         return fmt.Errorf("failed to evaluate transaction: %w", err)
       }
     }
   }

And ensure the import exists at the top of this file:

+ "github.com/ethereum/go-ethereum/common/hexutil"
🧹 Nitpick comments (3)
plugin/fees/transaction.go (3)

40-105: Redundant/misused WaitGroup; errgroup already handles lifecycle.

wg.Add is called inside the goroutine and wg.Wait() runs before eg.Wait(); this WaitGroup provides no ordering guarantees and can be dropped. Keep concurrency limiting via semaphore and the errgroup for error propagation.

Apply this diff:

@@
-  sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
-  var wg sync.WaitGroup
+  sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
   var eg errgroup.Group
   for _, run := range runs {
     run = run
     eg.Go(func() error {
@@
       feePolicy, err := fp.db.GetPluginPolicy(ctx, run.PolicyID)
       if err != nil {
         return fmt.Errorf("failed to get fee policy: %w", err)
       }
-      wg.Add(1)
       fp.logger.WithFields(logrus.Fields{"runId": run.ID, "policyId": run.PolicyID}).Info("Retrieved fee policy")
 
-      defer wg.Done()
       if err := sem.Acquire(ctx, 1); err != nil {
         return fmt.Errorf("failed to acquire semaphore: %w", err)
       }
       defer sem.Release(1)
@@
     })
   }
 
-  wg.Wait()
   if err := eg.Wait(); err != nil {
     return fmt.Errorf("failed to execute fee transaction: %w", err)
   }

If you adopt this, remove the now-unused sync import from this file.


63-75: Consider handling retryable “failed” runs too.

You have a TODO; if a run previously failed after signing/broadcast hiccups, it will never be retried. Consider explicit states and backoff. I can sketch a small state machine if helpful.


121-133: Avoid double vault retrieval/decrypt.

You ensure vault presence here, then proposeTransactions fetches/decrypts the vault again. Pass the vault down to avoid duplicate IO/decrypt.

Example signature change:

-func (fp *FeePlugin) executeFeeTransaction(ctx context.Context, run types.FeeRun, feePolicy vtypes.PluginPolicy) error {
+func (fp *FeePlugin) executeFeeTransaction(ctx context.Context, run types.FeeRun, feePolicy vtypes.PluginPolicy) error {
@@
-  _, err := common.GetVaultFromPolicy(fp.vaultStorage, feePolicy, fp.encryptionSecret)
+  vault, err := common.GetVaultFromPolicy(fp.vaultStorage, feePolicy, fp.encryptionSecret)
@@
-  keySignRequests, err := fp.proposeTransactions(ctx, feePolicy, run)
+  keySignRequests, err := fp.proposeTransactions(ctx, feePolicy, run, vault)

And update proposeTransactions to accept the vault without re-fetching.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 30700c0 and 4caaa4e.

📒 Files selected for processing (4)
  • plugin/fees/fees.go (2 hunks)
  • plugin/fees/helper.go (3 hunks)
  • plugin/fees/load.go (1 hunks)
  • plugin/fees/transaction.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/fees/helper.go
  • plugin/fees/load.go
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.
📚 Learning: 2025-07-31T15:43:42.631Z
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.

Applied to files:

  • plugin/fees/transaction.go
  • plugin/fees/fees.go
📚 Learning: 2025-07-24T19:34:47.441Z
Learnt from: webpiratt
PR: vultisig/plugin#141
File: plugin/payroll/transaction.go:208-267
Timestamp: 2025-07-24T19:34:47.441Z
Learning: In the vultisig/plugin codebase, duplicate transaction prevention for the payroll plugin has been moved to the verifier side, so the local `IsAlreadyProposed` check in plugin/payroll/transaction.go is no longer needed. The validation flow has changed and is now handled by the verifier rather than within the plugin itself.

Applied to files:

  • plugin/fees/transaction.go
  • plugin/fees/fees.go
📚 Learning: 2025-07-10T20:41:44.025Z
Learnt from: webpiratt
PR: vultisig/plugin#126
File: plugin/fees/policy.go:26-26
Timestamp: 2025-07-10T20:41:44.025Z
Learning: The constant `vtypes.PluginVultisigFees_feee` in the vultisig/plugin project is correctly spelled with "feee" (including the extra 'e'). This is the actual constant name defined in the external dependency github.com/vultisig/verifier/types and should not be changed to "fee" singular, as this is how it's defined in the external package.

Applied to files:

  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:50:42.832Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/constraints.go:3-3
Timestamp: 2025-07-04T10:50:42.832Z
Learning: For the vultisig/plugin project, use "fee" (singular) throughout the codebase wherever possible, not "fees" (plural). This applies to all naming conventions including plugin types, variable names, function names, comments, and documentation.

Applied to files:

  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:47:47.927Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/policy.go:46-47
Timestamp: 2025-07-04T10:47:47.927Z
Learning: For the fee plugin in plugin/fees/policy.go, the resource validation and recipe specification are intentionally configured to accept only USDC transfers ("ethereum.usdc.transfer"), not general ERC20 transfers ("ethereum.erc20.transfer"), as fees are only collected in USDC.

Applied to files:

  • plugin/fees/transaction.go
  • plugin/fees/fees.go
📚 Learning: 2025-08-20T13:51:05.194Z
Learnt from: neavra
PR: vultisig/plugin#157
File: api/plugin.go:236-236
Timestamp: 2025-08-20T13:51:05.194Z
Learning: In the vultisig/plugin codebase migration to vultisig-go, SystemMigrations() and vultisig_validator functionality were not moved from the verifier package to vultisig-go. This means some imports of github.com/vultisig/verifier/common are intentionally retained for accessing these specific functions, while vault-related types and common functionality were migrated to vultisig-go.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-08-20T13:51:41.769Z
Learnt from: neavra
PR: vultisig/plugin#157
File: api/server.go:22-24
Timestamp: 2025-08-20T13:51:41.769Z
Learning: In the vultisig-go migration, only vault-related functions (GetVaultBackupFilename, DecryptVaultFromBackup) were moved from github.com/vultisig/verifier/common to github.com/vultisig/vultisig-go/common. Other functions like SystemMigrations() remained in the verifier/common package, so files using these non-migrated functions should keep the original verifier/common import.

Applied to files:

  • plugin/fees/fees.go
🧬 Code Graph Analysis (2)
plugin/fees/transaction.go (4)
plugin/fees/fees.go (1)
  • FeePlugin (34-49)
internal/types/fees.go (2)
  • FeeRunStateDraft (14-14)
  • FeeRun (29-39)
common/vault.go (1)
  • GetVaultFromPolicy (12-24)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
plugin/fees/fees.go (1)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
⏰ 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: build
🔇 Additional comments (2)
plugin/fees/transaction.go (2)

107-116: Policy/run alignment check is solid.

Good early guard to avoid cross-policy leakage.


147-214: Helpers confirmed; remove stale TODO

I’ve verified that all three helpers live in plugin/fees/helper.go (lines 21–23, 46–48, 113–115), so there’s no missing implementation. The // TODO pass the real tx id at plugin/fees/transaction.go:206 is now stale—tx.Hash().Hex() already provides the correct identifier.

• Remove the // TODO pass the real tx id comment on line 206 of plugin/fees/transaction.go.
• (Optional) Ensure there are unit tests covering hexutilDecode, getHash, and decodeTx for the EVM flavor you’re targeting.

@garry-sharp garry-sharp force-pushed the fee-code-location-move branch from 4caaa4e to 09f218e Compare August 29, 2025 05:26
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: 4

♻️ Duplicate comments (1)
plugin/fees/transaction.go (1)

225-233: Use runtime chain ID for USDC token (avoid hard-coded 1)

Hard-coding ChainId=1 breaks non-mainnet deployments. Use config.

Apply this diff:

 	// ERC20 USDC Token List
 	var usdc *reth.Token = &reth.Token{
-		ChainId:  1,
+		ChainId:  int(fp.config.ChainId.Int64()),
 		Address:  fp.config.UsdcAddress,
 		Name:     "USD Coin",
 		Symbol:   "USDC",
 		Decimals: 6,
 	}
🧹 Nitpick comments (2)
plugin/fees/fees.go (1)

3-14: Update imports: drop base64, add hexutil

The refactor above requires hex decoding and no longer uses base64 here.

Apply this diff:

@@
-import (
-	"encoding/base64"
+import (
 	"fmt"
 	"sync"
@@
 	"github.com/ethereum/go-ethereum/ethclient"
+	"github.com/ethereum/go-ethereum/common/hexutil"
plugin/fees/transaction.go (1)

79-87: Remove redundant WaitGroup and use the cancelable context for semaphore Acquire

The WaitGroup duplicates errgroup.Wait(). Also pass gctx to semaphore to honor cancellation when a sibling errors.

Apply this diff:

-			wg.Add(1)
 			fp.logger.WithFields(logrus.Fields{"runId": run.ID, "policyId": run.PolicyID}).Info("Retrieved fee policy")
-
-			defer wg.Done()
-			if err := sem.Acquire(ctx, 1); err != nil {
+			if err := sem.Acquire(gctx, 1); err != nil {
 				return fmt.Errorf("failed to acquire semaphore: %w", err)
 			}

And at the end of the function:

-	wg.Wait()
-	if err := eg.Wait(); err != nil {
+	if err := eg.Wait(); err != nil {
 		return fmt.Errorf("failed to execute fee transaction: %w", err)
 	}

Also remove the now-unused sync import if nothing else uses it at the top of this file.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4caaa4e and 09f218e.

📒 Files selected for processing (5)
  • plugin/fees/config.go (6 hunks)
  • plugin/fees/fees.go (2 hunks)
  • plugin/fees/helper.go (3 hunks)
  • plugin/fees/load.go (1 hunks)
  • plugin/fees/transaction.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • plugin/fees/helper.go
  • plugin/fees/load.go
  • plugin/fees/config.go
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.
📚 Learning: 2025-07-24T19:34:47.441Z
Learnt from: webpiratt
PR: vultisig/plugin#141
File: plugin/payroll/transaction.go:208-267
Timestamp: 2025-07-24T19:34:47.441Z
Learning: In the vultisig/plugin codebase, duplicate transaction prevention for the payroll plugin has been moved to the verifier side, so the local `IsAlreadyProposed` check in plugin/payroll/transaction.go is no longer needed. The validation flow has changed and is now handled by the verifier rather than within the plugin itself.

Applied to files:

  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-07-31T15:43:42.631Z
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.

Applied to files:

  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:47:47.927Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/policy.go:46-47
Timestamp: 2025-07-04T10:47:47.927Z
Learning: For the fee plugin in plugin/fees/policy.go, the resource validation and recipe specification are intentionally configured to accept only USDC transfers ("ethereum.usdc.transfer"), not general ERC20 transfers ("ethereum.erc20.transfer"), as fees are only collected in USDC.

Applied to files:

  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-08-20T13:51:05.212Z
Learnt from: neavra
PR: vultisig/plugin#157
File: api/plugin.go:236-236
Timestamp: 2025-08-20T13:51:05.212Z
Learning: In the vultisig/plugin codebase migration to vultisig-go, SystemMigrations() and vultisig_validator functionality were not moved from the verifier package to vultisig-go. This means some imports of github.com/vultisig/verifier/common are intentionally retained for accessing these specific functions, while vault-related types and common functionality were migrated to vultisig-go.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-05-07T08:23:45.882Z
Learnt from: RaghavSood
PR: vultisig/plugin#36
File: api/server.go:21-33
Timestamp: 2025-05-07T08:23:45.882Z
Learning: The import path `github.com/vultisig/verifier/plugin` refers to an external dependency that provides the plugin interface, and should not be changed to `github.com/vultisig/plugin/plugin` as these are distinct packages with different purposes.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-08-20T13:51:41.822Z
Learnt from: neavra
PR: vultisig/plugin#157
File: api/server.go:22-24
Timestamp: 2025-08-20T13:51:41.822Z
Learning: In the vultisig-go migration, only vault-related functions (GetVaultBackupFilename, DecryptVaultFromBackup) were moved from github.com/vultisig/verifier/common to github.com/vultisig/vultisig-go/common. Other functions like SystemMigrations() remained in the verifier/common package, so files using these non-migrated functions should keep the original verifier/common import.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-07-10T20:41:44.025Z
Learnt from: webpiratt
PR: vultisig/plugin#126
File: plugin/fees/policy.go:26-26
Timestamp: 2025-07-10T20:41:44.025Z
Learning: The constant `vtypes.PluginVultisigFees_feee` in the vultisig/plugin project is correctly spelled with "feee" (including the extra 'e'). This is the actual constant name defined in the external dependency github.com/vultisig/verifier/types and should not be changed to "fee" singular, as this is how it's defined in the external package.

Applied to files:

  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:50:42.832Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/constraints.go:3-3
Timestamp: 2025-07-04T10:50:42.832Z
Learning: For the vultisig/plugin project, use "fee" (singular) throughout the codebase wherever possible, not "fees" (plural). This applies to all naming conventions including plugin types, variable names, function names, comments, and documentation.

Applied to files:

  • plugin/fees/transaction.go
🔇 Additional comments (2)
plugin/fees/transaction.go (2)

289-296: The commands are queued for execution.


260-270: Remove suggestion to use non-existent GetMagicConstant()
Constraint has no GetMagicConstant() method; continue using GetFixedValue() and parsing its string output.

Likely an incorrect or invalid review comment.

Comment on lines +55 to +61
sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
var wg sync.WaitGroup
var eg errgroup.Group
for _, run := range runs {
run = run
eg.Go(func() error {
//TODO also check failed runs
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use errgroup.WithContext to enable cancellation of siblings

An error in one worker should cancel others. Plain errgroup.Group doesn’t propagate cancellation. Use WithContext.

Apply this diff:

-	sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
-	var wg sync.WaitGroup
-	var eg errgroup.Group
+	sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
+	eg, gctx := errgroup.WithContext(ctx)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
var wg sync.WaitGroup
var eg errgroup.Group
for _, run := range runs {
run = run
eg.Go(func() error {
//TODO also check failed runs
sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
eg, gctx := errgroup.WithContext(ctx)
for _, run := range runs {
run = run
eg.Go(func() error {
//TODO also check failed runs
🤖 Prompt for AI Agents
In plugin/fees/transaction.go around lines 55 to 61, replace the plain
errgroup.Group with errgroup.WithContext so a failure in one worker cancels the
others: obtain a parent context (use the surrounding function's ctx if
available, otherwise context.Background()), call ctx, eg :=
errgroup.WithContext(parentCtx), and then in each goroutine use that ctx (e.g.,
sem.Acquire(ctx, 1) and check ctx for cancellation) and return errors via the eg
goroutine; keep using eg.Wait() to propagate the first error and ensure
sem.Release is deferred inside the goroutine.

Comment on lines +58 to +66
for _, run := range runs {
run = run
eg.Go(func() error {
//TODO also check failed runs
if run.Status != types.FeeRunStateDraft {
return nil
}

txs := []vtypes.PluginKeysignRequest{}
var magicConstantRecipientValue rtypes.MagicConstant = rtypes.MagicConstant_UNSPECIFIED
var token string
// This should only return one rule, but in case there are more/fewer rules, we'll loop through them all and error if it's the case.
for _, rule := range recipe.Rules {

// This section of code goes through the rules in the fee policy. It looks for the recipient of the fee collection policy and extracts it. If other data is found throws an error as they're unsupported rules.
var recipient string // The address specified in the fee policy.
switch rule.Resource {
case "ethereum.erc20.transfer":
for _, constraint := range rule.ParameterConstraints {
if constraint.ParameterName == "recipient" {
if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_MAGIC_CONSTANT {
return nil, fmt.Errorf("recipient constraint is not a magic constant")
}
iv, err := strconv.ParseInt(constraint.Constraint.GetFixedValue(), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse fixed value: %v", err)
}
magicConstantRecipientValue = rtypes.MagicConstant(iv)
}
if run.TxHash != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix goroutine capture of loop variable ‘run’

run = run is a no-op; the closure captures the reused range variable leading to wrong runs being processed.

Apply this diff:

-	for _, run := range runs {
-		run = run
-		eg.Go(func() error {
+	for _, run := range runs {
+		r := run
+		eg.Go(func() error {
+			run := r
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, run := range runs {
run = run
eg.Go(func() error {
//TODO also check failed runs
if run.Status != types.FeeRunStateDraft {
return nil
}
txs := []vtypes.PluginKeysignRequest{}
var magicConstantRecipientValue rtypes.MagicConstant = rtypes.MagicConstant_UNSPECIFIED
var token string
// This should only return one rule, but in case there are more/fewer rules, we'll loop through them all and error if it's the case.
for _, rule := range recipe.Rules {
// This section of code goes through the rules in the fee policy. It looks for the recipient of the fee collection policy and extracts it. If other data is found throws an error as they're unsupported rules.
var recipient string // The address specified in the fee policy.
switch rule.Resource {
case "ethereum.erc20.transfer":
for _, constraint := range rule.ParameterConstraints {
if constraint.ParameterName == "recipient" {
if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_MAGIC_CONSTANT {
return nil, fmt.Errorf("recipient constraint is not a magic constant")
}
iv, err := strconv.ParseInt(constraint.Constraint.GetFixedValue(), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse fixed value: %v", err)
}
magicConstantRecipientValue = rtypes.MagicConstant(iv)
}
if run.TxHash != nil {
// Before: for _, run := range runs {
// run = run
// eg.Go(func() error {
// After:
for _, run := range runs {
// capture the current iteration’s value
r := run
eg.Go(func() error {
// shadow run inside the closure so each goroutine sees its own copy
run := r
// TODO also check failed runs
if run.Status != types.FeeRunStateDraft {
return nil
}
if run.TxHash != nil {
// existing logic…
}
// …
})
}
🤖 Prompt for AI Agents
In plugin/fees/transaction.go around lines 58 to 66, the goroutine closure
captures the reused range variable `run` (the existing `run = run` is a no-op)
causing incorrect runs to be processed; fix by creating a local copy inside the
loop (e.g. `r := run`) and use that local `r` inside the eg.Go closure or
alternatively pass `run` as an argument to the closure so each goroutine gets
its own copy; remove the pointless `run = run` and update all references in the
closure to use the new local variable.

@garry-sharp garry-sharp force-pushed the fee-code-location-move branch from 09f218e to e7ef35f Compare September 1, 2025 03:15
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 (4)
plugin/fees/helper.go (3)

67-70: Fix potential panic when rawBytes is empty.

On empty input, rawBytes[0] in the error formatting will index out of range. Guard before indexing.

-	// Check transaction type (EIP-1559 is 0x02)
-	if len(rawBytes) == 0 || rawBytes[0] != 0x02 {
-		return nil, fmt.Errorf("unsupported transaction type: 0x%02x", rawBytes[0])
-	}
+	// Check transaction type (EIP-1559 is 0x02)
+	if len(rawBytes) == 0 {
+		return nil, fmt.Errorf("unsupported transaction type: empty payload")
+	}
+	if rawBytes[0] != 0x02 {
+		return nil, fmt.Errorf("unsupported transaction type: 0x%02x", rawBytes[0])
+	}

85-95: Add length checks before slicing and improve error wrapping.

tx.Data[:4] panics if len(tx.Data) < 4. Also wrap underlying errors for diagnostics.

-	// Get the method by selector
-	method, err := parsedABI.MethodById(tx.Data[:4])
-	if err != nil {
-		return nil, fmt.Errorf("unknown method ID")
-	}
+	// Get the method by selector
+	if len(tx.Data) < 4 {
+		return nil, fmt.Errorf("tx data too short for method selector: %d bytes", len(tx.Data))
+	}
+	method, err := parsedABI.MethodById(tx.Data[:4])
+	if err != nil {
+		return nil, fmt.Errorf("unknown method ID: %w", err)
+	}
@@
-	if err := method.Inputs.UnpackIntoMap(args, tx.Data[4:]); err != nil {
-		return nil, fmt.Errorf("failed get recipient and amount from tx")
-	}
+	if err := method.Inputs.UnpackIntoMap(args, tx.Data[4:]); err != nil {
+		return nil, fmt.Errorf("failed to unpack transfer args: %w", err)
+	}

107-111: Nil To-address check before dereference.

*tx.To will panic if tx.To == nil (e.g., malformed payload). Guard it.

-	return &erc20tx{
-		to:     recipient,
-		amount: amount,
-		token:  *tx.To,
-	}, nil
+	if tx.To == nil {
+		return nil, fmt.Errorf("nil token contract address in tx")
+	}
+	return &erc20tx{
+		to:     recipient,
+		amount: amount,
+		token:  *tx.To,
+	}, nil
plugin/fees/fees.go (1)

114-147: Validate the unsigned transaction payload, not the EIP-155 hash bytes.

Engine should evaluate the raw unsigned tx (hex) once per request. Prefer req.Transaction or fall back to Messages[0].RawMessage.

@@
-func (fp *FeePlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error {
-	// First validate the plugin policy itself
-	err := fp.ValidatePluginPolicy(policy)
-	if err != nil {
-		return fmt.Errorf("failed to validate plugin policy: %v", err)
-	}
+func (fp *FeePlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error {
+	// First validate the plugin policy itself
+	if err := fp.ValidatePluginPolicy(policy); err != nil {
+		return fmt.Errorf("failed to validate plugin policy: %w", err)
+	}
@@
-	recipe, err := policy.GetRecipe()
-	if err != nil {
-		return fmt.Errorf("failed to get recipe from policy: %v", err)
-	}
+	recipe, err := policy.GetRecipe()
+	if err != nil {
+		return fmt.Errorf("failed to get recipe from policy: %w", err)
+	}
@@
-	// Validate each proposed transaction
-	for _, tx := range txs {
-		for _, keysignMessage := range tx.Messages {
-			txBytes, err := base64.StdEncoding.DecodeString(keysignMessage.Message)
-			if err != nil {
-				return fmt.Errorf("failed to decode transaction: %w", err)
-			}
-
-			// Evaluate if the transaction is allowed by the policy
-			_, err = eng.Evaluate(recipe, vgcommon.Chain(keysignMessage.Chain), txBytes)
-			if err != nil {
-				return fmt.Errorf("failed to evaluate transaction: %w", err)
-			}
-		}
-	}
+	// Validate each proposed transaction request
+	for _, req := range txs {
+		// Prefer raw unsigned tx hex; fall back to the first message's RawMessage
+		var rawHex string
+		switch {
+		case req.Transaction != "":
+			rawHex = req.Transaction
+		case len(req.Messages) > 0 && req.Messages[0].RawMessage != "":
+			rawHex = req.Messages[0].RawMessage
+		default:
+			return fmt.Errorf("missing raw unsigned transaction in request")
+		}
+
+		rawBytes, err := hexutil.Decode(rawHex)
+		if err != nil {
+			return fmt.Errorf("failed to hex-decode raw tx: %w", err)
+		}
+
+		chain := vgcommon.Ethereum
+		if len(req.Messages) > 0 {
+			chain = vgcommon.Chain(req.Messages[0].Chain)
+		}
+		if _, err := eng.Evaluate(recipe, chain, rawBytes); err != nil {
+			return fmt.Errorf("failed to evaluate transaction: %w", err)
+		}
+	}
 	return nil
 }

Add import:

@@
-import (
-	"encoding/base64"
+import (
+	"encoding/base64"
+	"github.com/ethereum/go-ethereum/common/hexutil"
♻️ Duplicate comments (4)
plugin/fees/helper.go (1)

121-129: Stubs match the v2 run-based flow.

Keeping ProposeTransactions/SigningComplete as deprecated stubs aligns with the run-based architecture; good to keep for compatibility.

plugin/fees/transaction.go (3)

255-258: Resource must match USDC-only policy.

Policy enforces “ethereum.usdc.transfer”, not the generic ERC20 resource.

-	resourceName := "ethereum.erc20.transfer"
+	resourceName := "ethereum.usdc.transfer"

58-61: Fix goroutine loop variable capture; remove no-op assignment.

The closure captures the reused run variable; run = run does nothing. This causes incorrect runs to be processed.

-	for _, run := range runs {
-		run = run
-		eg.Go(func() error {
+	for _, run := range runs {
+		r := run
+		eg.Go(func() error {
+			run := r

225-232: Avoid hard-coded ChainId; use configured chain ID.

Hard-coding 1 breaks non-mainnet deployments.

-	var usdc *reth.Token = &reth.Token{
-		ChainId:  1,
+	var usdc *reth.Token = &reth.Token{
+		ChainId:  int(fp.config.ChainId.Int64()),
🧹 Nitpick comments (5)
plugin/fees/transaction.go (5)

55-99: Use errgroup.WithContext and drop the extra WaitGroup.

Let failures cancel sibling goroutines; the standalone WaitGroup is redundant with eg.Wait().

-	sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
-	var wg sync.WaitGroup
-	var eg errgroup.Group
+	sem := semaphore.NewWeighted(int64(fp.config.Jobs.Transact.MaxConcurrentJobs))
+	eg, gctx := errgroup.WithContext(ctx)
@@
-			wg.Add(1)
-			fp.logger.WithFields(logrus.Fields{"runId": run.ID, "policyId": run.PolicyID}).Info("Retrieved fee policy")
-
-			defer wg.Done()
-			if err := sem.Acquire(ctx, 1); err != nil {
+			fp.logger.WithFields(logrus.Fields{"runId": run.ID, "policyId": run.PolicyID}).Info("Retrieved fee policy")
+			if err := sem.Acquire(gctx, 1); err != nil {
 				return fmt.Errorf("failed to acquire semaphore: %w", err)
 			}
 			defer sem.Release(1)
-			if err := fp.executeFeeTransaction(ctx, run, *feePolicy); err != nil {
+			if err := fp.executeFeeTransaction(gctx, run, *feePolicy); err != nil {
 				fp.logger.WithError(err).WithFields(logrus.Fields{
 					"runId": run.ID,
 				}).Error("Failed to execute fee transaction")
 				return err
 			}
 			return nil
 		})
 	}
 
-	wg.Wait()
 	if err := eg.Wait(); err != nil {
 		return fmt.Errorf("failed to execute fee transaction: %w", err)
 	}

260-270: Prefer using the magic-constant getter over parsing strings.

If available in the generated types, use GetMagicConstant() to avoid brittle strconv.ParseInt on FixedValue.

-			iv, err := strconv.ParseInt(constraint.Constraint.GetFixedValue(), 10, 64)
-			if err != nil {
-				return nil, fmt.Errorf("failed to parse fixed value: %v", err)
-			}
-			magicConstantRecipientValue = rtypes.MagicConstant(iv)
+			// If supported by the generated API:
+			magicConstantRecipientValue = rtypes.MagicConstant(constraint.Constraint.GetMagicConstant())

If the getter is not available, keep existing logic.


309-327: Populate wrapper PolicyID for downstream JSON consumers.

PluginKeysignRequest also has a top-level PolicyID string used in JSON. Set it to avoid relying solely on the embedded field.

 	signRequest := vtypes.PluginKeysignRequest{
 		KeysignRequest: vtypes.KeysignRequest{
@@
 			PluginID:         policy.PluginID.String(),
 		},
 		Transaction: txHex,
+		PolicyID:    policy.ID, // or policy.ID.String() if type is uuid.UUID
 	}

Also consider setting TransactionType if consumers expect it.


183-187: Make decodeTx logging best-effort to avoid blocking broadcast.

A failure to decode for logging shouldn’t abort a successfully signed/broadcastable tx. Log the error and proceed.

-	erc20tx, err := decodeTx(req.Transaction)
-	if err != nil {
-		fp.logger.WithError(err).Error("failed to decode tx")
-		return fmt.Errorf("failed to decode tx: %w", err)
-	}
+	erc20tx, err := decodeTx(req.Transaction)
+	if err != nil {
+		fp.logger.WithError(err).Warn("failed to decode tx for logging; continuing")
+	}

203-206: Ensure idempotency if DB update fails after broadcast.

If SetFeeRunSent fails post-send, the run remains draft and may be re-broadcast. Consider persisting a “pending/broadcast” marker with raw tx or nonce before send, or reconciling by tx nonce/hash to prevent duplicates.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 09f218e and e7ef35f.

📒 Files selected for processing (5)
  • plugin/fees/config.go (6 hunks)
  • plugin/fees/fees.go (2 hunks)
  • plugin/fees/helper.go (3 hunks)
  • plugin/fees/load.go (1 hunks)
  • plugin/fees/transaction.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/fees/config.go
  • plugin/fees/load.go
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.
📚 Learning: 2025-07-31T15:43:42.631Z
Learnt from: garry-sharp
PR: vultisig/plugin#146
File: plugin/fees/transaction.go:158-160
Timestamp: 2025-07-31T15:43:42.631Z
Learning: In the vultisig/plugin fees plugin, the public ProposeTransactions method intentionally returns "not implemented" because the fees plugin v2 uses a run-based architecture instead of the standard plugin template pattern. The actual transaction proposal logic is implemented in the private proposeTransactions method that accepts a types.FeeRun parameter, allowing fees to be batched and tracked through fee runs rather than individual policy-based transactions.

Applied to files:

  • plugin/fees/helper.go
  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-07-24T19:34:47.441Z
Learnt from: webpiratt
PR: vultisig/plugin#141
File: plugin/payroll/transaction.go:208-267
Timestamp: 2025-07-24T19:34:47.441Z
Learning: In the vultisig/plugin codebase, duplicate transaction prevention for the payroll plugin has been moved to the verifier side, so the local `IsAlreadyProposed` check in plugin/payroll/transaction.go is no longer needed. The validation flow has changed and is now handled by the verifier rather than within the plugin itself.

Applied to files:

  • plugin/fees/helper.go
  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:47:47.927Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/policy.go:46-47
Timestamp: 2025-07-04T10:47:47.927Z
Learning: For the fee plugin in plugin/fees/policy.go, the resource validation and recipe specification are intentionally configured to accept only USDC transfers ("ethereum.usdc.transfer"), not general ERC20 transfers ("ethereum.erc20.transfer"), as fees are only collected in USDC.

Applied to files:

  • plugin/fees/fees.go
  • plugin/fees/transaction.go
📚 Learning: 2025-06-18T18:23:20.077Z
Learnt from: webpiratt
PR: vultisig/plugin#96
File: plugin/payroll/transaction.go:0-0
Timestamp: 2025-06-18T18:23:20.077Z
Learning: In the payroll plugin (plugin/payroll/transaction.go), signRequest.Transaction is stored with the "0x" prefix, making it compatible with gcommon.FromHex which requires 0x-prefixed hex strings.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-08-20T13:51:05.212Z
Learnt from: neavra
PR: vultisig/plugin#157
File: api/plugin.go:236-236
Timestamp: 2025-08-20T13:51:05.212Z
Learning: In the vultisig/plugin codebase migration to vultisig-go, SystemMigrations() and vultisig_validator functionality were not moved from the verifier package to vultisig-go. This means some imports of github.com/vultisig/verifier/common are intentionally retained for accessing these specific functions, while vault-related types and common functionality were migrated to vultisig-go.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-05-07T08:23:45.882Z
Learnt from: RaghavSood
PR: vultisig/plugin#36
File: api/server.go:21-33
Timestamp: 2025-05-07T08:23:45.882Z
Learning: The import path `github.com/vultisig/verifier/plugin` refers to an external dependency that provides the plugin interface, and should not be changed to `github.com/vultisig/plugin/plugin` as these are distinct packages with different purposes.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-08-20T13:51:41.822Z
Learnt from: neavra
PR: vultisig/plugin#157
File: api/server.go:22-24
Timestamp: 2025-08-20T13:51:41.822Z
Learning: In the vultisig-go migration, only vault-related functions (GetVaultBackupFilename, DecryptVaultFromBackup) were moved from github.com/vultisig/verifier/common to github.com/vultisig/vultisig-go/common. Other functions like SystemMigrations() remained in the verifier/common package, so files using these non-migrated functions should keep the original verifier/common import.

Applied to files:

  • plugin/fees/fees.go
📚 Learning: 2025-07-10T20:41:44.025Z
Learnt from: webpiratt
PR: vultisig/plugin#126
File: plugin/fees/policy.go:26-26
Timestamp: 2025-07-10T20:41:44.025Z
Learning: The constant `vtypes.PluginVultisigFees_feee` in the vultisig/plugin project is correctly spelled with "feee" (including the extra 'e'). This is the actual constant name defined in the external dependency github.com/vultisig/verifier/types and should not be changed to "fee" singular, as this is how it's defined in the external package.

Applied to files:

  • plugin/fees/transaction.go
📚 Learning: 2025-07-04T10:50:42.832Z
Learnt from: garry-sharp
PR: vultisig/plugin#117
File: plugin/fees/constraints.go:3-3
Timestamp: 2025-07-04T10:50:42.832Z
Learning: For the vultisig/plugin project, use "fee" (singular) throughout the codebase wherever possible, not "fees" (plural). This applies to all naming conventions including plugin types, variable names, function names, comments, and documentation.

Applied to files:

  • plugin/fees/transaction.go
🧬 Code graph analysis (3)
plugin/fees/helper.go (2)
plugin/fees/fees.go (1)
  • FeePlugin (33-48)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
plugin/fees/fees.go (1)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
plugin/fees/transaction.go (4)
plugin/fees/fees.go (1)
  • FeePlugin (33-48)
internal/types/fees.go (2)
  • FeeRunStateDraft (14-14)
  • FeeRun (29-39)
common/vault.go (1)
  • GetVaultFromPolicy (12-24)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
⏰ 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: build

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant