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

🚧 fees v2#146

Merged
garry-sharp merged 2 commits into
mainfrom
fees-v2
Aug 15, 2025
Merged

🚧 fees v2#146
garry-sharp merged 2 commits into
mainfrom
fees-v2

Conversation

@garry-sharp

@garry-sharp garry-sharp commented Jul 31, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Scheduled, automated fee workflows split into Load, Transact and Post stages with configurable cron schedules and queued tasks.
  • Improvements

    • Enhanced fee-run lifecycle (completed/failed), multi-fee runs, provider-based Ethereum support, transaction decoding/signing, and configurable confirmation checks.
    • DB now records transaction hashes and supports concurrent, idempotent processing.
  • Bug Fixes

    • More reliable status updates and verifier synchronization.
  • Chores

    • Configuration overhaul, new scheduling options and DB migrations.
  • Documentation

    • Added comprehensive FEES.md describing workflows and integration.

Copilot AI review requested due to automatic review settings July 31, 2025 14:35
@coderabbitai

coderabbitai Bot commented Jul 31, 2025

Copy link
Copy Markdown
Contributor

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

Splits the fees workflow into three scheduled stages (load, transact, post-tx); adds cron-driven enqueuers and Asynq handlers; refactors plugin into per-policy loading, per-run transacting and post‑tx verification with Ethereum helpers; replaces TxID UUID with TxHash string across types, DB, and migrations; adds verifier POST API for marking collected fees.

Changes

Cohort / File(s) Change Summary
Worker & Scheduling
cmd/fees/worker/main.go, etc/vultisig/fee.yml
Add startLoadingFees, startTransactingFees, startPostTx enqueuers; register handlers for fees:load, fees:transaction, fees:post_tx; add cron schedulers and job config entries (load/transact/post cron expressions, concurrency).
Plugin: core fees flow
plugin/fees/fees.go, plugin/fees/transaction.go, plugin/fees/post_tx.go, plugin/fees/helper.go, plugin/fees/constraints.go, plugin/fees/FEES.md
Replace single collection handler with LoadFees, HandleTransactions, HandlePostTx; add transactingMutex, ethClient; implement per-policy loading, per-run transaction flow, propose/sign/broadcast logic (moved into initSign/proposeTransactions), ERC‑20 decode/sign helpers; introduce three task constants and comprehensive FEES.md.
Plugin: config & options
plugin/fees/config.go
Replace RPC/collector fields with EthProvider; add nested Jobs (Load/Transact/Post) with Cronexpr and MaxConcurrentJobs; change chainId handling (unexported uint64 → exported *big.Int), add/adjust ConfigOption helpers and validation.
Verifier API & client
internal/verifierapi/verifierapi.go, internal/verifierapi/fees.go
Add postAuth helper for authenticated JSON POSTs; add VerifierApi.MarkFeeAsCollected(txHash, collectedAt, feeIds...) calling /fees/collected.
Types
internal/types/fees.go
Add FeeRunState values completed and failed; replace TxID *uuid.UUID with TxHash *string (db:"tx_hash"); add Fees []Fee to FeeRun.
Storage interface & implementation
storage/db.go, storage/postgres/fees.go, storage/postgres/policy.go
Rename GetAllPluginPoliciesGetPluginPolicies; add GetAllFeePolicies; make CreateFeeRun variadic fees; change SetFeeRunSent to accept txHash string; add SetFeeRunSuccess, GetAllFeeRuns, GetFees, GetPendingFeeRun, CreateFee, GetFeeRuns; implement transactional inserts, queries, and attach fees in Postgres backend.
Schema & migrations
storage/postgres/migrations/plugin/20250630152230_fee_runs.sql, storage/postgres/schema/schema.sql
Replace fee_run.tx_id UUID FK with tx_hash VARCHAR(66) (remove FK); update fee_run_with_totals view to use tx_hash; add index on fee_run(status).
Service layer
service/policy.go
PolicyService.GetPluginPolicies now calls db.GetPluginPolicies (DB method renamed).

Sequence Diagram(s)

sequenceDiagram
    participant Cron as CronScheduler
    participant AQ as AsynqClient
    participant W as Worker
    participant FP as FeePlugin
    participant DB as Database
    participant VA as VerifierAPI
    participant ETH as Ethereum

    Cron->>AQ: Enqueue fees:load
    AQ->>W: Deliver fees:load
    W->>FP: LoadFees()
    FP->>DB: GetAllFeePolicies / CreateFeeRun / CreateFee

    Cron->>AQ: Enqueue fees:transaction
    AQ->>W: Deliver fees:transaction
    W->>FP: HandleTransactions()
    FP->>DB: GetFeeRuns(state=Draft)
    FP->>ETH: proposeTransactions → sign & send
    FP->>DB: SetFeeRunSent(tx_hash)

    Cron->>AQ: Enqueue fees:post_tx
    AQ->>W: Deliver fees:post_tx
    W->>FP: HandlePostTx()
    FP->>DB: GetAllFeeRuns(status=Sent)
    FP->>ETH: GetTransactionReceipt(tx_hash)
    alt confirmed & success
        FP->>VA: MarkFeeAsCollected(tx_hash, time, feeIds)
        FP->>DB: SetFeeRunSuccess(runId)
    else failed / insufficient confirmations
        FP->>DB: (no change / mark failed)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit nudged the cron to sing,
Three hops — load, transact, then bring;
Hashes now whisper where UUIDs ran,
Jobs march in order, each with a plan;
Signed coins skip home — hooray, said the fan 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the "Integrations" page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fees-v2

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 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 implements version 2 of the fees plugin, restructuring the fee collection system from a single-step process to a multi-stage pipeline with improved transaction tracking and status management.

  • Replaces UUID-based transaction references with string-based transaction hashes
  • Introduces a three-phase job system: fee loading, transaction processing, and post-transaction status checking
  • Adds comprehensive fee run lifecycle management with proper state transitions

Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
storage/postgres/schema/schema.sql Updates fee_run table to use tx_hash instead of tx_id and removes foreign key constraint
storage/postgres/migrations/plugin/20250630152230_fee_runs.sql Migration reflecting the tx_hash schema change
storage/postgres/fees.go Adds new fee management methods and updates existing ones to use tx_hash
storage/postgres/policy.go Adds GetAllFeePolicies method and renames existing method
storage/db.go Updates interface to include new fee management methods
service/policy.go Updates method call to renamed function
plugin/fees/transaction.go Refactors transaction handling with proper hash generation and status updates
plugin/fees/post_tx.go New file implementing post-transaction status checking logic
plugin/fees/helper.go New utility functions for transaction decoding and hash generation
plugin/fees/fees.go Major restructure splitting fee collection into load/transaction phases
plugin/fees/constraints.go Updates task type constants for the new three-phase system
plugin/fees/config.go Restructures configuration for job-based processing
internal/verifierapi/verifierapi.go Adds POST request support for API calls
internal/verifierapi/fees.go Adds method to mark fees as collected
internal/types/fees.go Updates FeeRun type to use tx_hash and adds new states
etc/vultisig/fee.yml Updates configuration file structure
cmd/fees/worker/main.go Implements cron-based job scheduling for the three phases
Comments suppressed due to low confidence (1)

plugin/fees/config.go:19

  • [nitpick] The field name 'chainId' uses camelCase while 'ChainId' uses PascalCase. Consider using consistent naming convention, preferably 'chainID' and 'ChainID' to follow Go naming conventions for abbreviations.
	chainId       uint64 `mapstructure:"chain_id"`       // The chain ID of the Ethereum blockchain.

Comment thread plugin/fees/post_tx.go
Comment thread plugin/fees/fees.go Outdated
Comment thread plugin/fees/transaction.go
Comment thread plugin/fees/transaction.go
Comment thread plugin/fees/fees.go
Comment thread cmd/fees/worker/main.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🔭 Outside diff range comments (1)
storage/postgres/fees.go (1)

24-26: Use constant instead of hardcoded string for plugin ID

Consider importing and using the vtypes.PluginVultisigFees_feee constant instead of the hardcoded string to maintain consistency and avoid typos.

Import the types package at the top:

import vtypes "github.com/vultisig/verifier/types"

Then update the comparison:

