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
1 change: 1 addition & 0 deletions cmd/payroll/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type PayrollServerConfig struct {
Host string `mapstructure:"host" json:"host,omitempty"`
Port string `mapstructure:"port" json:"port,omitempty"`
} `mapstructure:"datadog" json:"datadog"`
EncryptionSecret string `mapstructure:"encryption_secret" json:"encryption_secret,omitempty"`
}

func GetConfigure() (*PayrollServerConfig, error) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/payroll/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func main() {
if err != nil {
logger.Fatalf("Failed to connect to database: %v", err)
}
p, err := payroll.NewPayrollPlugin(db, cfg.BaseConfigPath, txIndexerService, client, inspector)
p, err := payroll.NewPayrollPlugin(db, vaultStorage, cfg.BaseConfigPath, txIndexerService, client, inspector, cfg.EncryptionSecret)
if err != nil {
logger.Fatalf("failed to create payroll plugin,err: %s", err)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/payroll/worker/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ type PayrollWorkerConfig struct {
Database struct {
DSN string `mapstructure:"dsn" json:"dsn,omitempty"`
} `mapstructure:"database" json:"database,omitempty"`
BaseConfigPath string `mapstructure:"base_file_path" json:"base_file_path,omitempty"`
BaseConfigPath string `mapstructure:"base_file_path" json:"base_file_path,omitempty"`
EncryptionSecret string `mapstructure:"encryption_secret" json:"encryption_secret,omitempty"`
}

func GetConfigure() (*PayrollWorkerConfig, error) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/payroll/worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func main() {
if err != nil {
panic(fmt.Errorf("failed to create postgres backend: %w", err))
}
p, err := payroll.NewPayrollPlugin(postgressDB, cfg.BaseConfigPath, txIndexerService, client, inspector)
p, err := payroll.NewPayrollPlugin(postgressDB, vaultStorage, cfg.BaseConfigPath, txIndexerService, client, inspector, cfg.EncryptionSecret)
if err != nil {
panic(fmt.Errorf("failed to create payroll plugin: %w", err))
}
Expand Down
24 changes: 24 additions & 0 deletions common/vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package common

import (
"fmt"

v1 "github.com/vultisig/commondata/go/vultisig/vault/v1"
vcommon "github.com/vultisig/verifier/common"
vtypes "github.com/vultisig/verifier/types"
vault "github.com/vultisig/verifier/vault"
)

func GetVaultFromPolicy(s vault.Storage, policy vtypes.PluginPolicy, encryptionSecret string) (*v1.Vault, error) {
vaultFileName := vcommon.GetVaultBackupFilename(policy.PublicKey, policy.PluginID.String())
vaultContent, err := s.GetVault(vaultFileName)
if err != nil {
return nil, fmt.Errorf("failed to get vault")
}

if vaultContent == nil {
return nil, fmt.Errorf("vault not found")
}

return vcommon.DecryptVaultFromBackup(encryptionSecret, vaultContent)
}
Comment on lines +12 to +24

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

Improve error messages to include more context.

The error messages should wrap the original errors and provide more context for better debugging.

Apply this diff to improve error handling:

 func GetVaultFromPolicy(s vault.Storage, policy vtypes.PluginPolicy, encryptionSecret string) (*v1.Vault, error) {
 	vaultFileName := vcommon.GetVaultBackupFilename(policy.PublicKey, policy.PluginID.String())
 	vaultContent, err := s.GetVault(vaultFileName)
 	if err != nil {
-		return nil, fmt.Errorf("failed to get vault")
+		return nil, fmt.Errorf("failed to get vault %s: %w", vaultFileName, err)
 	}
 
 	if vaultContent == nil {
-		return nil, fmt.Errorf("vault not found")
+		return nil, fmt.Errorf("vault not found for file %s", vaultFileName)
 	}
 
 	return vcommon.DecryptVaultFromBackup(encryptionSecret, vaultContent)
 }
🤖 Prompt for AI Agents
In common/vault.go around lines 12 to 24, the error messages returned when
failing to get the vault or when the vault is not found lack context and do not
include the original error details. Modify the error handling to wrap the
original error using fmt.Errorf with the %w verb and add descriptive context
messages, so the returned errors provide more information for debugging.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/vultisig/commondata v0.0.0-20250430024109-a2492623ef05
github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74
github.com/vultisig/recipes v0.0.0-20250609134859-0655e0445c1b
github.com/vultisig/verifier v0.0.0-20250617205725-01cd5bd5b68b
github.com/vultisig/verifier v0.0.0-20250620085341-b935ecc82e40
github.com/vultisig/vultiserver v0.0.0-20250515110921-82d56d3d9cc9
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,8 @@ github.com/vultisig/verifier v0.0.0-20250617153835-9a8d53c93da1 h1:iTB/XLdeidd5v
github.com/vultisig/verifier v0.0.0-20250617153835-9a8d53c93da1/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A=
github.com/vultisig/verifier v0.0.0-20250617205725-01cd5bd5b68b h1:y02mtgV2eNRrxFBXj/8yzaLK1/j8FNNoTXxbjC4Dec4=
github.com/vultisig/verifier v0.0.0-20250617205725-01cd5bd5b68b/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A=
github.com/vultisig/verifier v0.0.0-20250620085341-b935ecc82e40 h1:SXFcpcq7HGCmveEg2fV+lXSrfVnyc/qxGrE1dwaEInU=
github.com/vultisig/verifier v0.0.0-20250620085341-b935ecc82e40/go.mod h1:jAepDQtls7VT2r4lb6J6Asl5xCSwt6mg/b8XBO3uzsA=
github.com/vultisig/vultiserver v0.0.0-20250515110921-82d56d3d9cc9 h1:pZhGN8q8+gPB1JJjVDC1hDg8qn6Tbj0XBJymgTQ8qQg=
github.com/vultisig/vultiserver v0.0.0-20250515110921-82d56d3d9cc9/go.mod h1:HwP2IgW6Mcu/gX8paFuKvfibrGE9UmPgkOFTub6dskM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
Expand Down
7 changes: 7 additions & 0 deletions plugin/payroll/payroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/vultisig/plugin/storage"
"github.com/vultisig/verifier/plugin"
"github.com/vultisig/verifier/tx_indexer"
"github.com/vultisig/verifier/vault"
)

var _ plugin.Plugin = (*PayrollPlugin)(nil)
Expand All @@ -22,14 +23,18 @@ type PayrollPlugin struct {
txIndexerService *tx_indexer.Service
client *asynq.Client
inspector *asynq.Inspector
vaultStorage vault.Storage
encryptionSecret string
}

func NewPayrollPlugin(
db storage.DatabaseStorage,
vaultStorage vault.Storage,
baseConfigPath string,
txIndexerService *tx_indexer.Service,
client *asynq.Client,
inspector *asynq.Inspector,
encryptionSecret string,
) (*PayrollPlugin, error) {
if db == nil {
return nil, fmt.Errorf("database storage cannot be nil")
Expand All @@ -53,6 +58,8 @@ func NewPayrollPlugin(
txIndexerService: txIndexerService,
client: client,
inspector: inspector,
vaultStorage: vaultStorage,
encryptionSecret: encryptionSecret,
}, nil
}

Expand Down
129 changes: 24 additions & 105 deletions plugin/payroll/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,51 @@ package payroll

import (
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"strconv"
"strings"
"time"

"github.com/ethereum/go-ethereum/accounts/abi"
gcommon "github.com/ethereum/go-ethereum/common"
gtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/golang/protobuf/proto"
"github.com/vultisig/recipes/chain"
"github.com/vultisig/recipes/engine"
rtypes "github.com/vultisig/recipes/types"
vtypes "github.com/vultisig/verifier/types"
)

type PayrollPolicy struct {
ChainID []string `json:"chain_id"`
TokenID []string `json:"token_id"`
Recipients []PayrollRecipient `json:"recipients"`
Schedule Schedule `json:"schedule"`
}

type PayrollRecipient struct {
Address string `json:"address"`
Amount string `json:"amount"`
}

// This is duplicated between DCA and Payroll to avoid a
// circular top-level dependency on the types package
type Schedule struct {
Frequency string `json:"frequency"`
Interval string `json:"interval"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time,omitempty"`
}

type ScheduleTyped struct {
Frequency rtypes.ScheduleFrequency
Interval int
StartTime time.Time
}

func (s Schedule) Typed() (ScheduleTyped, error) {
frequency, ok := rtypes.ScheduleFrequency_value[s.Frequency]
if !ok {
return ScheduleTyped{}, fmt.Errorf("unknown schedule frequency: %s", s.Frequency)
}

interval, err := strconv.Atoi(s.Interval)
if err != nil {
return ScheduleTyped{}, fmt.Errorf("strconv.Atoi(%s): %w", s.Interval, err)
}

startTime, err := time.Parse(time.RFC3339, s.StartTime)
if err != nil {
return ScheduleTyped{}, fmt.Errorf("time.Parse(%s): %w", s.StartTime, err)
}

return ScheduleTyped{
Frequency: rtypes.ScheduleFrequency(frequency),
Interval: interval,
StartTime: startTime,
}, nil
}

func (p *PayrollPlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error {
err := p.ValidatePluginPolicy(policy)
if err != nil {
return fmt.Errorf("failed to validate plugin policy: %v", err)
}

parsedABI, err := abi.JSON(strings.NewReader(erc20ABI))
recipe, err := policy.GetRecipe()
if err != nil {
return fmt.Errorf("failed to parse ABI: %v", err)
return fmt.Errorf("failed to get recipe from policy: %v", err)
}
Comment on lines +23 to 26

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

Wrap the original error for better debugging.

The error message should include the original error from GetRecipe.

 recipe, err := policy.GetRecipe()
 if err != nil {
-	return fmt.Errorf("failed to get recipe from policy: %v", err)
+	return fmt.Errorf("failed to get recipe from policy: %w", err)
 }
📝 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
recipe, err := policy.GetRecipe()
if err != nil {
return fmt.Errorf("failed to parse ABI: %v", err)
return fmt.Errorf("failed to get recipe from policy: %v", err)
}
recipe, err := policy.GetRecipe()
if err != nil {
return fmt.Errorf("failed to get recipe from policy: %w", err)
}
🤖 Prompt for AI Agents
In plugin/payroll/policy.go around lines 23 to 26, the error returned from
GetRecipe is formatted as a new error string without wrapping the original
error. Update the return statement to wrap the original error using fmt.Errorf
with the %w verb instead of %v, so the original error is preserved and can be
unwrapped for better debugging.


var payrollPolicy PayrollPolicy
// TODO: convert the recipes to PayrollPolicy
for i, tx := range txs {
var parsedTx *gtypes.Transaction
txBytes, err := hex.DecodeString(tx.Transaction)
if err != nil {
return fmt.Errorf("failed to decode transaction: %v", err)
}
eng := engine.NewEngine()

err = rlp.DecodeBytes(txBytes, &parsedTx)
if err != nil {
return fmt.Errorf("failed to parse transaction: %v", err)
}

txDestination := parsedTx.To()
if txDestination == nil {
return fmt.Errorf("transaction destination is nil")
}

if strings.ToLower(txDestination.Hex()) != strings.ToLower(payrollPolicy.TokenID[i]) {
// ERC20 transfer tx 'to' is contract address, and 'recipient' encoded in calldata
return fmt.Errorf("transaction destination does not match token ID")
}

txData := parsedTx.Data()
m, err := parsedABI.MethodById(txData[:4])
if err != nil {
return fmt.Errorf("failed to get method by ID: %v", err)
}

v := make(map[string]interface{})
if err := m.Inputs.UnpackIntoMap(v, txData[4:]); err != nil {
return fmt.Errorf("failed to unpack transaction data: %v", err)
}

fmt.Printf("Decoded: %+v\n", v)
for _, tx := range txs {
for _, keysignMessage := range tx.Messages {
messageChain, err := chain.GetChain(strings.ToLower(keysignMessage.Chain.String()))
if err != nil {
return fmt.Errorf("failed to get chain: %w", err)
}

recipientAddress, ok := v["recipient"].(gcommon.Address)
if !ok {
return fmt.Errorf("failed to get recipient address")
}
decodedTx, err := messageChain.ParseTransaction(keysignMessage.Message)
if err != nil {
return fmt.Errorf("failed to parse transaction: %w", err)
}

var recipientFound bool
for _, recipient := range payrollPolicy.Recipients {
if strings.EqualFold(recipientAddress.Hex(), recipient.Address) {
recipientFound = true
break
transactionAllowed, _, err := eng.Evaluate(recipe, messageChain, decodedTx)
if err != nil {
return fmt.Errorf("failed to evaluate transaction: %w", err)
}
}

if !recipientFound {
return fmt.Errorf("recipient not found in policy")
if !transactionAllowed {
return fmt.Errorf("transaction %s on %s not allowed by policy", keysignMessage.Hash, keysignMessage.Chain)
}
}
}

Expand Down Expand Up @@ -184,6 +100,7 @@ func (p *PayrollPlugin) validateAmount(pc *rtypes.ParameterConstraint) error {
}
return nil
}

func (p *PayrollPlugin) validateSchedule(schedule *rtypes.Schedule) error {
if schedule == nil {
return fmt.Errorf("schedule is nil")
Expand All @@ -202,6 +119,7 @@ func (p *PayrollPlugin) validateSchedule(schedule *rtypes.Schedule) error {

return nil
}

func (p *PayrollPlugin) checkRule(rule *rtypes.Rule) error {
if rule.Effect != rtypes.Effect_EFFECT_ALLOW {
return fmt.Errorf("rule effect must be ALLOW, got: %s", rule.Effect)
Expand All @@ -228,6 +146,7 @@ func (p *PayrollPlugin) checkRule(rule *rtypes.Rule) error {
}
return nil
}

func (p *PayrollPlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicy) error {
if policyDoc.PluginID != vtypes.PluginVultisigPayroll_0000 {
return fmt.Errorf("policy does not match plugin type, expected: %s, got: %s", vtypes.PluginVultisigPayroll_0000, policyDoc.PluginID)
Expand Down
Loading