Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions internal/verifierapi/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,27 +140,24 @@ func (v *VerifierApi) CreateFeeCredit(id uuid.UUID, amount int64, publicKey stri
return nil
}

func (v *VerifierApi) RevertFeeCredit(txHash string, batchId uuid.UUID) error {
response, err := v.postAuth(fmt.Sprintf("/fees/revert/%s", batchId), struct{}{})
if err != nil {
return fmt.Errorf("failed to revert fee credit: %w", err)
}
defer response.Body.Close()

return nil
}

func (v *VerifierApi) UpdateFeeBatchTxHash(publickey string, batchId uuid.UUID, hash string) (*FeeBatchCreateResponseDto, error) {
func (v *VerifierApi) UpdateFeeBatch(publickey string, batchId uuid.UUID, hash string, status types.FeeBatchState) (*APIResponse[FeeBatchCreateResponseDto], error) {
response, err := v.putAuth("/fees/batch", FeeBatchUpdateRequestResponseDto{
PublicKey: publickey,
BatchID: batchId,
TxHash: hash,
Status: types.FeeBatchStateSent,
Status: status,
})
if err != nil {
return nil, fmt.Errorf("failed to get fee batch: %w", err)
}
defer response.Body.Close()
var feeBatchResponse APIResponse[FeeBatchCreateResponseDto]
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to update fee batch, status code: %d", response.StatusCode)
}
if err := json.NewDecoder(response.Body).Decode(&feeBatchResponse); err != nil {
return nil, fmt.Errorf("failed to decode fee batch response: %w", err)
}

return nil, nil
return &feeBatchResponse, nil
}
5 changes: 0 additions & 5 deletions plugin/fees/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package fees
import (
"context"
"fmt"
"sync"

"github.com/google/uuid"
"github.com/hibiken/asynq"
Expand Down Expand Up @@ -33,14 +32,11 @@ func (fp *FeePlugin) LoadFees(ctx context.Context, task *asynq.Task) error {

// We limit the number of concurrent fee loading operations to 10
sem := semaphore.NewWeighted(int64(fp.config.Jobs.Load.MaxConcurrentJobs))
var wg sync.WaitGroup
var eg errgroup.Group

for _, feePolicy := range feePolicies {
wg.Add(1)
feePolicy = feePolicy
eg.Go(func() error {
defer wg.Done()
if err := sem.Acquire(ctx, 1); err != nil {
return fmt.Errorf("failed to acquire semaphore: %w", err)
}
Expand All @@ -61,7 +57,6 @@ func (fp *FeePlugin) LoadFees(ctx context.Context, task *asynq.Task) error {
})
}

wg.Wait()
if err := eg.Wait(); err != nil {
return fmt.Errorf("failed to execute fee loading: %w", err)
}
Expand Down
89 changes: 81 additions & 8 deletions plugin/fees/post_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hibiken/asynq"
"github.com/sirupsen/logrus"
"github.com/vultisig/plugin/internal/types"
vtypes "github.com/vultisig/verifier/types"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
)
Expand Down Expand Up @@ -38,7 +39,12 @@ func (fp *FeePlugin) HandlePostTx(ctx context.Context, task *asynq.Task) error {
return fmt.Errorf("failed to acquire semaphore: %w", err)
}
defer sem.Release(1)
return fp.updateStatus(ctx, feeBatch, currentBlock)
if err := fp.updateStatus(ctx, feeBatch, currentBlock); err != nil {
fp.logger.WithField("batch_id", feeBatch.BatchID).Error("Failed to update fee batch status", err)
} else {
fp.logger.WithField("batch_id", feeBatch.BatchID).Info("Fee batch status update run successfully")
}
return nil
Comment on lines +42 to +47
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

The error logging statement on line 42 has incorrect syntax. It should use WithError(err) or include the error in the message string, not pass it as a second argument to Error().

Copilot uses AI. Check for mistakes.
})
}
if err := eg.Wait(); err != nil {
Expand All @@ -61,6 +67,8 @@ func (fp *FeePlugin) updateStatus(ctx context.Context, batch types.FeeBatch, cur
fp.logger.WithFields(logrus.Fields{"batch_id": batch.BatchID}).Info("tx not found on chain, rebroadcasting")
return nil
}

// Tx successful
if receipt.Status == 1 {
if currentBlock > receipt.BlockNumber.Uint64()+fp.config.Jobs.Post.SuccessConfirmations {
fp.logger.WithFields(logrus.Fields{"batch_id": batch.BatchID}).Info("tx successful, setting to success")
Expand All @@ -69,23 +77,32 @@ func (fp *FeePlugin) updateStatus(ctx context.Context, batch types.FeeBatch, cur
if err != nil {
return err
}

var rollbackErr error
defer func() {
if rollbackErr != nil {
tx.Rollback(ctx)
}
}()

fp.verifierApi.UpdateFeeBatchTxHash(*batch.TxHash, batch.BatchID, *batch.TxHash)
hash := ""
if batch.TxHash != nil {
hash = *batch.TxHash
}

if err = fp.db.SetFeeBatchStatus(ctx, tx, batch.BatchID, types.FeeBatchStateSuccess); err != nil {
resp, err := fp.verifierApi.UpdateFeeBatch(batch.PublicKey, batch.BatchID, hash, types.FeeBatchStateSuccess)
if err != nil {
rollbackErr = err
return fmt.Errorf("failed to update verifier fee batch to success: %w", err)
}
if resp.Error.Message != "" {
rollbackErr = fmt.Errorf("failed to update verifier fee batch to success: %s", resp.Error.Message)
return fmt.Errorf("failed to update verifier fee batch to success: %s", resp.Error.Message)
}

if err = fp.db.SetFeeBatchStatus(ctx, tx, batch.BatchID, types.FeeBatchStateSuccess); err != nil {
rollbackErr = err
return fmt.Errorf("failed to set fee batch success: %w", err)
return fmt.Errorf("failed to update verifier fee batch to success: %w", err)
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

The error message is inconsistent with the actual operation. This error occurs when setting the fee batch status in the database, not when updating the verifier fee batch. The message should be 'failed to set fee batch status to success: %w'.

Suggested change
return fmt.Errorf("failed to update verifier fee batch to success: %w", err)
return fmt.Errorf("failed to set fee batch status to success: %w", err)

Copilot uses AI. Check for mistakes.
}

if err = tx.Commit(ctx); err != nil {
Expand All @@ -97,10 +114,66 @@ func (fp *FeePlugin) updateStatus(ctx context.Context, batch types.FeeBatch, cur
return nil
}
} else {
// TODO failed tx logic
fp.logger.WithFields(logrus.Fields{"batch_id": batch.BatchID}).Info("tx failed, setting to failed")
fp.verifierApi.RevertFeeCredit(*batch.TxHash, batch.BatchID)
return nil
// Handle failed tx - in this case, we simply set the batch to failed. And request for the verifier to create a new debit line of "failed tx"
return fp.handleFailedTx(ctx, batch)
}
return nil
}

// This function sets a batch id to be failed and requests for a new debit line to be created. The failed tx then gets picked up in a new batch.
func (fp *FeePlugin) handleFailedTx(ctx context.Context, batch types.FeeBatch) error {
fp.logger.WithFields(logrus.Fields{"batch_id": batch.BatchID}).Info("tx failed, setting to failed")

tx, err := fp.db.Pool().Begin(ctx)
if err != nil {
return err
}
var rollbackErr error
defer func() {
if rollbackErr != nil {
tx.Rollback(ctx)
}
}()

// This api call automatically creates a new debit line for the failed tx, which will get picked up in a new batch.
err = fp.db.SetFeeBatchStatus(ctx, tx, batch.BatchID, types.FeeBatchStateFailed)
if err != nil {
rollbackErr = err
return fmt.Errorf("failed to set fee batch status to failed: %w", err)
}

hash := ""
if batch.TxHash != nil {
hash = *batch.TxHash
}
resp, err := fp.verifierApi.UpdateFeeBatch(batch.PublicKey, batch.BatchID, hash, types.FeeBatchStateFailed)
if err != nil {
rollbackErr = err
return fmt.Errorf("failed to update verifier fee batch to failed: %w", err)
}
if resp.Error.Message != "" {
rollbackErr = fmt.Errorf("failed to update verifier fee batch to failed: %s", resp.Error.Message)
return fmt.Errorf("failed to update verifier fee batch to failed: %s", resp.Error.Message)
}

if err = tx.Commit(ctx); err != nil {
rollbackErr = err
return fmt.Errorf("failed to commit transaction: %w", err)
}

feePolicies, err := fp.db.GetPluginPolicies(ctx, batch.PublicKey, vtypes.PluginVultisigFees_feee, true)
if err != nil {
rollbackErr = err
return fmt.Errorf("failed to get plugin policy: %w", err)
}

if len(feePolicies) < 1 {
rollbackErr = err
return fmt.Errorf("failed to get plugin policy: %w", err)
Comment on lines +171 to +172
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

The variable err is being assigned to rollbackErr but err is nil at this point since the previous operation succeeded. This should be rollbackErr = fmt.Errorf(\"no plugin policies found\") or similar.

Suggested change
rollbackErr = err
return fmt.Errorf("failed to get plugin policy: %w", err)
rollbackErr = fmt.Errorf("no plugin policies found for public key %s", batch.PublicKey)
return fmt.Errorf("no plugin policies found for public key %s", batch.PublicKey)

Copilot uses AI. Check for mistakes.
}

feePolicy := feePolicies[0]

// Immediately load a new fee batch
return fp.executeFeeLoading(ctx, feePolicy)
}
34 changes: 28 additions & 6 deletions plugin/fees/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,6 @@ func (fp *FeePlugin) initSign(
feeBatch types.FeeBatch,
) error {

fmt.Printf("Init sign: %+v\n", req)

sigs, err := fp.signer.Sign(ctx, req)
if err != nil {
fp.logger.WithError(err).Error("Keysign failed")
Expand Down Expand Up @@ -193,11 +191,35 @@ func (fp *FeePlugin) initSign(
return fmt.Errorf("failed to decode tx: %w", err)
}

if err := fp.db.SetFeeBatchSent(ctx, txHash.Hash().Hex(), feeBatch.BatchID); err != nil {
tx, err := fp.db.Pool().Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
var rollbackErr error
defer func() {
if rollbackErr != nil {
tx.Rollback(ctx)
}
}()

if err := fp.db.SetFeeBatchSent(ctx, tx, txHash.Hash().Hex(), feeBatch.BatchID); err != nil {
rollbackErr = err
return fmt.Errorf("failed to set fee batch sent: %w", err)
}
resp, err := fp.verifierApi.UpdateFeeBatch(pluginPolicy.PublicKey, feeBatch.BatchID, txHash.Hash().Hex(), types.FeeBatchStateSent)
if err != nil {
rollbackErr = err
return fmt.Errorf("failed to update fee batch: %w", err)
}
if resp.Error.Message != "" {
rollbackErr = err
Comment on lines +214 to +215
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

The variable err is being assigned to rollbackErr but err is nil at this point since the API call succeeded. This should be rollbackErr = fmt.Errorf(\"API error: %s\", resp.Error.Message) or similar.

Suggested change
if resp.Error.Message != "" {
rollbackErr = err
rollbackErr = fmt.Errorf("API error: %s", resp.Error.Message)

Copilot uses AI. Check for mistakes.
return fmt.Errorf("failed to update fee batch: %s", resp.Error.Message)
}
Comment on lines +214 to +217
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: rollback never triggers on API-level error (leaks transaction/connection).

rollbackErr = err assigns nil (transport succeeded) when resp.Error.Message is set, so the deferred rollback won’t run. The tx stays open and the connection is pinned.

Apply:

-  if resp.Error.Message != "" {
-    rollbackErr = err
-    return fmt.Errorf("failed to update fee batch: %s", resp.Error.Message)
-  }
+  if resp.Error.Message != "" {
+    rollbackErr = fmt.Errorf("verifier api error: %s", resp.Error.Message)
+    return rollbackErr
+  }
📝 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 resp.Error.Message != "" {
rollbackErr = err
return fmt.Errorf("failed to update fee batch: %s", resp.Error.Message)
}
if resp.Error.Message != "" {
rollbackErr = fmt.Errorf("verifier api error: %s", resp.Error.Message)
return rollbackErr
}
🤖 Prompt for AI Agents
In plugin/fees/transaction.go around lines 214 to 217, the deferred rollback
never runs because rollbackErr is set to err (which is nil) when
resp.Error.Message is present; change the code to set rollbackErr to a non-nil
error built from resp.Error.Message (e.g., rollbackErr = fmt.Errorf("API error
updating fee batch: %s", resp.Error.Message)) before returning so the deferred
rollback runs and the transaction/connection is released.


fp.verifierApi.UpdateFeeBatchTxHash(pluginPolicy.PublicKey, feeBatch.BatchID, txHash.Hash().Hex())
if err := tx.Commit(ctx); err != nil {
rollbackErr = err
return fmt.Errorf("failed to commit transaction: %w", err)
}

fp.logger.WithFields(logrus.Fields{
"tx_hash": txHash.Hash().Hex(),
Expand All @@ -208,14 +230,14 @@ func (fp *FeePlugin) initSign(
"batch_id": feeBatch.BatchID,
}).Info("fee collection transaction")

tx, err := fp.eth.Send(ctx, decodedHexTx, r, s, v)
ethTx, err := fp.eth.Send(ctx, decodedHexTx, r, s, v)
if err != nil {
fp.logger.WithError(err).WithField("tx_hex", req.Transaction).Error("fp.eth.Send")
return fmt.Errorf("failed to send transaction: %w", err)
}

fp.logger.WithFields(logrus.Fields{
"tx_hash": tx.Hash().Hex(),
"tx_hash": ethTx.Hash().Hex(),
"tx_to": erc20tx.to.Hex(),
"tx_amount": erc20tx.amount.String(),
"tx_token": erc20tx.token.Hex(),
Expand Down
7 changes: 3 additions & 4 deletions storage/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ type DatabaseStorage interface {
InsertPluginPolicyTx(ctx context.Context, dbTx pgx.Tx, policy vtypes.PluginPolicy) (*vtypes.PluginPolicy, error)
UpdatePluginPolicyTx(ctx context.Context, dbTx pgx.Tx, policy vtypes.PluginPolicy) (*vtypes.PluginPolicy, error)

CreateFeeBatch(ctx context.Context, tx pgx.Tx, batches ...types.FeeBatch) ([]types.FeeBatch, error)
SetFeeBatchTxHash(ctx context.Context, tx pgx.Tx, batchId uuid.UUID, txHash string) error
SetFeeBatchStatus(ctx context.Context, tx pgx.Tx, batchId uuid.UUID, status types.FeeBatchState) error
GetFeeBatch(ctx context.Context, batchIDs ...uuid.UUID) ([]types.FeeBatch, error)
GetFeeBatchByStatus(ctx context.Context, status types.FeeBatchState) ([]types.FeeBatch, error)
SetFeeBatchSent(ctx context.Context, txHash string, batchId uuid.UUID) error
CreateFeeBatch(ctx context.Context, tx pgx.Tx, batches ...types.FeeBatch) ([]types.FeeBatch, error)
SetFeeBatchStatus(ctx context.Context, tx pgx.Tx, batchId uuid.UUID, status types.FeeBatchState) error
SetFeeBatchSent(ctx context.Context, tx pgx.Tx, txHash string, batchId uuid.UUID) error

Pool() *pgxpool.Pool
}
4 changes: 2 additions & 2 deletions storage/postgres/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ func (p *PostgresBackend) GetFeeBatchByStatus(ctx context.Context, status types.
return feeBatches, nil
}

func (p *PostgresBackend) SetFeeBatchSent(ctx context.Context, txHash string, batchId uuid.UUID) error {
func (p *PostgresBackend) SetFeeBatchSent(ctx context.Context, tx pgx.Tx, txHash string, batchId uuid.UUID) error {
query := `update fee_batch set status = $1, tx_hash = $2 where batch_id = $3`
_, err := p.pool.Exec(ctx, query, types.FeeBatchStateSent, txHash, batchId)
_, err := tx.Exec(ctx, query, types.FeeBatchStateSent, txHash, batchId)
return err
}