-	if pluginId != "vultisig-fees-feee" {
-		return nil, errors.New("plugin id not found or not vultisig-fees-feee")
+	if pluginId != string(vtypes.PluginVultisigFees_feee) {
+		return nil, errors.New("plugin id not found or not " + string(vtypes.PluginVultisigFees_feee))
🧹 Nitpick comments (12)
etc/vultisig/fee.yml (1)

7-16: Excellent job scheduling configuration with appropriate timing.

The three-stage job configuration supports the new fee processing workflow effectively:

  • load (@every 2m): Frequent fee discovery
  • transact (0 12 * * 5): Weekly execution on Fridays at noon - appropriate for batch processing
  • post (@every 5m): Regular post-transaction status updates

The max_concurrent_jobs: 10 setting provides good concurrency control.

Fix the missing newline at the end of the file:

     post:
         cronexpr: "@every 5m"
-        max_concurrent_jobs: 10
+        max_concurrent_jobs: 10
+
plugin/fees/constraints.go (1)

6-8: Consider using singular "fee" for consistency.

Based on the established naming convention for this project, consider using "fee:" instead of "fees:" prefix for these constants to maintain consistency with the singular form used throughout the codebase.

-const TypeFeeLoad = "fees:load"            // Load list of pending fees into the db from the verifier
-const TypeFeeTransact = "fees:transaction" // Collect a list of loaded fees from the users wallet
-const TypeFeePostTx = "fees:post_tx"       // Check the status of the fee runs
+const TypeFeeLoad = "fee:load"            // Load list of pending fees into the db from the verifier
+const TypeFeeTransact = "fee:transaction" // Collect a list of loaded fees from the users wallet
+const TypeFeePostTx = "fee:post_tx"       // Check the status of the fee runs
plugin/fees/post_tx.go (3)

60-63: Remove redundant validation

These checks are already performed by the caller in HandlePostTx (lines 46-48), making them redundant here.

 func (fp *FeePlugin) updateStatus(ctx context.Context, run types.FeeRun, currentBlock uint64) error {
-	if run.TxHash == nil || run.Status == types.FeeRunStateDraft {
-		return nil
-	}
 	fp.logger.WithFields(logrus.Fields{"run_id": run.ID}).Info("Beginning status check/update")

77-80: Pre-allocate slice for better performance

Pre-allocating the slice with known capacity avoids multiple allocations during append operations.

-			ids := []uuid.UUID{}
+			ids := make([]uuid.UUID, 0, len(run.Fees))
 			for _, fee := range run.Fees {
 				ids = append(ids, fee.ID)
 			}

95-97: Implement failed transaction handling

The TODO comment indicates missing critical logic for handling failed transactions. This should be implemented to ensure proper state management.

Would you like me to help implement the failed transaction logic or create an issue to track this task?

plugin/fees/helper.go (2)

75-75: Move ABI definition to constants

The transfer ABI string should be defined as a package-level constant for better maintainability and reusability.

Add at the package level:

const erc20TransferABI = `[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]}]`

Then use it:

-	const transferABI = `[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]}]`
-	parsedABI, err := abi.JSON(strings.NewReader(transferABI))
+	parsedABI, err := abi.JSON(strings.NewReader(erc20TransferABI))

78-78: Improve error messages for better debugging

The error messages are too generic and don't provide enough context for debugging.

-		return nil, fmt.Errorf("failed to parse ABI")
+		return nil, fmt.Errorf("failed to parse ERC20 transfer ABI: %w", err)

-		return nil, fmt.Errorf("unknown method ID")
+		return nil, fmt.Errorf("unknown method ID: expected ERC20 transfer, got %x", tx.Data[:4])

-		return nil, fmt.Errorf("failed get recipient and amount from tx")
+		return nil, fmt.Errorf("failed to unpack recipient and amount from tx data: %w", err)

Also applies to: 84-84, 90-90

plugin/fees/fees.go (1)

191-202: Optimize fee existence check

Instead of retrieving all fee data just to check existence, consider adding a more efficient method like FeeExists(ctx, feeID) to the database interface.

This would improve performance by avoiding unnecessary data retrieval when you only need to check existence.

plugin/fees/transaction.go (1)

220-223: Add transaction state recovery mechanism

The comment indicates this is critical code where errors could cause state mismatches. Consider implementing a recovery mechanism or two-phase commit pattern.

Consider:

  1. Using database transactions to ensure atomicity
  2. Implementing a recovery mechanism to handle partial failures
  3. Adding monitoring/alerting for state mismatches
plugin/fees/config.go (2)

24-24: Fix comment accuracy

The comments mention "consecutive tasks" but the fields control concurrent tasks.

-			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` //How many consecutive tasks can take place
+			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` // Maximum number of concurrent tasks

Also applies to: 32-32


19-20: Consider consistent chain ID handling

Having both chainId (unexported) and ChainId (exported) fields could be confusing. Consider using only the exported field with custom unmarshaling.

Implement a custom UnmarshalJSON or use a setter method to handle the conversion directly during configuration loading.

Also applies to: 129-129

storage/postgres/fees.go (1)

176-194: Consider consolidating with GetAllFeeRuns

This method provides similar functionality to GetAllFeeRuns but without fetching associated fees. If the intent is performance optimization by not loading fees, this is fine. Otherwise, consider using GetAllFeeRuns with a single status parameter.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4d07390 and 49d4ad1.

📒 Files selected for processing (17)
  • cmd/fees/worker/main.go (3 hunks)
  • etc/vultisig/fee.yml (1 hunks)
  • internal/types/fees.go (2 hunks)
  • internal/verifierapi/fees.go (2 hunks)
  • internal/verifierapi/verifierapi.go (2 hunks)
  • plugin/fees/config.go (4 hunks)
  • plugin/fees/constraints.go (1 hunks)
  • plugin/fees/fees.go (8 hunks)
  • plugin/fees/helper.go (1 hunks)
  • plugin/fees/post_tx.go (1 hunks)
  • plugin/fees/transaction.go (6 hunks)
  • service/policy.go (1 hunks)
  • storage/db.go (1 hunks)
  • storage/postgres/fees.go (2 hunks)
  • storage/postgres/migrations/plugin/20250630152230_fee_runs.sql (2 hunks)
  • storage/postgres/policy.go (1 hunks)
  • storage/postgres/schema/schema.sql (2 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
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.
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.
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.
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: for the fee plugin in plugin/fees/policy.go, the resource validation and recipe specification are in...
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/config.go
  • storage/db.go
  • plugin/fees/fees.go
  • plugin/fees/constraints.go
  • plugin/fees/helper.go
  • cmd/fees/worker/main.go
  • storage/postgres/schema/schema.sql
  • internal/verifierapi/fees.go
  • etc/vultisig/fee.yml
  • storage/postgres/policy.go
  • plugin/fees/post_tx.go
  • internal/types/fees.go
  • plugin/fees/transaction.go
  • storage/postgres/fees.go
📚 Learning: in the vultisig/plugin codebase, duplicate transaction prevention for the payroll plugin has been mo...
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/config.go
  • storage/db.go
  • plugin/fees/fees.go
  • plugin/fees/constraints.go
  • plugin/fees/helper.go
  • cmd/fees/worker/main.go
  • internal/verifierapi/fees.go
  • etc/vultisig/fee.yml
  • storage/postgres/policy.go
  • plugin/fees/post_tx.go
  • plugin/fees/transaction.go
  • storage/postgres/fees.go
📚 Learning: the constant `vtypes.pluginvultisigfees_feee` in the vultisig/plugin project is correctly spelled wi...
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/config.go
  • storage/db.go
  • plugin/fees/fees.go
  • plugin/fees/constraints.go
  • plugin/fees/helper.go
  • internal/verifierapi/fees.go
  • etc/vultisig/fee.yml
  • storage/postgres/policy.go
  • internal/types/fees.go
  • plugin/fees/transaction.go
  • storage/postgres/fees.go
📚 Learning: for the vultisig/plugin project, use "fee" (singular) throughout the codebase wherever possible, not...
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/config.go
  • storage/db.go
  • plugin/fees/fees.go
  • plugin/fees/constraints.go
  • storage/postgres/policy.go
  • plugin/fees/transaction.go
  • storage/postgres/fees.go
📚 Learning: vaultserviceconfig is defined as a field of type vault_config.config from the external package "gith...
Learnt from: johnnyluo
PR: vultisig/plugin#108
File: cmd/payroll/worker/main.go:84-84
Timestamp: 2025-07-02T04:58:30.139Z
Learning: VaultServiceConfig is defined as a field of type vault_config.Config from the external package "github.com/vultisig/verifier/vault_config" in worker configuration structs across the vultisig/plugin codebase (cmd/payroll/worker/config.go, cmd/fees/worker/config.go, cmd/dca/worker/config.go). The vault_config.Config struct contains an EncryptionSecret field that can be accessed via cfg.VaultServiceConfig.EncryptionSecret.

Applied to files:

  • plugin/fees/config.go
  • plugin/fees/fees.go
📚 Learning: in the vultisig/plugin codebase, the hardcoded ethereumevmchainid = big.newint(1) in plugin/payroll/...
Learnt from: webpiratt
PR: vultisig/plugin#96
File: plugin/payroll/transaction.go:43-44
Timestamp: 2025-06-18T18:22:06.358Z
Learning: In the vultisig/plugin codebase, the hardcoded ethereumEvmChainID = big.NewInt(1) in plugin/payroll/transaction.go is intentional for the current implementation phase. The team is implementing ETH first, with plans to add other EVM chains later. The functions/methods are already designed to work with all EVM chains.

Applied to files:

  • plugin/fees/config.go
  • plugin/fees/fees.go
  • plugin/fees/constraints.go
  • plugin/fees/helper.go
  • etc/vultisig/fee.yml
📚 Learning: vaultserviceconfig is defined as a field of type vault_config.config in worker configuration structs...
Learnt from: johnnyluo
PR: vultisig/plugin#108
File: cmd/payroll/worker/main.go:84-84
Timestamp: 2025-07-02T04:58:30.139Z
Learning: VaultServiceConfig is defined as a field of type vault_config.Config in worker configuration structs across the vultisig/plugin codebase (cmd/payroll/worker/config.go, cmd/fees/worker/config.go, cmd/dca/worker/config.go). It contains an EncryptionSecret field that can be accessed via cfg.VaultServiceConfig.EncryptionSecret.

Applied to files:

  • plugin/fees/config.go
📚 Learning: the embed.fs directive `//go:embed migrations/plugin/*.sql` in storage/postgres/db_plugin.go correct...
Learnt from: RaghavSood
PR: vultisig/plugin#75
File: storage/postgres/db_plugin.go:25-40
Timestamp: 2025-05-30T02:44:31.711Z
Learning: The embed.FS directive `//go:embed migrations/plugin/*.sql` in storage/postgres/db_plugin.go correctly embeds plugin migration files that exist in the storage/postgres/migrations/plugin/ directory. The path "migrations/plugin" passed to goose.Up() references the embedded filesystem structure, not the physical file system path.

Applied to files:

  • storage/db.go
📚 Learning: in the payroll plugin (plugin/payroll/transaction.go), signrequest.transaction is stored with the "0...
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
  • plugin/fees/constraints.go
  • plugin/fees/helper.go
  • plugin/fees/transaction.go
📚 Learning: the import path `github.com/vultisig/verifier/plugin` refers to an external dependency that provides...
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
  • internal/verifierapi/verifierapi.go
  • plugin/fees/transaction.go
  • storage/postgres/fees.go
📚 Learning: the erc20abi constant is defined in plugin/payroll/constants.go within the payroll package, making i...
Learnt from: webpiratt
PR: vultisig/plugin#96
File: plugin/payroll/transaction.go:510-514
Timestamp: 2025-06-18T18:20:59.510Z
Learning: The erc20ABI constant is defined in plugin/payroll/constants.go within the payroll package, making it accessible to other files in the same package like transaction.go.

Applied to files:

  • plugin/fees/constraints.go
  • plugin/fees/helper.go
📚 Learning: in plugin/payroll/transaction.go, the gettokenid function is designed to return evm.zeroaddress.hex(...
Learnt from: webpiratt
PR: vultisig/plugin#125
File: plugin/payroll/transaction.go:186-197
Timestamp: 2025-07-09T22:22:36.651Z
Learning: In plugin/payroll/transaction.go, the getTokenID function is designed to return evm.ZeroAddress.Hex() when no "token" parameter constraint is found, rather than returning an error. This is expected behavior according to the team's design intent, even though the token parameter is marked as required in the recipe specification.

Applied to files:

  • plugin/fees/helper.go
  • plugin/fees/transaction.go
📚 Learning: in the `tx_indexer` table (storage/postgres/migrations/plugin/*_tx_indexer.sql), the `policy_id` col...
Learnt from: webpiratt
PR: vultisig/plugin#89
File: storage/postgres/migrations/plugin/20250603181247_tx_indexer.sql:13-13
Timestamp: 2025-06-11T18:42:54.241Z
Learning: In the `tx_indexer` table (storage/postgres/migrations/plugin/*_tx_indexer.sql), the `policy_id` column is intentionally **not** defined with a foreign-key constraint to `plugin_policies(id)`.

Applied to files:

  • storage/postgres/schema/schema.sql
  • storage/postgres/migrations/plugin/20250630152230_fee_runs.sql
📚 Learning: in internal/keysign/signer.go, the `continue` statement in the checkkeysigncomplete error handling (...
Learnt from: webpiratt
PR: vultisig/plugin#127
File: internal/keysign/signer.go:134-142
Timestamp: 2025-07-19T12:28:15.328Z
Learning: In internal/keysign/signer.go, the `continue` statement in the CheckKeysignComplete error handling (around line 140) is intentional. The code should attempt to check all messages in each polling iteration, even if some CheckKeysignComplete calls fail, before proceeding to the next polling cycle. This provides resilience for temporary failures on individual messages.

Applied to files:

  • plugin/fees/transaction.go
🧬 Code Graph Analysis (6)
storage/db.go (2)
internal/types/fees.go (3)
  • FeeRunState (11-11)
  • FeeRun (29-39)
  • Fee (21-26)
internal/verifierapi/fees.go (1)
  • FeeDto (12-21)
plugin/fees/fees.go (1)
internal/types/fees.go (2)
  • FeeRunStateDraft (14-14)
  • FeeRun (29-39)
internal/verifierapi/fees.go (1)
internal/verifierapi/verifierapi.go (1)
  • VerifierApi (26-31)
plugin/fees/post_tx.go (2)
plugin/fees/fees.go (1)
  • FeePlugin (36-51)
internal/types/fees.go (3)
  • FeeRunStateSent (15-15)
  • FeeRunStateDraft (14-14)
  • FeeRun (29-39)
plugin/fees/transaction.go (3)
plugin/fees/fees.go (1)
  • FeePlugin (36-51)
internal/types/fees.go (1)
  • FeeRun (29-39)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
storage/postgres/fees.go (3)
storage/postgres/db.go (1)
  • PostgresBackend (15-17)
internal/types/fees.go (5)
  • FeeRunState (11-11)
  • FeeRun (29-39)
  • FeeRunStateSent (15-15)
  • FeeRunStateSuccess (16-16)
  • Fee (21-26)
internal/verifierapi/fees.go (1)
  • FeeDto (12-21)
🪛 YAMLlint (1.37.1)
etc/vultisig/fee.yml

[error] 16-16: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (25)
internal/types/fees.go (3)

14-18: LGTM! Well-structured fee run state expansion.

The addition of FeeRunStateSuccess ("completed") and FeeRunStateFailed ("failed") states provides better granularity for tracking the fee run lifecycle beyond the existing "draft" and "sent" states.


34-34: Good architectural change from UUID to transaction hash.

The migration from TxID *uuid.UUID to TxHash *string aligns well with blockchain transaction identification patterns, where transaction hashes are the natural identifiers for on-chain transactions.


38-38: Excellent addition of direct fee association.

The new Fees []Fee field enables direct association of multiple fees with a fee run, supporting the refactored workflow where fees are managed in batches per run.

service/policy.go (1)

133-133: LGTM! Clean method name refactoring.

The change from GetAllPluginPolicies to GetPluginPolicies aligns with the database layer refactoring while maintaining the same functionality and signature.

etc/vultisig/fee.yml (1)

5-6: LGTM! Good configuration structure for Ethereum provider.

The eth_provider URL and chain_id: 1 configuration aligns with the architectural changes and supports Ethereum mainnet operations.

storage/postgres/migrations/plugin/20250630152230_fee_runs.sql (2)

9-9: Perfect column specification for transaction hashes.

The tx_hash VARCHAR(66) column specification is correctly sized for Ethereum transaction hashes (0x prefix + 64 hex characters) and appropriately removes the foreign key constraint since transaction hashes are external blockchain identifiers.


28-34: LGTM! View properly updated for new schema.

The fee_run_with_totals view is correctly updated to select and group by tx_hash instead of tx_id, maintaining consistency with the table structure changes.

internal/verifierapi/verifierapi.go (2)

4-5: LGTM! Appropriate imports for JSON POST functionality.

The new imports bytes and encoding/json are correctly added to support the JSON marshaling and HTTP POST request functionality.


55-70: Excellent implementation of authenticated POST method.

The postAuth method is well-implemented with:

  • Proper JSON marshaling with error handling
  • Correct HTTP request construction
  • Appropriate headers (Content-Type and Authorization)
  • Consistent pattern with existing getAuth method

This provides the necessary functionality for fee collection API interactions.

storage/postgres/schema/schema.sql (2)

74-74: LGTM! Schema change supports the new transaction hash workflow.

The change from tx_id UUID with foreign key constraint to tx_hash varchar(66) is appropriate for storing Ethereum transaction hashes and aligns with the new fee processing workflow.


84-90: LGTM! View correctly updated to reflect schema changes.

The fee_run_with_totals view has been properly updated to select and group by tx_hash instead of tx_id, maintaining consistency with the table schema changes.

internal/verifierapi/fees.go (1)

60-84: LGTM! Well-implemented API method.

The MarkFeeAsCollected method is properly implemented with:

  • Clear method signature using variadic parameters for fee IDs
  • Structured request body with appropriate field names
  • Proper error handling and HTTP status code validation
  • Correct resource cleanup with deferred response body closing
storage/postgres/policy.go (1)

41-71: LGTM! Database method correctly implemented.

The GetAllFeePolicies method is well-implemented with:

  • Correct hardcoded plugin_id 'vultisig-fees-feee' (matches external dependency spelling)
  • Proper use of DISTINCT ON(public_key) with ORDER BY to get the latest policy per public key
  • Consistent error handling and row scanning pattern
  • Good use of singular "Fee" in method name
cmd/fees/worker/main.go (3)

25-40: LGTM! Helper function properly implements fee loading.

The startLoadingFees function correctly:

  • Marshals the fee collection configuration into JSON payload
  • Uses appropriate task enqueueing settings (zero retries, timeouts, retention)
  • Includes proper error handling for marshaling failures

176-178: LGTM! Task handlers correctly registered.

The mux handlers are properly registered with the new task type constants and corresponding fee plugin methods, supporting the 3-stage fee processing workflow.


180-199: Well-implemented cron scheduling system.

The three separate cron schedulers are properly configured with:

  • Configuration-driven cron expressions from feePluginConfig.Jobs
  • Appropriate helper function calls for each stage
  • Clear comments explaining the purpose of each scheduler
plugin/fees/transaction.go (1)

187-193: Good error handling pattern

Excellent use of errors.Join to aggregate multiple decoding errors while maintaining individual error context.

plugin/fees/config.go (1)

52-54: Verify default cron expressions align with business requirements

The default cron expressions show significant differences:

  • Load: Every 2 minutes
  • Transact: Weekly on Fridays at noon
  • Post: Every 5 minutes

The transact job running only once per week seems unusual for a fee collection system.

Please confirm these default schedules align with business requirements, particularly the weekly transaction schedule.

storage/postgres/fees.go (5)

60-66: LGTM!

The method correctly updates the fee run status and transaction hash. Good use of the status constant.


68-74: LGTM!

Clean implementation for updating fee run status to success.


115-120: Verify map value modification pattern

The code modifies a copy of the struct from the map and then reassigns it. While this works correctly in Go, it's a common source of confusion.

The current implementation is correct, but consider using pointers in the map for clarity:

rm := make(map[uuid.UUID]*types.FeeRun)

130-148: LGTM!

Clean implementation for fetching fees by IDs. Good use of ANY clause for efficient batch queries.


168-174: LGTM!

Simple and correct implementation for creating individual fees.

storage/db.go (2)

19-20: LGTM - Method rename improves clarity

The rename from GetAllPluginPolicies to GetPluginPolicies better reflects that the method can filter results, not just return all policies.


25-32: Well-designed fee management interface

The new fee-related methods provide a comprehensive API for fee run lifecycle management:

  • Creation with variadic fees parameter for flexibility
  • Status updates (sent, success)
  • Multiple query methods for different use cases
  • Proper use of transaction hash instead of UUID

The interface follows good naming conventions using "fee" singular as per project guidelines.

Comment thread cmd/fees/worker/main.go
Comment thread plugin/fees/fees.go
Comment thread plugin/fees/fees.go Outdated
Comment thread plugin/fees/fees.go Outdated
Comment thread plugin/fees/helper.go Outdated
Comment thread plugin/fees/post_tx.go
Comment thread plugin/fees/post_tx.go Outdated
Comment thread plugin/fees/transaction.go
Comment thread storage/postgres/fees.go Outdated
Comment thread storage/postgres/fees.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

151-154: Intentional "not implemented" for run-based architecture

As documented in the learnings, the public ProposeTransactions method returning "not implemented" is intentional since the fees plugin v2 uses a run-based architecture with the private proposeTransactions method.

storage/postgres/fees.go (1)

156-157: Consider using FeeRunStateDraft constant

For consistency with other status references in the codebase, consider using the constant instead of hardcoding the status value.

-	query := `select id, status, created_at, updated_at, tx_hash, policy_id, total_amount, fee_count from fee_run_with_totals where status = $1 and policy_id = $2 order by created_at desc limit 1`
-	rows, err := p.pool.Query(ctx, query, types.FeeRunStateDraft, policyId)
+	query := `select id, status, created_at, updated_at, tx_hash, policy_id, total_amount, fee_count from fee_run_with_totals where status = $1 and policy_id = $2 order by created_at desc limit 1`
+	rows, err := p.pool.Query(ctx, query, types.FeeRunStateDraft, policyId)
plugin/fees/fees.go (4)

69-69: Reuse the existing Ethereum client

The rpcClient is already created and can be reused instead of storing it separately as ethClient.

-	rpcClient, err := ethclient.Dial(feeConfig.EthProvider)
 	// ... other code ...
-		ethClient:        rpcClient,
+		ethClient:        rpcClient,  // Already created above

Also applies to: 108-108


138-138: Use the provided context for semaphore acquisition

Using context.Background() ignores cancellation signals from the parent context, which could lead to goroutines continuing to run after the parent context is cancelled.

-			if err := sem.Acquire(ctx, 1); err != nil {
+			if err := sem.Acquire(ctx, 1); err != nil {

219-221: Add error handling for CreateFee

The error from CreateFee should be handled to avoid silent failures when adding fees to existing runs.

-				if err := fp.db.CreateFee(ctx, run.ID, fee); err != nil {
-					return fmt.Errorf("failed to create fee: %w", err)
-				}
+				if err := fp.db.CreateFee(ctx, run.ID, fee); err != nil {
+					return fmt.Errorf("failed to add fee to fee run: %w", err)
+				}

284-284: Use the provided context for semaphore acquisition

Similar to the previous issue, use the provided context instead of context.Background().

-			if err := sem.Acquire(ctx, 1); err != nil {
+			if err := sem.Acquire(ctx, 1); err != nil {
🧹 Nitpick comments (5)
etc/vultisig/fee.yml (1)

16-16: Add missing newline at end of file

YAML files should end with a newline character.

     post:
         cronexpr: "@every 5m"
         max_concurrent_jobs: 10
+
internal/types/fees.go (1)

34-38: Consider documenting the relationship between TotalAmount and Fees

The FeeRun struct has both a TotalAmount field and a Fees slice. It would be helpful to document whether TotalAmount is derived from summing the Fees amounts or if it can differ.

Add a comment to clarify the relationship:

 type FeeRun struct {
 	ID          uuid.UUID   `db:"id"`
 	Status      FeeRunState `db:"status"`
 	CreatedAt   time.Time   `db:"created_at"`
 	UpdatedAt   time.Time   `db:"updated_at"`
 	TxHash      *string     `db:"tx_hash"`
 	PolicyID    uuid.UUID   `db:"policy_id"`
+	// TotalAmount is the sum of all Fee.Amount values in Fees slice
 	TotalAmount int         `db:"total_amount"`
 	FeeCount    int         `db:"fee_count"`
 	Fees        []Fee       `db:"fees"`
 }
plugin/fees/config.go (3)

19-21: Consider making chainId field consistent with naming convention.

The struct has both chainId (unexported, mapstructure tag) and ChainId (exported) fields for the same data. While this works functionally, it creates potential confusion about which field to use.

Consider renaming the internal field to follow Go conventions (e.g., internalChainId or rawChainId) to make the distinction clearer.

-	chainId       uint64 `mapstructure:"chain_id"`       // The chain ID of the Ethereum blockchain.
+	rawChainId    uint64 `mapstructure:"chain_id"`       // The chain ID of the Ethereum blockchain.

And update line 129 accordingly:

-	c.ChainId = big.NewInt(0).SetUint64(c.chainId)
+	c.ChainId = big.NewInt(0).SetUint64(c.rawChainId)

24-24: Fix comment typo: "consecutive" should be "concurrent".

The comment mentions "consecutive tasks" but based on the field name MaxConcurrentJobs, it should be "concurrent tasks".

-			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` //How many consecutive tasks can take place
+			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` //How many concurrent tasks can take place

Apply the same fix to lines 28 and 34.

Also applies to: 28-28, 34-34


32-32: Misleading comment for SuccessConfirmations field.

The comment "How many consecutive tasks can take place" doesn't match the field name SuccessConfirmations, which typically refers to the number of block confirmations needed before considering a transaction successful.

-			SuccessConfirmations uint64 `mapstructure:"success_confirmations"` //How many consecutive tasks can take place
+			SuccessConfirmations uint64 `mapstructure:"success_confirmations"` //How many block confirmations required for transaction success
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 49d4ad1 and 6f000ec.

📒 Files selected for processing (17)
  • cmd/fees/worker/main.go (3 hunks)
  • etc/vultisig/fee.yml (1 hunks)
  • internal/types/fees.go (2 hunks)
  • internal/verifierapi/fees.go (2 hunks)
  • internal/verifierapi/verifierapi.go (2 hunks)
  • plugin/fees/config.go (4 hunks)
  • plugin/fees/constraints.go (1 hunks)
  • plugin/fees/fees.go (7 hunks)
  • plugin/fees/helper.go (1 hunks)
  • plugin/fees/post_tx.go (1 hunks)
  • plugin/fees/transaction.go (6 hunks)
  • service/policy.go (1 hunks)
  • storage/db.go (1 hunks)
  • storage/postgres/fees.go (2 hunks)
  • storage/postgres/migrations/plugin/20250630152230_fee_runs.sql (2 hunks)
  • storage/postgres/policy.go (1 hunks)
  • storage/postgres/schema/schema.sql (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • service/policy.go
  • plugin/fees/helper.go
  • storage/postgres/schema/schema.sql
  • plugin/fees/post_tx.go
  • internal/verifierapi/verifierapi.go
  • storage/postgres/policy.go
  • internal/verifierapi/fees.go
  • plugin/fees/constraints.go
  • cmd/fees/worker/main.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-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/config.go
  • storage/postgres/fees.go
  • plugin/fees/transaction.go
  • plugin/fees/fees.go
  • etc/vultisig/fee.yml
📚 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:

  • storage/postgres/fees.go
  • plugin/fees/transaction.go
  • plugin/fees/fees.go
📚 Learning: 2025-06-11T18:42:54.241Z
Learnt from: webpiratt
PR: vultisig/plugin#89
File: storage/postgres/migrations/plugin/20250603181247_tx_indexer.sql:13-13
Timestamp: 2025-06-11T18:42:54.241Z
Learning: In the `tx_indexer` table (storage/postgres/migrations/plugin/*_tx_indexer.sql), the `policy_id` column is intentionally **not** defined with a foreign-key constraint to `plugin_policies(id)`.

Applied to files:

  • storage/postgres/migrations/plugin/20250630152230_fee_runs.sql
📚 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
📚 Learning: 2025-07-19T12:28:15.328Z
Learnt from: webpiratt
PR: vultisig/plugin#127
File: internal/keysign/signer.go:134-142
Timestamp: 2025-07-19T12:28:15.328Z
Learning: In internal/keysign/signer.go, the `continue` statement in the CheckKeysignComplete error handling (around line 140) is intentional. The code should attempt to check all messages in each polling iteration, even if some CheckKeysignComplete calls fail, before proceeding to the next polling cycle. This provides resilience for temporary failures on individual messages.

Applied to files:

  • plugin/fees/transaction.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-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/transaction.go
🧬 Code Graph Analysis (4)
storage/db.go (2)
internal/types/fees.go (3)
  • FeeRunState (11-11)
  • FeeRun (29-39)
  • Fee (21-26)
internal/verifierapi/fees.go (1)
  • FeeDto (12-21)
storage/postgres/fees.go (3)
storage/postgres/db.go (1)
  • PostgresBackend (15-17)
internal/types/fees.go (6)
  • FeeRunState (11-11)
  • FeeRun (29-39)
  • FeeRunStateSent (15-15)
  • FeeRunStateSuccess (16-16)
  • Fee (21-26)
  • FeeRunStateDraft (14-14)
internal/verifierapi/fees.go (1)
  • FeeDto (12-21)
plugin/fees/transaction.go (3)
plugin/fees/fees.go (1)
  • FeePlugin (36-51)
internal/types/fees.go (1)
  • FeeRun (29-39)
internal/types/keysign.go (1)
  • PluginKeysignRequest (7-12)
plugin/fees/fees.go (1)
internal/types/fees.go (2)
  • FeeRunStateDraft (14-14)
  • FeeRun (29-39)
🪛 YAMLlint (1.37.1)
etc/vultisig/fee.yml

[error] 16-16: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (16)
storage/postgres/migrations/plugin/20250630152230_fee_runs.sql (2)

9-9: LGTM! Appropriate column type for Ethereum transaction hashes

Using VARCHAR(66) for tx_hash is correct for storing Ethereum transaction hashes with the '0x' prefix (2 chars) + 64 hex characters.


37-37: Index on status column will improve query performance

Good addition of the index on fee_run(status) as it will speed up queries filtering by status, which appears to be a common operation based on the new methods like GetFeeRuns and GetAllFeeRuns.

internal/types/fees.go (1)

14-17: Well-structured state machine for fee runs

The fee run states follow a clear progression: draft → sent → completed/failed. This provides good visibility into the transaction lifecycle.

storage/db.go (2)

19-20: Clean API separation for fee policies

The separation of GetPluginPolicies (filtered by plugin type) and GetAllFeePolicies (specifically for fees) provides a clear API surface.


25-32: Comprehensive fee run management API

The new methods provide a complete API for the fee run lifecycle - from creation through transaction sending to completion. The variadic parameters in CreateFeeRun and filtering options in GetAllFeeRuns are well-designed.

plugin/fees/transaction.go (1)

189-192: Robust transaction hash calculation

The transaction hash calculation using getHash properly handles the transaction bytes and signature components to derive the final hash before broadcasting.

storage/postgres/fees.go (1)

108-109: Efficient fee filtering by run IDs

Good optimization to filter fees by the relevant run IDs instead of fetching all fees from the database.

plugin/fees/fees.go (1)

288-293: Error from executeFeeTransaction is properly handled

The error from executeFeeTransaction is correctly logged and returned to the errgroup, ensuring failures are propagated.

plugin/fees/config.go (8)

22-36: Well-structured job configuration design.

The nested Jobs structure with Load, Transact, and Post stages provides clear separation of concerns and allows for fine-grained control over each stage's concurrency and scheduling. The mapstructure tags are properly configured for configuration unmarshalling.


47-50: Good default values for job concurrency and confirmations.

The default values are reasonable:

  • 10 concurrent jobs for each stage provides good parallelism without overwhelming the system
  • 20 success confirmations is a conservative choice for Ethereum transaction finality

85-92: LGTM: Clean helper function for job concurrency configuration.

The WithJobConcurrency function provides a convenient way to set all three job concurrency limits in one call, which is useful for testing and configuration.


94-101: LGTM: Consistent helper function for cron expressions.

The WithCronexpr function follows the same pattern as WithJobConcurrency, providing a clean way to configure all three job schedules simultaneously.


129-129: *LGTM: Proper conversion from uint64 to big.Int.

The conversion from the internal chainId field to the exported ChainId field is handled correctly, ensuring the big.Int is properly initialized.


156-158: LGTM: Essential validation for Ethereum provider.

The validation ensures EthProvider is configured, which is critical for the fees plugin to interact with the Ethereum blockchain.


160-167: LGTM: Reasonable bounds for concurrent job limits.

The validation ensures all job types have concurrency between 1 and 100, which prevents both misconfiguration (0 jobs) and potential resource exhaustion (too many concurrent jobs).


52-54: Cron expression confirmed as intentional

The 0 12 * * 5 schedule in plugin/fees/config.go is explicitly documented in the codebase, indicating it was an intentional choice:

  • cmd/fees/worker/main.go: // Transact fees every Friday at 12:00 PM

No changes are required.

Comment thread etc/vultisig/fee.yml Outdated
Comment on lines +215 to 217
if err := fp.db.SetFeeRunSent(ctx, runId, tx.Hash().Hex()); err != nil { //TODO pass the real tx id
return fmt.Errorf("failed to set fee run sent: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Ensure fee run status is updated after broadcast

The comment correctly highlights the importance of this operation. If SetFeeRunSent fails after the transaction is broadcast, the fee run will remain in draft status despite the transaction being on-chain, causing potential double-spending issues.

Consider wrapping this in a retry mechanism or implementing a reconciliation process:

-	if err := fp.db.SetFeeRunSent(ctx, runId, tx.Hash().Hex()); err != nil { //TODO pass the real tx id
-		return fmt.Errorf("failed to set fee run sent: %w", err)
+	// Retry up to 3 times to ensure the status is updated
+	var updateErr error
+	for i := 0; i < 3; i++ {
+		if updateErr = fp.db.SetFeeRunSent(ctx, runId, tx.Hash().Hex()); updateErr == nil {
+			break
+		}
+		fp.logger.WithError(updateErr).WithField("attempt", i+1).Warn("Failed to update fee run status, retrying...")
+	}
+	if updateErr != nil {
+		// Log critical error - transaction was sent but status not updated
+		fp.logger.WithFields(logrus.Fields{
+			"runId": runId,
+			"txHash": tx.Hash().Hex(),
+			"error": updateErr,
+		}).Error("CRITICAL: Transaction broadcast but fee run status not updated")
+		return fmt.Errorf("failed to set fee run sent after transaction broadcast: %w", updateErr)
 	}
📝 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
if err := fp.db.SetFeeRunSent(ctx, runId, tx.Hash().Hex()); err != nil { //TODO pass the real tx id
return fmt.Errorf("failed to set fee run sent: %w", err)
}
// Retry up to 3 times to ensure the status is updated
var updateErr error
for i := 0; i < 3; i++ {
if updateErr = fp.db.SetFeeRunSent(ctx, runId, tx.Hash().Hex()); updateErr == nil {
break
}
fp.logger.WithError(updateErr).WithField("attempt", i+1).Warn("Failed to update fee run status, retrying...")
}
if updateErr != nil {
// Log critical error - transaction was sent but status not updated
fp.logger.WithFields(logrus.Fields{
"runId": runId,
"txHash": tx.Hash().Hex(),
"error": updateErr,
}).Error("CRITICAL: Transaction broadcast but fee run status not updated")
return fmt.Errorf("failed to set fee run sent after transaction broadcast: %w", updateErr)
}
🤖 Prompt for AI Agents
In plugin/fees/transaction.go around lines 215 to 217, the call to
fp.db.SetFeeRunSent after broadcasting the tx can fail and leave the fee run in
draft; wrap this operation in a retry-with-backoff loop (e.g., 3-5 attempts with
exponential backoff), log each failure with context (runId and tx.Hash().Hex()),
and on final failure enqueue a durable reconciliation record (or persistent
retry queue) containing runId and tx hash so a background reconciler can mark
the run sent or alert operators; also replace the TODO by ensuring the real
transaction id is used when available and emit a metric/event on success and on
terminal failure for monitoring.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
etc/vultisig/fee.yml (1)

8-9: Good catch: removed hardcoded provider API key

Replacing the hardcoded Alchemy URL with an environment variable resolves the earlier secret exposure concern.

🧹 Nitpick comments (1)
etc/vultisig/fee.yml (1)

4-4: Optional: externalize verifier_token as a secret

If this token is used outside of local dev, consider moving it to an env var (e.g., ${VERIFIER_TOKEN}) to avoid accidental exposure in different environments.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6f000ec and c6c620f.

📒 Files selected for processing (1)
  • etc/vultisig/fee.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 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-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:

  • etc/vultisig/fee.yml
🪛 YAMLlint (1.37.1)
etc/vultisig/fee.yml

[error] 7-7: syntax error: mapping values are not allowed here

(syntax)

🔇 Additional comments (1)
etc/vultisig/fee.yml (1)

10-20: Verify environment variable expansion and cron semantics

  • In plugin/fees/config.go, the mapstructure tags for eth_provider, chain_id, jobs, cronexpr, and max_concurrent_jobs correctly match the keys in your YAML.
  • Viper is set up with AutomaticEnv and the FEES_ prefix, so FEES_ETH_PROVIDER will override the file. It will not perform inline expansion of ${ETH_PROVIDER_URL} in the YAML value—if you need that, apply os.ExpandEnv (or equivalent) to EthProvider after unmarshalling.
  • I didn’t locate the scheduler code in plugin/fees; please verify which scheduler (e.g., asynq’s Scheduler or robfig/cron) is used for cronexpr and ensure it supports both @every … and standard 5-field cron syntax, as well as the desired timezone semantics.

Comment thread etc/vultisig/fee.yml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (6)
etc/vultisig/fee.yml (1)

5-20: ⚠️ Merge conflict markers committed into configuration file

The configuration file contains git merge conflict markers (lines 5-9 starting with ---, +++, @@) which break the YAML structure and will cause parsing failures at runtime.

Apply this diff to fix the file:

---- etc/vultisig/fee.yml
-+++ etc/vultisig/fee.yml
-@@ Lines: 5-5
--eth_provider: https://eth-mainnet.g.alchemy.com/v2/HAtIwB5y82TNVwaHkwcMj
-+eth_provider: ${ETH_PROVIDER_URL}
+eth_provider: ${ETH_PROVIDER_URL}
 chain_id: 1
 jobs:
-    load:
-        cronexpr: "@every 2m"
-        max_concurrent_jobs: 10
-    transact:
-        cronexpr: "0 12 * * 5"
-        max_concurrent_jobs: 10
-    post:
-        cronexpr: "@every 5m"
-        max_concurrent_jobs: 10
+  load:
+    cronexpr: "@every 2m"
+    max_concurrent_jobs: 10
+  transact:
+    cronexpr: "0 12 * * 5"
+    max_concurrent_jobs: 10
+  post:
+    cronexpr: "@every 5m"
+    max_concurrent_jobs: 10
cmd/fees/worker/main.go (1)

25-40: Inconsistent payload handling between fee tasks

The LoadFees handler in plugin/fees/fees.go completely ignores the task payload, yet startLoadingFees marshals a structured FeeCollectionFormat payload. This creates unnecessary complexity and confusion.

Since the payload is ignored, simplify by using an empty payload like the other tasks:

 func startLoadingFees(asynqClient *asynq.Client, logger *logrus.Logger) {
 	logger.Info("Loading fees")
-	payload, err := json.Marshal(fees.FeeCollectionFormat{
-		FeeCollectionType: fees.FeeCollectionTypeAll,
-	})
-	if err != nil {
-		logger.WithError(err).Error("Failed to marshal fee loading config in demo run")
-		return
-	}
+	payload := []byte{}
 	asynqClient.Enqueue(
 		asynq.NewTask(fees.TypeFeeLoad, payload),
plugin/fees/fees.go (4)

69-72: Remove duplicate Ethereum client initialization

The Ethereum client is created as rpcClient at line 69 and stored in ethClient at line 108. This is the same client being used twice.

Since both fields use the same client, consider removing the duplicate field or clearly documenting why both are needed if there's a specific reason.

Also applies to: 108-108


138-140: Use the provided context for semaphore acquisition

Using context.Background() instead of the provided ctx means cancellation signals won't be properly propagated, potentially causing goroutines to continue running after the parent context is cancelled.

-			if err := sem.Acquire(context.Background(), 1); err != nil {
+			if err := sem.Acquire(ctx, 1); err != nil {

219-221: Add error handling for CreateFee

The error returned by CreateFee should be handled to avoid silent failures when adding fees to existing runs.

The error is already being handled correctly in the current code. The method properly returns the error if CreateFee fails.


284-286: Use the provided context for semaphore acquisition

Similar to the issue in LoadFees, using context.Background() prevents proper cancellation propagation.

-			if err := sem.Acquire(context.Background(), 1); err != nil {
+			if err := sem.Acquire(ctx, 1); err != nil {
🧹 Nitpick comments (8)
cmd/fees/worker/main.go (3)

180-186: Inconsistent comment about scheduling frequency

The comment states "Load fees every 10 minutes" but the actual cron expression is configured in the YAML file as "@every 2m" (every 2 minutes).

Update the comment to be more generic:

-	// Load fees every 10 minutes
+	// Load fees based on configured schedule

187-193: Update comment to be configuration-aware

The comment states "Transact fees every Friday at 12:00 PM" but the actual schedule is determined by the configuration.

-	// Transact fees every Friday at 12:00 PM
+	// Transact fees based on configured schedule

194-199: Fix misleading comment

The comment states "Update verifier" but this actually handles post-transaction processing.

-	// // Update verifier every 10 minutes
+	// Post-transaction processing based on configured schedule
storage/postgres/fees.go (2)

15-58: Consider using a database transaction for the entire CreateFeeRun operation

The method begins a transaction at line 28 but performs the policy validation query outside of it (lines 17-26). If another process deletes or modifies the policy between the validation and the transaction, it could lead to inconsistent state.

Move the policy validation inside the transaction:

 func (p *PostgresBackend) CreateFeeRun(ctx context.Context, policyId uuid.UUID, state types.FeeRunState, fees ...verifierapi.FeeDto) (*types.FeeRun, error) {
-	// Check policy id is valid
-	query := `select plugin_id from plugin_policies where id = $1`
-	policyrows := p.pool.QueryRow(ctx, query, policyId)
-	var pluginId string
-	err := policyrows.Scan(&pluginId)
-	if err != nil {
-		return nil, err
-	}
-	if pluginId != "vultisig-fees-feee" {
-		return nil, errors.New("plugin id not found or not vultisig-fees-feee")
-	}
-
 	tx, err := p.pool.Begin(ctx)
 	if err != nil {
 		return nil, err
 	}
 	defer tx.Rollback(ctx)
+	
+	// Check policy id is valid within the transaction
+	query := `select plugin_id from plugin_policies where id = $1`
+	var pluginId string
+	err = tx.QueryRow(ctx, query, policyId).Scan(&pluginId)
+	if err != nil {
+		return nil, err
+	}
+	if pluginId != "vultisig-fees-feee" {
+		return nil, errors.New("plugin id not found or not vultisig-fees-feee")
+	}
+	
 	runId := uuid.New()

76-133: Consider optimizing fee fetching for large datasets

When fetching fee runs with many associated fees, the current implementation makes N+1 queries (one for runs, one for all fees). While the fee query is now properly filtered by run IDs, consider using a JOIN query for better performance with large datasets.

Consider using a single query with JSON aggregation:

SELECT 
  fr.id, fr.status, fr.created_at, fr.updated_at, fr.tx_hash, 
  fr.policy_id, fr.total_amount, fr.fee_count,
  COALESCE(json_agg(
    json_build_object(
      'id', f.id,
      'fee_run_id', f.fee_run_id,
      'amount', f.amount
    ) ORDER BY f.created_at
  ) FILTER (WHERE f.id IS NOT NULL), '[]') as fees
FROM fee_run_with_totals fr
LEFT JOIN fee f ON f.fee_run_id = fr.id
WHERE fr.status = ANY($1)
GROUP BY fr.id, fr.status, fr.created_at, fr.updated_at, 
         fr.tx_hash, fr.policy_id, fr.total_amount, fr.fee_count
plugin/fees/config.go (3)

22-36: Fix comment inconsistencies in Jobs struct field documentation.

Several comments contain inaccuracies:

  • Line 24, 28: "consecutive tasks" should be "concurrent tasks"
  • Line 25, 29: "Cron link expression" should be "Cron expression"
  • Line 32: Comment describes "How many consecutive tasks" but field name is SuccessConfirmations which represents confirmation count, not task count

Apply this diff to fix the comment inconsistencies:

-			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` //How many consecutive tasks can take place
-			Cronexpr          string `mapstructure:"cronexpr"`            // Cron link expression on how often these tasks should run
+			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` // How many concurrent tasks can take place
+			Cronexpr          string `mapstructure:"cronexpr"`            // Cron expression on how often these tasks should run
-			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` //How many consecutive tasks can take place
-			Cronexpr          string `mapstructure:"cronexpr"`            // Cron link expression on how often these tasks should run
+			MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` // How many concurrent tasks can take place
+			Cronexpr          string `mapstructure:"cronexpr"`            // Cron expression on how often these tasks should run
-			SuccessConfirmations uint64 `mapstructure:"success_confirmations"` //How many consecutive tasks can take place
-			Cronexpr             string `mapstructure:"cronexpr"`              // Cron link expression on how often these tasks should run
+			SuccessConfirmations uint64 `mapstructure:"success_confirmations"` // Number of confirmations required for transaction success
+			Cronexpr             string `mapstructure:"cronexpr"`              // Cron expression on how often these tasks should run

19-21: Consider making chainId field private and adding validation.

The dual field approach (unexported chainId and exported ChainId) is reasonable for unmarshalling, but consider adding validation to ensure the conversion is safe and prevent integer overflow scenarios.

Consider adding validation in the WithFileConfig function:

+		// Validate chainId before conversion to prevent overflow
+		if c.chainId == 0 {
+			return errors.New("chain_id must be greater than 0")
+		}
+
 		c.ChainId = big.NewInt(0).SetUint64(c.chainId)

71-76: Consider renaming function to match the field it configures.

The function WithEthClient sets the EthProvider field, which creates a semantic mismatch. Consider renaming for clarity.

Apply this diff to rename the function:

-func WithEthClient(url string) ConfigOption {
+func WithEthProvider(url string) ConfigOption {
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c6c620f and 4c5d21d.

📒 Files selected for processing (17)
  • cmd/fees/worker/main.go (3 hunks)
  • etc/vultisig/fee.yml (1 hunks)
  • internal/types/fees.go (2 hunks)
  • internal/verifierapi/fees.go (2 hunks)
  • internal/verifierapi/verifierapi.go (2 hunks)
  • plugin/fees/config.go (4 hunks)
  • plugin/fees/constraints.go (1 hunks)
  • plugin/fees/fees.go (7 hunks)
  • plugin/fees/helper.go (1 hunks)
  • plugin/fees/post_tx.go (1 hunks)
  • plugin/fees/transaction.go (6 hunks)
  • service/policy.go (1 hunks)
  • storage/db.go (1 hunks)
  • storage/postgres/fees.go (2 hunks)
  • storage/postgres/migrations/plugin/20250630152230_fee_runs.sql (2 hunks)
  • storage/postgres/policy.go (1 hunks)
  • storage/postgres/schema/schema.sql (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • internal/types/fees.go
  • storage/postgres/schema/schema.sql
  • storage/postgres/policy.go
  • storage/postgres/migrations/plugin/20250630152230_fee_runs.sql
  • plugin/fees/helper.go
  • service/policy.go
  • plugin/fees/post_tx.go
  • internal/verifierapi/verifierapi.go
  • plugin/fees/constraints.go
  • plugin/fees/transaction.go
🧰 Additional context used
🧠 Learnings (4)
📓 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-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:

  • cmd/fees/worker/main.go
  • storage/postgres/fees.go
  • plugin/fees/config.go
  • plugin/fees/fees.go
  • etc/vultisig/fee.yml
📚 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:

  • cmd/fees/worker/main.go
  • plugin/fees/fees.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:

  • cmd/fees/worker/main.go
  • storage/postgres/fees.go
  • plugin/fees/fees.go
🧬 Code Graph Analysis (4)
internal/verifierapi/fees.go (1)
internal/verifierapi/verifierapi.go (1)
  • VerifierApi (26-31)
cmd/fees/worker/main.go (2)
plugin/fees/config.go (3)
  • FeeCollectionFormat (188-191)
  • FeeCollectionType (179-179)
  • FeeCollectionTypeAll (185-185)
plugin/fees/constraints.go (3)
  • TypeFeeLoad (6-6)
  • TypeFeeTransact (7-7)
  • TypeFeePostTx (8-8)
storage/postgres/fees.go (3)
storage/postgres/db.go (1)
  • PostgresBackend (15-17)
internal/types/fees.go (6)
  • FeeRunState (11-11)
  • FeeRun (29-39)
  • FeeRunStateSent (15-15)
  • FeeRunStateSuccess (16-16)
  • Fee (21-26)
  • FeeRunStateDraft (14-14)
internal/verifierapi/fees.go (1)
  • FeeDto (12-21)
storage/db.go (2)
internal/types/fees.go (3)
  • FeeRunState (11-11)
  • FeeRun (29-39)
  • Fee (21-26)
internal/verifierapi/fees.go (1)
  • FeeDto (12-21)
🪛 YAMLlint (1.37.1)
etc/vultisig/fee.yml

[error] 7-7: syntax error: mapping values are not allowed here

(syntax)

⏰ 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 (9)
internal/verifierapi/fees.go (1)

60-84: LGTM! Well-structured fee collection marking endpoint.

The implementation follows good practices with proper error handling, response body cleanup via defer, and clear status code validation.

plugin/fees/fees.go (1)

288-293: Consider how to handle transaction execution errors

When executeFeeTransaction fails, the error is logged and returned to the error group, which will cause HandleTransactions to return an error. Consider whether failed transactions should mark the fee run with a failed status to prevent repeated processing attempts.

How are failed fee runs tracked and retried? Should there be a mechanism to mark runs as failed to prevent infinite retry loops?

plugin/fees/config.go (2)

160-167: LGTM! Comprehensive validation for job concurrency limits.

The validation correctly ensures all job types have reasonable concurrency limits between 1 and 100, preventing resource exhaustion while ensuring at least one concurrent job can execute.


47-54: Confirm Cron Expression Parsing & Production Fit

I’ve verified with robfig/cron/v3’s parser that all three expressions are syntactically valid. Please ensure these defaults align with your production workload:

File: plugin/fees/config.go (lines 47–54)

  • c.Jobs.Load.Cronexpr = "@every 2m"
  • c.Jobs.Post.Cronexpr = "@every 5m"
  • c.Jobs.Transact.Cronexpr = "0 12 * * 5" (Fridays at noon)
storage/db.go (5)

19-20: LGTM! Method rename improves API consistency.

The rename from GetAllPluginPolicies to GetPluginPolicies better reflects the method's filtering capabilities (by publicKey, pluginID, and active status) and aligns with the new GetAllFeePolicies method that truly returns all fee policies.


25-25: Excellent variadic parameter improvement for CreateFeeRun.

The change from fees []verifierapi.FeeDto to fees ...verifierapi.FeeDto provides better API ergonomics while maintaining backward compatibility. Callers can now pass fees directly without constructing a slice.


26-26: Consistent transaction identifier type change.

The change from txId uuid.UUID to txHash string aligns with the broader refactoring mentioned in the AI summary, replacing UUID-based transaction IDs with string-based transaction hashes throughout the system.


27-32: Well-designed fee run lifecycle management methods.

The new methods provide comprehensive fee run management:

  • SetFeeRunSuccess completes the transaction lifecycle
  • GetAllFeeRuns with optional status filtering supports monitoring
  • GetFees enables fee detail retrieval
  • GetPendingFeeRun prevents duplicate runs per policy
  • CreateFee supports incremental fee addition
  • GetFeeRuns provides status-specific queries

The API design supports the three-stage workflow (load, transact, post-tx) mentioned in the PR objectives.


28-28: GetAllFeeRuns variadic handling is correct – no action required

The implementation in storage/postgres/fees.go clearly checks len(statuses) == 0 to return all fee runs when no states are passed, and applies WHERE status = ANY($1) otherwise. The plural statuses parameter and accompanying comment are consistent.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (8)
plugin/fees/FEES.md (6)

7-7: Clarify system description: two components, three stages

Currently says “2 part system” but the doc describes three stages (load, transact, post). Be explicit about both.

-As this is a 2 part system, and the fees plugin works differently from other plugins. Here is an overview of the current architecture.
+This is a two-part system (Verifier + Plugin) with a three-stage workflow (Load, Transact, Post). The fee plugin works differently from other plugins. Here is an overview of the current architecture.

21-21: Tighten phrasing per style hint

Remove wordiness and clarify the audit fields.

-Fees are known to be collected successfully by the use of a `collected_at` attribute and the `charged_at` as being set as the date the fee was incurred which is pre-empted to be needed for audit purposes.
+Fees are marked as collected via `collected_at`. The `charged_at` field records when the fee was incurred for audit purposes.

42-43: Fix grammar and clarify separation of concerns

Improve readability.

-The db structure are different between verifier and plugin here due to a separation of concerns. Verifier needs to create fee entries, track their state across their lifecycle and handle treasury output from them. The plugin server simply needs to group them and track their ids and amounts.
+The DB structures differ between the verifier and the plugin due to separation of concerns. The verifier creates fee entries, tracks their lifecycle, and handles treasury accounting. The plugin groups fees into runs and tracks their IDs and amounts.

57-57: Fix mutual-exclusion phrasing

There are 3 stages total; Load should run only when the other two are not running.

-Will only run when one of the other 3 processes isn't running
+Runs only when neither of the other two stages (transact/post) is running

80-83: Avoid hardcoding chainId and gas; make them config/estimated

Reflects real behavior and avoids misleading implementers.

-- **Chain**: Ethereum (chainId: 1)
+- **Chain**: Ethereum (chainId: configured; e.g., 1 for mainnet)
@@
-- **Gas Limit**: 65,000 (typical ERC20 transfer upper bound)
+- **Gas**: Estimated at runtime with a safety cap (typical ERC20 transfer ≈ 50–65k)

135-141: Use consistent logging field casing

All fields use lowerCamel except tx_hash. Align for consistency.

-- `tx_hash`: Blockchain transaction hash
+- `txHash`: Blockchain transaction hash
etc/vultisig/fee.yml (2)

14-16: Optionally expose success confirmations for post stage

If the code supports a Post.SuccessConfirmations setting, surface it here instead of relying on an implicit default.

Apply:

     post:
         cronexpr: "@every 5m"
         max_concurrent_jobs: 10
+        success_confirmations: 3

If 3 isn’t the intended default, adjust accordingly.


16-16: Add a trailing newline

YAMLlint flagged the missing EOF newline. Add one to satisfy linters and POSIX text-file conventions.

Apply:

-        max_concurrent_jobs: 10
+        max_concurrent_jobs: 10
+
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4c5d21d and 15a008c.

📒 Files selected for processing (2)
  • etc/vultisig/fee.yml (1 hunks)
  • plugin/fees/FEES.md (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 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-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/FEES.md
📚 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.md
  • etc/vultisig/fee.yml
📚 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.md
🪛 LanguageTool
plugin/fees/FEES.md

[style] ~21-~21: ‘by the use of’ might be wordy. Consider a shorter alternative.
Context: ... are known to be collected successfully by the use of a collected_at attribute and the `cha...

(EN_WORDINESS_PREMIUM_BY_THE_USE_OF)

🪛 YAMLlint (1.37.1)
etc/vultisig/fee.yml

[error] 16-16: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (2)
etc/vultisig/fee.yml (2)

5-6: Good move: replace hardcoded provider URL with an environment variable

Switching from an embedded API key to an env var removes secrets from VCS and aligns with best practices.


3-6: Sanity-check chain_id vs USDC address

The USDC address provided is mainnet. Confirm chain_id: 1 is intentional and test/staging environments won’t pair a testnet chain_id with a mainnet address.

If you need environment-specific configs, consider separate YAMLs or templating to avoid cross‑environment misconfigurations.

Comment thread etc/vultisig/fee.yml
- 0x5BB06B9C5e4f7624Df7100Badb2F5AA1C86d8498
collector_address: 0x7d760c17d798a7A9a4c4AcAf311A02dC95972503
usdc_address: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
verifier_token: localhost-fee-apikey

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t commit verifier tokens; parameterize via environment

Even placeholder API keys can leak into logs/config dumps. Prefer env or secret manager injection.

Apply:

-verifier_token: localhost-fee-apikey
+verifier_token: ${FEES_VERIFIER_TOKEN}

If you don’t perform env expansion in YAML, read this from env in code instead.

📝 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
verifier_token: localhost-fee-apikey
verifier_token: ${FEES_VERIFIER_TOKEN}
🤖 Prompt for AI Agents
In etc/vultisig/fee.yml around line 4, the verifier_token value is a committed
API key placeholder; remove this hard-coded token and parameterize it so the
runtime reads it from an environment variable or secret manager instead. Replace
the literal verifier_token entry with a reference (or remove the field) and
update application config loading to read process environment (or secret store)
e.g., load VERIFIER_TOKEN at startup, validate presence and fail fast if
missing, and ensure any deployment manifests/CI inject the secret rather than
committing it to repo.

Comment thread etc/vultisig/fee.yml
collector_address: 0x7d760c17d798a7A9a4c4AcAf311A02dC95972503
usdc_address: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
verifier_token: localhost-fee-apikey
eth_provider: ${ETH_PROVIDER_URL}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure env var expansion is actually performed for eth_provider

Many YAML loaders don’t expand ${...} by default. If the config loader doesn’t explicitly expand env vars, this will be treated as the literal string "${ETH_PROVIDER_URL}" at runtime.

Run this to verify the codebase performs env expansion or reads eth_provider from env:

If env expansion isn’t present, either:

  • Expand at load-time in code (preferred), or
  • Document/ensure deployment runs envsubst on the file before loading.

🏁 Script executed:

#!/bin/bash
# Look for explicit env expansion or reading of ETH_PROVIDER_URL / eth_provider
rg -n 'os\.ExpandEnv|os\.Getenv|ExpandString|envsubst|ETH_PROVIDER_URL|eth_provider' -A2
# Inspect fees config parsing for struct tags and env handling
rg -n 'EthProvider|ChainId|cronexpr|max_concurrent_jobs|success_confirmations' -A4

Length of output: 8970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- viper occurrences ----"
rg -n "viper\." -S || true

echo
echo "---- config-related functions (ReadInConfig, Unmarshal, AutomaticEnv, SetEnvPrefix, SetEnvKeyReplacer) ----"
rg -n "ReadInConfig|Unmarshal|AutomaticEnv|SetEnvPrefix|SetEnvKeyReplacer|mapstructure" -S || true

echo
echo "---- plugin/fees/config.go ----"
sed -n '1,240p' plugin/fees/config.go || true

echo
echo "---- cmd/fees/config/config.go ----"
sed -n '1,240p' cmd/fees/config/config.go || true

Length of output: 24792


Action required: make ETH provider expansion explicit (fix required)

Viper does not expand ${...} inside YAML. WithFileConfig in plugin/fees/config.go uses viper.AutomaticEnv() with prefix FEES, so the runtime env var that will override the key is FEES_ETH_PROVIDER — but the literal string "${ETH_PROVIDER_URL}" in etc/vultisig/fee.yml will not be expanded unless you preprocess the file or change the loader.

Files to address

  • etc/vultisig/fee.yml — line with:
    eth_provider: ${ETH_PROVIDER_URL}
  • plugin/fees/config.go — WithFileConfig (viper setup / ReadInConfig / Unmarshal)

Recommended fixes (pick one)

  • Quick/deployment fix: set FEES_ETH_PROVIDER in your environment (this will override the config value because of AutomaticEnv + SetEnvPrefix). Optionally change fee.yml to an explicit empty/placeholder value to avoid the ${...} illusion:
    eth_provider: "" # overridden by FEES_ETH_PROVIDER

  • Preferred code fix: expand env vars in the loaded config before unmarshalling. Replace the ReadInConfig+Unmarshal block with a read + os.ExpandEnv + v.ReadConfig approach, e.g.:

    // after v.SetEnvKeyReplacer(...)
    if err := v.ReadInConfig(); err != nil {
    return fmt.Errorf("failed to read config: %w", err)
    }
    cfgPath := v.ConfigFileUsed()
    if cfgPath != "" {
    b, err := os.ReadFile(cfgPath)
    if err != nil {
    return fmt.Errorf("failed to read config for env expansion: %w", err)
    }
    expanded := os.ExpandEnv(string(b))
    v.SetConfigType("yaml")
    if err := v.ReadConfig(bytes.NewBufferString(expanded)); err != nil {
    return fmt.Errorf("failed to read expanded config: %w", err)
    }
    }
    if err := v.Unmarshal(c); err != nil {
    return fmt.Errorf("failed to unmarshal config: %w", err)
    }

    (Add imports: "os", "bytes")

Either document that deployments must run envsubst on fee.yml before starting, or implement the in-code expansion above. Leaving the current ${ETH_PROVIDER_URL} in the file without one of these changes will result in the literal string being used and NewFeeConfig failing the "eth_provider is required" check unless FEES_ETH_PROVIDER is set.

🤖 Prompt for AI Agents
In etc/vultisig/fee.yml around line 5 and plugin/fees/config.go in
WithFileConfig (viper setup / ReadInConfig / Unmarshal): the YAML contains a
literal "${ETH_PROVIDER_URL}" which Viper will not expand, so either set
FEES_ETH_PROVIDER in the environment (quick fix) or implement in-code env
expansion before Unmarshal (preferred) by reading the config file path via
v.ConfigFileUsed(), os.ReadFile it to a string, run os.ExpandEnv on the
contents, call v.SetConfigType("yaml") and
v.ReadConfig(bytes.NewBufferString(expanded)) and then v.Unmarshal(c); add
imports for "os" and "bytes".

Comment thread etc/vultisig/fee.yml
Comment on lines +7 to +16
jobs:
load:
cronexpr: "@every 2m"
max_concurrent_jobs: 10
transact:
cronexpr: "0 12 * * 5"
max_concurrent_jobs: 10
post:
cronexpr: "@every 5m"
max_concurrent_jobs: 10 No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm scheduler expects these keys and cron syntax

Validate that the config parser and scheduler expect:

  • Keys: jobs.load|transact|post with fields cronexpr and max_concurrent_jobs
  • Cron formats: robfig/cron/Asynq compatibility with "@every 2m" and "0 12 * * 5"

Also confirm concurrency here configures enqueuer concurrency (not worker pool), to avoid over-enqueueing or duplicate runs.

Run:

Operational advice:

  • Consider staggering load vs post to reduce overlap.
  • If transact is heavy, keep its concurrency lower unless the handler is idempotent and protected by dedupe/locks.

🏁 Script executed:

#!/bin/bash
# Verify struct tags/fields for Jobs in fees config
rg -n 'type .*Job|struct \{|\byaml:"cronexpr"|\byaml:"max_concurrent_jobs"' -A3
# Verify mapping from config.jobs.* to scheduler setup
rg -n 'startLoadingFees|startTransactingFees|startPostTx|cronexpr|max_concurrent_jobs' -A4
# Verify cron parser supports "@every"
rg -n '@every|cron\.New|WithSeconds|ParseStandard'

Length of output: 22380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== go.mod ==="
sed -n '1,200p' go.mod || true
echo

echo "=== grep for robfig/cron in repo ==="
rg --hidden --no-ignore -n "robfig/cron" || true
echo

echo "=== cmd/fees/worker/main.go (full) ==="
sed -n '1,400p' cmd/fees/worker/main.go || true
echo

echo "=== plugin/fees/config.go (full) ==="
sed -n '1,260p' plugin/fees/config.go || true
echo

echo "=== search for MaxConcurrentJobs usage ==="
rg -n "MaxConcurrentJobs|max_concurrent_jobs" || true
echo

echo "=== search for asynq client/server usage ==="
rg -n "asynq.New|asynq.Client|asynq.Server|asynq.NewClient|asynq.NewServer" || true
echo

Length of output: 26213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== semaphore usages ==="
rg -n "semaphore.NewWeighted|semaphore" || true
echo

echo "=== plugin/fees/fees.go (full) ==="
sed -n '1,400p' plugin/fees/fees.go || true
echo

echo "=== plugin/fees/post_tx.go (full) ==="
sed -n '1,400p' plugin/fees/post_tx.go || true
echo

Length of output: 14752


🌐 Web query:

Does robfig/cron v3 support the "@every" descriptor and 5-field cron specs (e.g. "0 12 * * 5")? cron.New() default parser behavior

💡 Result:

Short answer: Yes.

  • @every (and the other descriptors like @daily/@hourly) are supported in v3. (pkg.go.dev)
  • The default parser in v3 expects the standard 5-field cron spec (minute hour day-of-month month day-of-week), so "0 12 * * 5" is accepted by cron.New() without any special options. v3 intentionally switched the default to the standard 5-field form (the old v1/v2 behavior that included an initial seconds field is no longer the default). (pkg.go.dev, github.com)

If you need a seconds field (6-field/quartz-like) or want to customise parsing, construct the Cron with options, e.g.:

  • Required seconds: cron.New(cron.WithSeconds())
  • Custom parser: cron.New(cron.WithParser(cron.NewParser(...flags...))) (pkg.go.dev, github.com)

If you want, I can show a tiny code example demonstrating AddFunc with "@every 1h" and with "0 12 * * 5".

Citations:


Confirm cron keys & syntax; concurrency is worker-side (semaphores), not the enqueuer

Snippet (unchanged):

jobs:
    load:
        cronexpr: "@every 2m"
        max_concurrent_jobs: 10
    transact:
        cronexpr: "0 12 * * 5"
        max_concurrent_jobs: 10
    post:
        cronexpr: "@every 5m"
        max_concurrent_jobs: 10

Summary (very short):

  • Keys exist and are unmarshaled by viper (plugin/fees/config.go — Jobs struct + mapstructure tags; defaults set in withDefaults).
  • robfig/cron v3 (present in go.mod) + cron.New() accept both "@every 2m" and "0 12 * * 5" (v3 default parser = standard 5-field, descriptors supported).
  • MaxConcurrentJobs is used inside handlers to limit concurrent processing via semaphore.NewWeighted(...) (plugin/fees/fees.go — Load/Transact, plugin/fees/post_tx.go — Post). It does NOT throttle the scheduler/enqueuer.
  • The scheduler simply calls asynqClient.Enqueue (cmd/fees/worker/main.go — cron.New().AddFunc(...)), and asynq server concurrency is hard-coded to 10 in cmd/fees/worker/main.go.

Files to review / attention:

  • plugin/fees/config.go — Jobs struct, defaults and validation (cronexpr, max_concurrent_jobs).
  • etc/vultisig/fee.yml — current cron exprs shown above.
  • cmd/fees/worker/main.go — cron.New() + AddFunc uses feePluginConfig.Jobs..Cronexpr; asynq.NewServer configured with Concurrency: 10; start functions enqueue tasks.
  • plugin/fees/fees.go & plugin/fees/post_tx.go — semaphore.NewWeighted(int64(fp.config.Jobs.*.MaxConcurrentJobs)) used to limit processing concurrency.

Actionable recommendation (short):

  • If the intent was to throttle/enforce a single in-flight scheduled job (avoid over-enqueueing), add explicit checks before enqueueing (use asynq.Inspector to check queue / inflight tasks, or use a distributed lock/dedupe), or make asynq.Server concurrency configurable from FeeConfig and align it with MaxConcurrentJobs.
  • Consider staggering load vs post or lowering transact concurrency if transact work is heavy or not idempotent.
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 16-16: no new line character at the end of file

(new-line-at-end-of-file)

🤖 Prompt for AI Agents
In etc/vultisig/fee.yml lines 7-16, the cron entries are valid but the
configured max_concurrent_jobs only gates worker-side processing (semaphores)
and does not prevent the scheduler/enqueuer from pushing many tasks; to fix this
either (A) add a pre-enqueue guard in cmd/fees/worker/main.go to avoid
over-enqueueing (e.g., use asynq.Inspector to inspect queue/inflight counts or
acquire a distributed lock/dedupe key before calling asynqClient.Enqueue), or
(B) make asynq.Server concurrency configurable from the fee config and align
asynq concurrency with Jobs.*.MaxConcurrentJobs so enqueue rate matches worker
capacity; optionally stagger schedules or lower transact concurrency if that job
is heavy.

Comment thread plugin/fees/FEES.md
Comment on lines +1 to +3
# Fees plugin

The fees pluigin is responsible for collecting user based fees incurred from other plugins

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Adopt singular "fee" naming and fix typo in intro

Aligns with the project's naming convention and fixes a spelling error.

Apply:

-# Fees plugin
+# Fee plugin
@@
-The fees pluigin is responsible for collecting user based fees incurred from other plugins
+The fee plugin is responsible for collecting user-based fees incurred from other plugins

Additionally, consider renaming this file to FEE.md to fully comply with the “use fee (singular)” guideline.

📝 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
# Fees plugin
The fees pluigin is responsible for collecting user based fees incurred from other plugins
# Fee plugin
The fee plugin is responsible for collecting user-based fees incurred from other plugins
🤖 Prompt for AI Agents
In plugin/fees/FEES.md around lines 1 to 3, the intro uses plural "fees" and
contains a typo ("pluigin"); update the wording to use the singular "fee"
throughout and fix the spelling (e.g., "The fee plugin is responsible for
collecting user-based fees incurred from other plugins"), and rename the file to
FEE.md to match the project's singular naming convention.

Comment thread plugin/fees/FEES.md
Comment on lines +25 to +27
- *currently pending pr* - fee ids are passed along with a signing request which are used to verify that the amount being requested for a signature is the same.
- *current* - the system checks for all fees which are due to be collected and if the amount doesn't match it rejects a sign request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update Tx signing notes: remove “pending pr” and reflect current behavior

The PR introduces MarkFeeAsCollected and fee-id-aware signing. Remove “pending pr” and document the actual behavior.

-#### Tx signing
-
-- *currently pending pr* - fee ids are passed along with a signing request which are used to verify that the amount being requested for a signature is the same.
-- *current* - the system checks for all fees which are due to be collected and if the amount doesn't match it rejects a sign request.
+#### Tx signing
+
+- Fee IDs are included with the signing request and validated so the requested amount matches the sum of pending fees.
+- If the amount does not match, the sign request is rejected.
📝 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
- *currently pending pr* - fee ids are passed along with a signing request which are used to verify that the amount being requested for a signature is the same.
- *current* - the system checks for all fees which are due to be collected and if the amount doesn't match it rejects a sign request.
#### Tx signing
- Fee IDs are included with the signing request and validated so the requested amount matches the sum of pending fees.
- If the amount does not match, the sign request is rejected.
🤖 Prompt for AI Agents
In plugin/fees/FEES.md around lines 25 to 27, remove the phrase "currently
pending pr" and update the two bullet points to reflect the implemented
behavior: state that fee IDs are now passed with signing requests and used to
verify the requested signature amount matches the fee record (fee-id-aware
signing), and that the system checks all fees due and rejects sign requests when
amounts don't match; also mention the new MarkFeeAsCollected flow which marks
fees collected once verified. Keep the wording concise and replace the outdated
"pending pr" note with the concrete implemented behavior.

Comment thread plugin/fees/FEES.md
Comment on lines +48 to +49
**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Align schedule with current config defaults

Doc says “every 10 minutes” but fee.yml uses @every 2m per the PR summary. Adjust or explicitly reference config.

-**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
+**Trigger**: Scheduled cron job (configurable; default: @every 2m, see fee.yml)

If the default has changed, please update fee.yml accordingly or confirm the intended default.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Repo root: $(pwd) ==="
echo
echo "1) Find files with 'fee' in the name (yml/yaml):"
find . -type f \( -iname "*fee*.yml" -o -iname "*fee*.yaml" \) -print || true
echo
echo "2) Ripgrep for '@every' and 'fee.yml' occurrences:"
rg --hidden -n --no-ignore-vcs -S '@every' || true
rg --hidden -n --no-ignore-vcs -S 'fee\.ya?ml' || true
echo
echo "3) Show content of likely fee config files (first 200 lines each):"
files=$(find . -type f \( -iname "*fee*.yml" -o -iname "*fee*.yaml" \) -print)
if [ -z "$files" ]; then
  echo "No fee*.yml/yaml files found by name. Searching for 'fee' directories under plugin/fees..."
  files=$(find plugin -type f -iname "*.yml" -o -iname "*.yaml" | rg -n 'fees|fee' || true)
fi

if [ -n "$files" ]; then
  echo "$files" | while read -r f; do
    echo
    echo "---- $f ----"
    sed -n '1,200p' "$f" || true
  done
else
  echo "No fee-related yaml files found to display."
fi

Length of output: 2145


Align FEES.md schedule with actual config defaults

FEES.md states "every 10 minutes" but the repo config uses different defaults (load = "@every 2m", post = "@every 5m"). Update the docs or confirm/change the config.

Files to update / check:

  • plugin/fees/FEES.md — lines ~48-49 (replace the "every 10 minutes" text).
  • etc/vultisig/fee.yml — jobs.load.cronexpr: "@every 2m", jobs.post.cronexpr: "@every 5m", jobs.transact.cronexpr: "0 12 * * 5"
  • plugin/fees/config.go — c.Jobs.Load.Cronexpr = "@every 2m" (line ~52), c.Jobs.Post.Cronexpr = "@every 5m" (line ~54)

Suggested change:

-**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
+**Trigger**: Scheduled cron job (configurable; defaults: load @every 2m, post @every 5m — see etc/vultisig/fee.yml)

If the intended default is different, update etc/vultisig/fee.yml and plugin/fees/config.go to match, or confirm and keep the doc change.

📝 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
**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
**Trigger**: Scheduled cron job (configurable; defaults: load @every 2m, post @every 5m — see etc/vultisig/fee.yml)
🤖 Prompt for AI Agents
In plugin/fees/FEES.md around lines 48-49 the schedule text "every 10 minutes"
is incorrect; update that phrase to reflect the actual defaults used in the repo
(load = "@every 2m", post = "@every 5m") or change the config to match the doc.
Edit plugin/fees/FEES.md to replace "every 10 minutes" with the correct
schedules (e.g., "load: every 2 minutes; post: every 5 minutes"), and verify
consistency by checking etc/vultisig/fee.yml (jobs.load.cronexpr: "@every 2m",
jobs.post.cronexpr: "@every 5m", jobs.transact.cronexpr: "0 12 * * 5") and
plugin/fees/config.go (c.Jobs.Load.Cronexpr and c.Jobs.Post.Cronexpr) and update
either the documentation or the two config files so the doc and configs match.

Comment thread plugin/fees/FEES.md
Comment on lines +65 to +71
#### 2. Transaction Handling (`HandleTransactions`)

**Trigger**: Scheduled cron job (configurable interval, default: fridays weekly)

**Process**:
1. Retrieves all fee runs in `draft` status
2. For each valid fee run:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document run-based transaction proposal and the intentionally unimplemented public method

Add an explicit note to prevent confusion for integrators.

-#### 2. Transaction Handling (`HandleTransactions`)
+#### 2. Transaction Handling (`HandleTransactions`)
+
+Note: The public `ProposeTransactions` method intentionally returns “not implemented” for the fee plugin v2. The actual logic uses a run-based flow implemented in the private `proposeTransactions(types.FeeRun)` method.
@@
-**Trigger**: Scheduled cron job (configurable interval, default: fridays weekly)
+**Trigger**: Scheduled cron job (configurable; default: 0 12 * 5, i.e. Fridays at 12:00 UTC)
📝 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
#### 2. Transaction Handling (`HandleTransactions`)
**Trigger**: Scheduled cron job (configurable interval, default: fridays weekly)
**Process**:
1. Retrieves all fee runs in `draft` status
2. For each valid fee run:
#### 2. Transaction Handling (`HandleTransactions`)
Note: The public `ProposeTransactions` method intentionally returns “not implemented” for the fee plugin v2. The actual logic uses a run-based flow implemented in the private `proposeTransactions(types.FeeRun)` method.
**Trigger**: Scheduled cron job (configurable; default: 0 12 * 5, i.e. Fridays at 12:00 UTC)
**Process**:
1. Retrieves all fee runs in `draft` status
2. For each valid fee run:
🤖 Prompt for AI Agents
In plugin/fees/FEES.md around lines 65 to 71, the docs for "Transaction Handling
(HandleTransactions)" lack an explicit note about the run-based transaction
proposal and an intentionally unimplemented public method; update this section
to add a clear, concise paragraph stating that transactions are handled per fee
run (describe the run-based approach in one sentence) and explicitly note which
public method is intentionally left unimplemented, why (e.g., internal-only /
planned future work), and what integrators should use instead (e.g., supported
public API or cron trigger). Ensure the note prevents confusion by naming the
method, stating its current status, and pointing to the recommended integration
path.

Comment thread plugin/fees/FEES.md
Comment on lines +86 to +88
**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)

**Process**:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Align post-tx schedule with current config defaults

Matches the fee.yml example in the PR summary.

-**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
+**Trigger**: Scheduled cron job (configurable; default: @every 5m, see fee.yml)

Please confirm the intended default and update either the doc or config if they differ.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- plugin/fees/FEES.md (lines 1-200) ----"
if [ -f plugin/fees/FEES.md ]; then
  sed -n '1,200p' plugin/fees/FEES.md
else
  echo "plugin/fees/FEES.md not found"
fi

echo
echo "---- Searching repository for '@every', 'every 5m', 'every 10m', or '10 minutes' ----"
rg -n --hidden -S '@every|every\s*5m|every\s*10m|10\s*minutes' || true

echo
echo "---- Searching repository for fee.yml/fee.yaml/fees.yml/fees.yaml strings ----"
rg -n --hidden -S 'fee\.yml|fee\.yaml|fees\.yml|fees\.yaml' || true

echo
echo "---- Show plugin/fees/fee.yml if present ----"
if [ -f plugin/fees/fee.yml ]; then
  sed -n '1,200p' plugin/fees/fee.yml
else
  echo "plugin/fees/fee.yml not found"
fi

echo
echo "---- Show root fee.yml if present ----"
if [ -f fee.yml ]; then
  sed -n '1,200p' fee.yml
else
  echo "fee.yml not found at repo root"
fi

Length of output: 9886


Align scheduling defaults in FEES.md with config values

FEES.md documents "default: every 10 minutes" for the LoadFees and HandlePostTx jobs, but the repository defaults are @every 2m (load) and @every 5m (post). Update the docs or the config so they match.

Files/locations to fix:

  • plugin/fees/FEES.md — update Trigger lines at (rg matches) lines 48 and 86.
  • plugin/fees/config.go — current values: Jobs.Load.Cronexpr = "@every 2m" (line ~52), Jobs.Post.Cronexpr = "@every 5m" (line ~54).
  • etc/vultisig/fee.yml — cronexpr entries: "@every 2m" and "@every 5m" (lines ~9 and ~15).
  • cmd/fees/worker/main.go — update stale comments referencing "every 10 minutes" (lines ~180 and ~194).

Suggested diffs for FEES.md:

-**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
+**Trigger**: Scheduled cron job (configurable; default: @every 2m — see plugin/fees/config.go or etc/vultisig/fee.yml)

(for LoadFees)

-**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
+**Trigger**: Scheduled cron job (configurable; default: @every 5m — see plugin/fees/config.go or etc/vultisig/fee.yml)

(for HandlePostTx)

Please update the docs or adjust the config if the intended defaults are different.

📝 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
**Trigger**: Scheduled cron job (configurable interval, default: every 10 minutes)
**Process**:
**Trigger**: Scheduled cron job (configurable; default: @every 5m — see plugin/fees/config.go or etc/vultisig/fee.yml)
**Process**:

Comment thread plugin/fees/FEES.md
Comment on lines +112 to +114
1. **Resource Validation**: Only `ethereum.erc20.transfer` operations allowed
2. **Recipient Validation**: Must use `VULTISIG_TREASURY` magic constant
3. **Amount Constraints**: Maximum fee amount enforcement

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Restrict resource to USDC transfer per policy constraints

Project policy is USDC-only, not general ERC20.

-1. **Resource Validation**: Only `ethereum.erc20.transfer` operations allowed
+1. **Resource Validation**: Only `ethereum.usdc.transfer` operations allowed

To verify consistency across the repo:


🏁 Script executed:

#!/bin/bash
# Check occurrences of resource names and verify USDC-only constraint mentioned in policy/validation code and docs.
rg -n "ethereum\.erc20\.transfer|ethereum\.usdc\.transfer" -A 2

Length of output: 1686


Restrict fees plugin to USDC transfers — change required

FEES.md and the fees plugin contain inconsistent resource names; the fees plugin must be USDC-only.

Files needing changes:

  • plugin/fees/FEES.md (line ~112): replace ethereum.erc20.transferethereum.usdc.transfer
  • plugin/fees/policy.go (lines ~46-47): replace Full: "ethereum.erc20.transfer"Full: "ethereum.usdc.transfer"
  • plugin/fees/transaction.go (line ~73): replace case "ethereum.erc20.transfer":case "ethereum.usdc.transfer":

Note: plugin/payroll/transaction.go contains ethereum.erc20.transfer (lines ~314–316) — likely unrelated to the fees plugin; leave or review separately if intended.

Updated diff (docs):

-1. **Resource Validation**: Only `ethereum.erc20.transfer` operations allowed
+1. **Resource Validation**: Only `ethereum.usdc.transfer` operations allowed

(Used retrieved learning confirming the fees plugin is USDC-only.)

📝 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
1. **Resource Validation**: Only `ethereum.erc20.transfer` operations allowed
2. **Recipient Validation**: Must use `VULTISIG_TREASURY` magic constant
3. **Amount Constraints**: Maximum fee amount enforcement
1. **Resource Validation**: Only `ethereum.usdc.transfer` operations allowed
2. **Recipient Validation**: Must use `VULTISIG_TREASURY` magic constant
3. **Amount Constraints**: Maximum fee amount enforcement
🤖 Prompt for AI Agents
In plugin/fees/FEES.md around lines 112–114, plugin/fees/policy.go around lines
46–47, and plugin/fees/transaction.go around line 73 the resource name
incorrectly uses "ethereum.erc20.transfer"; change each occurrence to
"ethereum.usdc.transfer" to restrict the fees plugin to USDC-only and keep docs,
policy, and transaction code consistent; do not modify
plugin/payroll/transaction.go (lines ~314–316) as that is separate.

Comment thread plugin/fees/FEES.md
Comment on lines +128 to +130
1. **`GetPublicKeysFees(publicKey)`** - Retrieves pending fees for a vault
2. **`MarkFeeAsCollected(txHash, timestamp, feeIds...)`** - Marks fees as collected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Specify HTTP endpoints and payload for verifier integration

Make the API contract explicit and remove ambiguity around method names.

-1. **`GetPublicKeysFees(publicKey)`** - Retrieves pending fees for a vault
-2. **`MarkFeeAsCollected(txHash, timestamp, feeIds...)`** - Marks fees as collected
+1. GET `/fees?publicKey={publicKey}` - Retrieves pending fees for a vault
+2. POST `/fees/collected` - Marks fees as collected
+   Body (JSON):
+   {
+     "txHash": "0x...",
+     "collectedAt": "2025-08-01T12:34:56Z",
+     "feeIds": ["fee_123", "fee_456"]
+   }

If the actual endpoints or parameter names differ, please update the doc to match the implementation. You can confirm with:


🏁 Script executed:

#!/bin/bash
# Discover the verifier client methods and routes
rg -n "MarkFeeAsCollected|/fees/collected|GetPublicKey|GetPublicKeysFees|/fees\?" -A 3

Length of output: 1908


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,240p' internal/verifierapi/fees.go

Length of output: 3044


Update FEES.md to match verifier API (endpoints & JSON payloads)

The verifier implements GET /fees/publickey/{ecdsaPublicKey} and POST /fees/collected with snake_case JSON keys — update the doc to match.

Files to update / reference:

  • plugin/fees/FEES.md (lines ~128-130) — update these entries
  • internal/verifierapi/fees.go — source of truth for endpoints/payload
  • plugin/fees/fees.go and plugin/fees/post_tx.go — caller examples

Suggested diff:

-1. **`GetPublicKeysFees(publicKey)`** - Retrieves pending fees for a vault
-2. **`MarkFeeAsCollected(txHash, timestamp, feeIds...)`** - Marks fees as collected
+1. GET `/fees/publickey/{ecdsaPublicKey}` - Retrieves pending fees for a vault
+   Response: 200 JSON -> APIResponse with "data" containing FeeHistoryDto
+2. POST `/fees/collected` - Marks fees as collected
+   Body (JSON):
+   {
+     "ids": ["fee-uuid-1", "fee-uuid-2"],
+     "tx_hash": "0x...",
+     "collected_at": "2025-08-01T12:34:56Z" // ISO8601 timestamp
+   }
+   Response: 200 OK
📝 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
1. **`GetPublicKeysFees(publicKey)`** - Retrieves pending fees for a vault
2. **`MarkFeeAsCollected(txHash, timestamp, feeIds...)`** - Marks fees as collected
1. GET `/fees/publickey/{ecdsaPublicKey}` - Retrieves pending fees for a vault
Response: 200 JSON -> APIResponse with "data" containing FeeHistoryDto
2. POST `/fees/collected` - Marks fees as collected
Body (JSON):
{
"ids": ["fee-uuid-1", "fee-uuid-2"],
"tx_hash": "0x...",
"collected_at": "2025-08-01T12:34:56Z" // ISO8601 timestamp
}
Response: 200 OK
🤖 Prompt for AI Agents
In plugin/fees/FEES.md around lines 128-130, the documented API methods are out
of sync with the verifier: replace the two list entries with the actual
endpoints and snake_case payloads used by the verifier — document GET
/fees/publickey/{ecdsaPublicKey} (returns pending fees for the vault) and POST
/fees/collected with a snake_case JSON body (e.g., tx_hash, timestamp, fee_ids
array) and the expected response; use internal/verifierapi/fees.go as the source
of truth and mirror the caller examples in plugin/fees/fees.go and
plugin/fees/post_tx.go to ensure endpoint paths and JSON key names match
exactly.

@RaghavSood RaghavSood left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM for WIP

@garry-sharp Can we rebase once to make sure nothing else breaks before merging?

Comment thread cmd/fees/worker/main.go
"github.com/vultisig/plugin/storage/postgres"
)

func startLoadingFees(asynqClient *asynq.Client, logger *logrus.Logger) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should probably have a mutex - it's possible that in theory the cron run could trigger while the previous run hasn't completed yet

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a mutex in the service itself which stops loads and txs happening synchronously

@garry-sharp
garry-sharp merged commit d80752a into main Aug 15, 2025
3 checks passed
@garry-sharp
garry-sharp deleted the fees-v2 branch August 15, 2025 18:26
@coderabbitai coderabbitai Bot mentioned this pull request Aug 19, 2025
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.

3 participants