Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.
Merged
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
4 changes: 3 additions & 1 deletion api/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ func (s *Server) DeletePluginPolicyById(c echo.Context) error {
return c.JSON(http.StatusInternalServerError, NewErrorResponse("failed to delete policy"))
}

return c.NoContent(http.StatusNoContent)
return c.JSON(http.StatusOK, map[string]interface{}{
"policy_id": policyID,
})
Comment thread
garry-sharp marked this conversation as resolved.
}

func (s *Server) GetPolicySchema(c echo.Context) error {
Expand Down
3 changes: 2 additions & 1 deletion fee.worker.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"local_party_prefix": "fee-plugin-0000",
"queue_email_task": false,
"encryption_secret": "test123"
"encryption_secret": "test123",
"do_setup_msg": true
},
"redis": {
"host": "redis-fees",
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ require (
github.com/stretchr/testify v1.10.0
github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778
github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74
github.com/vultisig/recipes v0.0.0-20250724185247-fde1a4c02314
github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf
github.com/vultisig/recipes v0.0.0-20250725112632-c546f5f66830
github.com/vultisig/verifier v0.0.0-20250725121724-4a7f10cc3689
github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b
golang.org/x/sync v0.14.0
google.golang.org/protobuf v1.36.6
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -761,8 +761,12 @@ github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070 h1:AcE6x2dxkl/4P/
github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070/go.mod h1:30NOW5y2BnMCmEYFeVls3NhxkbMvuv7BNyRQfoFhGIM=
github.com/vultisig/recipes v0.0.0-20250724185247-fde1a4c02314 h1:817gEvSIW4cNqmQxZIpwttnuzaw3RYIGbHFtIsm9Eyk=
github.com/vultisig/recipes v0.0.0-20250724185247-fde1a4c02314/go.mod h1:30NOW5y2BnMCmEYFeVls3NhxkbMvuv7BNyRQfoFhGIM=
github.com/vultisig/recipes v0.0.0-20250725112632-c546f5f66830 h1:r1Gw32G3yE1/w9Sxtq9fR53xixGu/r41JRl1HAqGGnY=
github.com/vultisig/recipes v0.0.0-20250725112632-c546f5f66830/go.mod h1:30NOW5y2BnMCmEYFeVls3NhxkbMvuv7BNyRQfoFhGIM=
github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf h1:3VhxFQR0oJEieaajNDvKlOGBDyTBbSMJSxow3zwUlmw=
github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf/go.mod h1:9f3yGSZWKZeXn30mU+/brufkbkLoGlFmNLKOpRbQgeI=
github.com/vultisig/verifier v0.0.0-20250725121724-4a7f10cc3689 h1:o/eXinXl4jgR2Km+zYP/TcmId+Za9rWzb9XvS2NX9Rk=
github.com/vultisig/verifier v0.0.0-20250725121724-4a7f10cc3689/go.mod h1:Q+Dp/lISXGVTNALJCpFTi+YhxsCd2UDQaA75IQkevOY=
github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b h1:Ed2DOWo8fA0KG6e36rzUmGpxcOQjmWTbxWyvUbI5by8=
github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b/go.mod h1:HwP2IgW6Mcu/gX8paFuKvfibrGE9UmPgkOFTub6dskM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
Expand Down
14 changes: 14 additions & 0 deletions plugin/fees/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fees
import (
"errors"
"fmt"
"math/big"
"slices"
"strings"

Expand Down Expand Up @@ -36,11 +37,13 @@ type FeeConfig struct {
MaxFeeAmount uint64 `mapstructure:"max_fee_amount"` // Policies that are created/submitted which do not have this amount will be rejected.
UsdcAddress string `mapstructure:"usdc_address"` // The address of the USDC token on the Ethereum blockchain.
VerifierToken string `mapstructure:"verifier_token"` // The token to use for the verifier API.
ChainId *big.Int `mapstructure:"chain_id"` // The chain ID of the Ethereum blockchain.
}

type ConfigOption func(*FeeConfig) error

func withDefaults(c *FeeConfig) {
c.ChainId = big.NewInt(1)
c.Type = PLUGIN_TYPE
c.Version = "1.0.0"
c.RpcURL = "https://ethereum.publicnode.com/"
Expand Down Expand Up @@ -77,6 +80,13 @@ func WithMaxFeeAmount(maxFeeAmount uint64) ConfigOption {
}
}

func WithChainId(chainId *big.Int) ConfigOption {
return func(c *FeeConfig) error {
c.ChainId = chainId
return nil
}
}

func WithFileConfig(basePath string) ConfigOption {
return func(c *FeeConfig) error {

Expand Down Expand Up @@ -141,6 +151,10 @@ func NewFeeConfig(fns ...ConfigOption) (*FeeConfig, error) {
return c, errors.New("verifier_token is required")
}

if c.ChainId == nil {
return c, errors.New("chain_id is required")
}

return c, nil
}

Expand Down
8 changes: 1 addition & 7 deletions plugin/fees/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,6 @@ func NewFeePlugin(db storage.DatabaseStorage,
return nil, err
}

// Initialize the Ethereum SDK for transaction broadcasting
ethEvmChainID, err := vcommon.Ethereum.EvmID()
if err != nil {
return nil, fmt.Errorf("vcommon.Ethereum.EvmID: %w", err)
}

if _, ok := logger.(*logrus.Logger); !ok {
return nil, fmt.Errorf("logger must be *logrus.Logger, got %T", logger)
}
Expand All @@ -98,7 +92,7 @@ func NewFeePlugin(db storage.DatabaseStorage,

return &FeePlugin{
db: db,
eth: evm.NewSDK(ethEvmChainID, rpcClient, rpcClient.Client()),
eth: evm.NewSDK(feeConfig.ChainId, rpcClient, rpcClient.Client()),
signer: signer,
logger: logger.WithField("plugin", "fees"),
config: feeConfig,
Expand Down
96 changes: 18 additions & 78 deletions plugin/fees/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@ package fees

import (
"context"
"encoding/hex"
"errors"
"crypto/sha256"
"encoding/base64"
"fmt"
"math/big"
"strings"
"time"

"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/vultisig/mobile-tss-lib/tss"
"github.com/vultisig/plugin/common"
"github.com/vultisig/plugin/internal/scheduler"
"github.com/vultisig/recipes/chain"
"github.com/vultisig/recipes/engine"
reth "github.com/vultisig/recipes/ethereum"
Expand All @@ -24,6 +21,8 @@ import (
vtypes "github.com/vultisig/verifier/types"

gcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
etypes "github.com/ethereum/go-ethereum/core/types"
)

func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.PluginKeysignRequest, error) {
Expand Down Expand Up @@ -89,30 +88,6 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P
}
amount := feeHistory.FeesPendingCollection

//Check if fees have been collected withing a 6 hour time window.
fromTime := time.Now().Add(-6 * time.Hour)
toTime := time.Now()

_, err = fp.txIndexerService.GetTxInTimeRange(
ctx,
chain,
policy.PluginID,
policy.ID,
usdc.Address,
recipient,
fromTime,
toTime,
)
if err == nil {
fp.logger.WithFields(logrus.Fields{
"recipient": recipient,
"amount": amount,
"chain_id": chain,
"token_id": usdc.Address,
}).Info("transaction already proposed, skipping")
return nil, nil
}

tx, err := fp.eth.MakeAnyTransfer(ctx,
gcommon.HexToAddress(ethAddress),
gcommon.HexToAddress(recipient),
Expand All @@ -122,7 +97,13 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P
return nil, fmt.Errorf("failed to generate unsigned transaction: %w", err)
}

txHex := hex.EncodeToString(tx)
txHex := hexutil.Encode(tx)

txData, e := reth.DecodeUnsignedPayload(tx)
if e != nil {
return nil, fmt.Errorf("ethereum.DecodeUnsignedPayload: %w", e)
}
txHashToSign := etypes.LatestSignerForChainID(fp.config.ChainId).Hash(etypes.NewTx(txData))

txToTrack, e := fp.txIndexerService.CreateTx(ctx, storage.CreateTxDto{
PluginID: policy.PluginID,
Expand All @@ -137,18 +118,18 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P
return nil, fmt.Errorf("error creating tx indexed transaction: %w", e)
}

// Create signing request
msgHash := sha256.Sum256(txHashToSign.Bytes())

signRequest := vtypes.PluginKeysignRequest{
KeysignRequest: vtypes.KeysignRequest{
PublicKey: policy.PublicKey,
Messages: []vtypes.KeysignMessage{
{
TxIndexerID: txToTrack.ID.String(),
Message: txHex,
Chain: vcommon.Ethereum,
// Doesn't make sense to compute hash with empty V,R,S,
// not on-chain hash without signature
Hash: txHex,
TxIndexerID: txToTrack.ID.String(),
Message: base64.StdEncoding.EncodeToString(txHashToSign.Bytes()),
Chain: vcommon.Ethereum,
Hash: base64.StdEncoding.EncodeToString(msgHash[:]),
HashFunction: vtypes.HashFunction_SHA256,
},
},
SessionID: "",
Expand All @@ -165,47 +146,6 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P
return txs, nil
}

// Copy from the payroll plugin. Checks if a tx was created
func (fp *FeePlugin) IsAlreadyProposed(
ctx context.Context,
frequency rtypes.ScheduleFrequency,
startTime time.Time,
interval int,
chainID vcommon.Chain,
pluginID vtypes.PluginID,
policyID uuid.UUID,
tokenID, recipientPublicKey string,
) (bool, error) {
sched, err := scheduler.NewIntervalSchedule(
frequency,
startTime,
interval,
)
if err != nil {
return false, fmt.Errorf("failed to create interval schedule: %w", err)
}

fromTime, toTime := sched.ToRangeFrom(time.Now())

_, err = fp.txIndexerService.GetTxInTimeRange(
ctx,
chainID,
pluginID,
policyID,
tokenID,
recipientPublicKey,
fromTime,
toTime,
)
if err == nil {
return true, nil
}
if errors.Is(err, storage.ErrNoTx) {
return false, nil
}
return false, fmt.Errorf("failed to get tx in time range: %w", err)
}

func (fp *FeePlugin) initSign(
ctx context.Context,
req vtypes.PluginKeysignRequest,
Expand Down
43 changes: 0 additions & 43 deletions plugin/payroll/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,18 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
"sync"
"time"

gcommon "github.com/ethereum/go-ethereum/common"
etypes "github.com/ethereum/go-ethereum/core/types"
"github.com/google/uuid"
"github.com/hibiken/asynq"
"github.com/sirupsen/logrus"
"github.com/vultisig/mobile-tss-lib/tss"
"github.com/vultisig/plugin/common"
"github.com/vultisig/plugin/internal/plugin"
"github.com/vultisig/plugin/internal/scheduler"
"github.com/vultisig/recipes/ethereum"
"github.com/vultisig/recipes/sdk/evm"
rtypes "github.com/vultisig/recipes/types"
Expand Down Expand Up @@ -104,46 +101,6 @@ func (p *Plugin) initSign(
return nil
}

func (p *Plugin) IsAlreadyProposed(
ctx context.Context,
frequency rtypes.ScheduleFrequency,
startTime time.Time,
interval int,
chainID vcommon.Chain,
pluginID vtypes.PluginID,
policyID uuid.UUID,
tokenID, recipientPublicKey string,
) (bool, error) {
sched, err := scheduler.NewIntervalSchedule(
frequency,
startTime,
interval,
)
if err != nil {
return false, fmt.Errorf("failed to create interval schedule: %w", err)
}

fromTime, toTime := sched.ToRangeFrom(time.Now())

_, err = p.txIndexerService.GetTxInTimeRange(
ctx,
chainID,
pluginID,
policyID,
tokenID,
recipientPublicKey,
fromTime,
toTime,
)
if err == nil {
return true, nil
}
if errors.Is(err, storage.ErrNoTx) {
return false, nil
}
return false, fmt.Errorf("p.txIndexerService.GetTxInTimeRange: %w", err)
}

func getTokenID(rule *rtypes.Rule) (string, error) {
if rule == nil {
return "", fmt.Errorf("rule is nil")
Expand Down
10 changes: 2 additions & 8 deletions storage/postgres/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,21 +159,15 @@ func (p *PostgresBackend) UpdatePluginPolicyTx(ctx context.Context, dbTx pgx.Tx,

func (p *PostgresBackend) DeletePluginPolicyTx(ctx context.Context, dbTx pgx.Tx, id uuid.UUID) error {
_, err := dbTx.Exec(ctx, `
DELETE FROM transaction_history
WHERE policy_id = $1
`, id)
if err != nil {
return fmt.Errorf("failed to delete transaction history: %w", err)
}
_, err = dbTx.Exec(ctx, `
DELETE FROM time_triggers
WHERE policy_id = $1
`, id)
if err != nil {
return fmt.Errorf("failed to delete time triggers: %w", err)
Comment thread
garry-sharp marked this conversation as resolved.
}
_, err = dbTx.Exec(ctx, `
DELETE FROM plugin_policies
UPDATE plugin_policies
SET deleted = true
WHERE id = $1
`, id)
if err != nil {
Expand Down
Loading