diff --git a/api/plugin.go b/api/plugin.go index a767383..54aed0e 100644 --- a/api/plugin.go +++ b/api/plugin.go @@ -60,7 +60,7 @@ func (s *Server) SignPluginMessages(c echo.Context) error { return fmt.Errorf("policy plugin ID mismatch") } - if err := s.plugin.ValidateProposedTransactions(policy.ToPluginPolicyCreateUpdate(), []vtypes.PluginKeysignRequest{req}); err != nil { + if err := s.plugin.ValidateProposedTransactions(policy, []vtypes.PluginKeysignRequest{req}); err != nil { return fmt.Errorf("failed to validate transaction proposal: %w", err) } @@ -192,7 +192,7 @@ func (s *Server) GetAllPluginPolicies(c echo.Context) error { } func (s *Server) CreatePluginPolicy(c echo.Context) error { - var policy vtypes.PluginPolicyCreateUpdate + var policy vtypes.PluginPolicy if err := c.Bind(&policy); err != nil { return fmt.Errorf("fail to parse request, err: %w", err) } @@ -208,12 +208,12 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { policy.ID = uuid.New() } - if !s.verifyPolicySignature(policy.ToPluginPolicy()) { + if !s.verifyPolicySignature(policy) { s.logger.Error("invalid policy signature") return c.JSON(http.StatusForbidden, NewErrorResponse("Invalid policy signature")) } - newPolicy, err := s.policyService.CreatePolicy(c.Request().Context(), policy.ToPluginPolicy()) + newPolicy, err := s.policyService.CreatePolicy(c.Request().Context(), policy) if err != nil { s.logger.WithError(err).Error("Failed to create plugin policy") return c.JSON(http.StatusInternalServerError, NewErrorResponse("failed to create policy")) @@ -223,7 +223,7 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { } func (s *Server) UpdatePluginPolicyById(c echo.Context) error { - var policy vtypes.PluginPolicyCreateUpdate + var policy vtypes.PluginPolicy if err := c.Bind(&policy); err != nil { return fmt.Errorf("fail to parse request, err: %w", err) } @@ -236,12 +236,12 @@ func (s *Server) UpdatePluginPolicyById(c echo.Context) error { return c.JSON(http.StatusBadRequest, NewErrorResponse("failed to validate policy")) } - if !s.verifyPolicySignature(policy.ToPluginPolicy()) { + if !s.verifyPolicySignature(policy) { s.logger.Error("invalid policy signature") return c.JSON(http.StatusForbidden, NewErrorResponse("Invalid policy signature")) } - updatedPolicy, err := s.policyService.UpdatePolicy(c.Request().Context(), policy.ToPluginPolicy()) + updatedPolicy, err := s.policyService.UpdatePolicy(c.Request().Context(), policy) if err != nil { s.logger.WithError(err).Error("Failed to update plugin policy") return c.JSON(http.StatusInternalServerError, NewErrorResponse("failed to update policy")) @@ -394,7 +394,7 @@ func policyToMessageHex(policy vtypes.PluginPolicy) ([]byte, error) { fields := []string{ policy.Recipe, policy.PublicKey, - policy.PolicyVersion, + fmt.Sprintf("%d", policy.PolicyVersion), policy.PluginVersion} for _, item := range fields { if strings.Contains(item, delimiter) { diff --git a/go.mod b/go.mod index 172d282..8036299 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,8 @@ require ( github.com/spf13/viper v1.20.1 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-20250606100120-0ee16a73412d - github.com/vultisig/verifier v0.0.0-20250611063307-6e5e760de529 + github.com/vultisig/recipes v0.0.0-20250609134859-0655e0445c1b + github.com/vultisig/verifier v0.0.0-20250612165949-9db8a6828606 github.com/vultisig/vultiserver v0.0.0-20250515110921-82d56d3d9cc9 ) diff --git a/go.sum b/go.sum index e679b9e..b0cfb47 100644 --- a/go.sum +++ b/go.sum @@ -745,10 +745,10 @@ github.com/vultisig/go-wrappers v0.0.0-20250403041248-86911e8aa33f h1:124Xlloih1 github.com/vultisig/go-wrappers v0.0.0-20250403041248-86911e8aa33f/go.mod h1:UfGCxUQW08kiwxyNBiHwXe+ePPuBmHVVS+BS51aU/Jg= github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 h1:goqwk4nQ/NEVIb3OPP9SUx7/u9ZfsUIcd5fIN/e4DVU= github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74/go.mod h1:nOykk4nOy1L3yXtLSlYvVsgizBnCQ3tR2N5uwGPdvaM= -github.com/vultisig/recipes v0.0.0-20250606100120-0ee16a73412d h1:BVc4OL0BNY/cCmptU3EeqAoxcX8vS13Pr13gWpd5Xi0= -github.com/vultisig/recipes v0.0.0-20250606100120-0ee16a73412d/go.mod h1:JT1FTsiJ8tY5W065vSsrxvrCSl1amp5o6SsBc1VVcCQ= -github.com/vultisig/verifier v0.0.0-20250611063307-6e5e760de529 h1:owEyLmbtlN1yL0xG7EhzXgvXbBZYHSHITh4P8lvPhyQ= -github.com/vultisig/verifier v0.0.0-20250611063307-6e5e760de529/go.mod h1:YpvZN2mbn6Z/34FDfnMS/TdE+B5Y8jghu5FvqOfRmB0= +github.com/vultisig/recipes v0.0.0-20250609134859-0655e0445c1b h1:rXhRaf40re1FdUYlYbCTEIO6JqIx8FfLlksksu8LiGE= +github.com/vultisig/recipes v0.0.0-20250609134859-0655e0445c1b/go.mod h1:JT1FTsiJ8tY5W065vSsrxvrCSl1amp5o6SsBc1VVcCQ= +github.com/vultisig/verifier v0.0.0-20250612165949-9db8a6828606 h1:DGia8ZQM3izAm+Oo/LM+5yWzENc9MIWtEfFCsCZOgO0= +github.com/vultisig/verifier v0.0.0-20250612165949-9db8a6828606/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A= 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= diff --git a/plugin/dca/dca.go b/plugin/dca/dca.go index 54cd8fd..7a6da3b 100644 --- a/plugin/dca/dca.go +++ b/plugin/dca/dca.go @@ -94,7 +94,7 @@ func (p *DCAPlugin) SigningComplete( ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, - policy vtypes.PluginPolicyCreateUpdate, + policy vtypes.PluginPolicy, ) error { var dcaPolicy DCAPolicy // TODO: convert recipe to DCAPolicy @@ -135,7 +135,7 @@ func (p *DCAPlugin) SigningComplete( return nil } -func (p *DCAPlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicyCreateUpdate) error { +func (p *DCAPlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicy) error { if policyDoc.PluginID != vtypes.PluginVultisigDCA_0000 { return fmt.Errorf("policy does not match plugin type, expected: %s, got: %s", pluginType, policyDoc.PluginID) } @@ -144,8 +144,8 @@ func (p *DCAPlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicyCreateUpda return fmt.Errorf("policy does not match plugin version, expected: %s, got: %s", pluginVersion, policyDoc.PluginVersion) } - if policyDoc.PolicyVersion != policyVersion { - return fmt.Errorf("policy does not match policy version, expected: %s, got: %s", policyVersion, policyDoc.PolicyVersion) + if fmt.Sprintf("%d", policyDoc.PolicyVersion) != policyVersion { //TODO policy version will essentially be a nonce that is incremented by 1, not, like plugin versions that will follow traditional engineering versioning e.g. v2.12.3. For now we can compare string types + return fmt.Errorf("policy does not match policy version, expected: %s, got: %d", policyVersion, policyDoc.PolicyVersion) } if policyDoc.PublicKey == "" { @@ -270,7 +270,7 @@ func validateInterval(intervalStr string, frequency string) error { return nil } -func (p *DCAPlugin) ProposeTransactions(policy vtypes.PluginPolicyCreateUpdate) ([]vtypes.PluginKeysignRequest, error) { +func (p *DCAPlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.PluginKeysignRequest, error) { p.logger.Info("DCA: PROPOSE TRANSACTIONS") var txs []vtypes.PluginKeysignRequest @@ -301,7 +301,7 @@ func (p *DCAPlugin) ProposeTransactions(policy vtypes.PluginPolicyCreateUpdate) } if completedSwaps >= totalOrders.Int64() { - if err := p.completePolicy(context.Background(), policy.ToPluginPolicy()); err != nil { + if err := p.completePolicy(context.Background(), policy); err != nil { return txs, fmt.Errorf("fail to complete policy: %w", err) } return txs, ErrCompletedPolicy @@ -358,7 +358,7 @@ func (p *DCAPlugin) ProposeTransactions(policy vtypes.PluginPolicyCreateUpdate) return txs, nil } -func (p *DCAPlugin) ValidateProposedTransactions(policy vtypes.PluginPolicyCreateUpdate, txs []vtypes.PluginKeysignRequest) error { +func (p *DCAPlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error { p.logger.Info("DCA: VALIDATE TRANSACTION PROPOSAL") if len(txs) == 0 { @@ -407,7 +407,7 @@ func (p *DCAPlugin) ValidateProposedTransactions(policy vtypes.PluginPolicyCreat } // TODO: Change this to make the policy to status COMPLETED if: completed swaps == total orders. if completedSwaps >= totalOrders.Int64() { - if err := p.completePolicy(context.Background(), policy.ToPluginPolicy()); err != nil { + if err := p.completePolicy(context.Background(), policy); err != nil { return fmt.Errorf("fail to complete policy: %w", err) } p.logger.Info("DCA: COMPLETED SWAPS: ", totalOrders.Int64()) diff --git a/plugin/fees/config.go b/plugin/fees/config.go new file mode 100644 index 0000000..e374f80 --- /dev/null +++ b/plugin/fees/config.go @@ -0,0 +1,63 @@ +package fees + +import ( + "errors" + "fmt" + "strings" + + "github.com/spf13/viper" +) + +type PluginConfig struct { + Type string `mapstructure:"type"` + Version string `mapstructure:"version"` + RpcURL string `mapstructure:"rpc_url"` // URL for the RPC endpoint to interact with the ethereum/evm blockchain + USDCAddress string `mapstructure:"usdc_address"` // If a non-ethereum chain is used, this is the address of the USDC token contract on that chain + Gas struct { + LimitMultiplier int `mapstructure:"limit_multiplier"` + PriceMultiplier int `mapstructure:"price_multiplier"` + } `mapstructure:"gas"` + Monitoring struct { + TimeoutMinutes int `mapstructure:"timeout_minutes"` + CheckIntervalSeconds int `mapstructure:"check_interval_seconds"` + } `mapstructure:"monitoring"` +} + +func loadPluginConfig(basePath string) (*PluginConfig, error) { + v := viper.New() + v.SetConfigName("fees") + + // Add config paths in order of precedence + if basePath != "" { + v.AddConfigPath(basePath) + } + v.AddConfigPath(".") + v.AddConfigPath("/etc/vultisig") + + // Enable environment variable overrides + v.AutomaticEnv() + v.SetEnvPrefix("FEES") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + if err := v.ReadInConfig(); err != nil { + return nil, fmt.Errorf("failed to read config: %w", err) + } + + var config PluginConfig + if err := v.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + // Validate configuration + if config.Type != PLUGIN_TYPE { + return nil, fmt.Errorf("invalid plugin type: %s", config.Type) + } + if config.RpcURL == "" { + return nil, errors.New("rpc_url is required") + } + if config.Gas.LimitMultiplier <= 0 { + return nil, errors.New("gas limit multiplier must be positive") + } + + return &config, nil +} diff --git a/plugin/fees/constraints.go b/plugin/fees/constraints.go new file mode 100644 index 0000000..f8aa3e5 --- /dev/null +++ b/plugin/fees/constraints.go @@ -0,0 +1,12 @@ +package fees + +const PLUGIN_TYPE = "fees" +const erc20ABI = `[{ + "name": "transfer", + "type": "function", + "inputs": [ + {"name": "recipient", "type": "address"}, + {"name": "amount", "type": "uint256"} + ], + "outputs": [{"name": "", "type": "bool"}] +}]` diff --git a/plugin/fees/fees.go b/plugin/fees/fees.go new file mode 100644 index 0000000..b996742 --- /dev/null +++ b/plugin/fees/fees.go @@ -0,0 +1,68 @@ +package fees + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/ethclient" + "github.com/sirupsen/logrus" + "github.com/vultisig/mobile-tss-lib/tss" + "github.com/vultisig/verifier/plugin" + "github.com/vultisig/verifier/types" + + "github.com/vultisig/plugin/storage" + rtypes "github.com/vultisig/recipes/types" +) + +var _ plugin.Plugin = (*FeePlugin)(nil) + +type FeePlugin struct { + db storage.DatabaseStorage + rpcClient *ethclient.Client + logger logrus.FieldLogger + config *PluginConfig +} + +func NewFeePlugin(db storage.DatabaseStorage, baseConfigPath string) (*FeePlugin, error) { + if db == nil { + return nil, fmt.Errorf("database storage cannot be nil") + } + cfg, err := loadPluginConfig(baseConfigPath) + if err != nil { + return nil, fmt.Errorf("failed to load plugin config: %w", err) + } + + rpcClient, err := ethclient.Dial(cfg.RpcURL) + if err != nil { + return nil, err + } + + return &FeePlugin{ + db: db, + rpcClient: rpcClient, + logger: logrus.WithField("plugin", "payroll"), + config: cfg, + }, nil +} + +func (fp *FeePlugin) GetRecipeSpecification() rtypes.RecipeSchema { + fp.logger.Debug("Getting recipe specification") + //TODO garry + return rtypes.RecipeSchema{ + PluginId: "vultisig-fees-0000", + Version: 1, + } +} + +func (fp *FeePlugin) ValidatePluginPolicy(policyDoc types.PluginPolicy) error { + return nil +} +func (fp *FeePlugin) ProposeTransactions(policy types.PluginPolicy) ([]types.PluginKeysignRequest, error) { + return []types.PluginKeysignRequest{}, nil +} +func (fp *FeePlugin) ValidateProposedTransactions(policy types.PluginPolicy, txs []types.PluginKeysignRequest) error { + return nil +} +func (fp *FeePlugin) SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest types.PluginKeysignRequest, policy types.PluginPolicy) error { + return nil +} diff --git a/plugin/payroll/policy.go b/plugin/payroll/policy.go index 70bc2c9..fca7b3b 100644 --- a/plugin/payroll/policy.go +++ b/plugin/payroll/policy.go @@ -37,7 +37,7 @@ type Schedule struct { EndTime string `json:"end_time,omitempty"` } -func (p *PayrollPlugin) ValidateProposedTransactions(policy vtypes.PluginPolicyCreateUpdate, txs []vtypes.PluginKeysignRequest) error { +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) @@ -196,7 +196,7 @@ func (p *PayrollPlugin) checkRule(rule *rtypes.Rule) error { } return nil } -func (p *PayrollPlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicyCreateUpdate) error { +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) } diff --git a/plugin/payroll/transaction.go b/plugin/payroll/transaction.go index 615063d..e87225c 100644 --- a/plugin/payroll/transaction.go +++ b/plugin/payroll/transaction.go @@ -52,7 +52,7 @@ func (p *PayrollPlugin) HandleSchedulerTrigger(ctx context.Context, t *asynq.Tas _ = pluginPolicy return nil } -func (p *PayrollPlugin) ProposeTransactions(policy vtypes.PluginPolicyCreateUpdate) ([]vtypes.PluginKeysignRequest, error) { +func (p *PayrollPlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.PluginKeysignRequest, error) { var txs []vtypes.PluginKeysignRequest err := p.ValidatePluginPolicy(policy) if err != nil { @@ -232,8 +232,8 @@ func (p *PayrollPlugin) generatePayrollTransaction(amountString, recipientString return txHash, rawTx, nil } -func (p *PayrollPlugin) SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, policy vtypes.PluginPolicyCreateUpdate) error { - R, S, V, originalTx, chainID, _, err := p.convertData(signature, signRequest, policy.ToPluginPolicy()) +func (p *PayrollPlugin) SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, policy vtypes.PluginPolicy) error { + R, S, V, originalTx, chainID, _, err := p.convertData(signature, signRequest, policy) if err != nil { return fmt.Errorf("failed to convert R and S: %v", err) } diff --git a/scripts/dev/create_dca_policy/main.go b/scripts/dev/create_dca_policy/main.go index febf47e..dc912c1 100644 --- a/scripts/dev/create_dca_policy/main.go +++ b/scripts/dev/create_dca_policy/main.go @@ -75,7 +75,7 @@ func main() { PublicKey: key, PluginID: vtypes.PluginVultisigDCA_0000, PluginVersion: "1.0.0", - PolicyVersion: "1.0.0", + PolicyVersion: 1, Active: true, Signature: "0x0000000000000000000000000000000000000000000000000000000000000000", } diff --git a/scripts/dev/create_payroll_policy/main.go b/scripts/dev/create_payroll_policy/main.go index 8e24fa7..d5af2a5 100644 --- a/scripts/dev/create_payroll_policy/main.go +++ b/scripts/dev/create_payroll_policy/main.go @@ -99,7 +99,7 @@ func main() { PublicKey: key, PluginID: vtypes.PluginVultisigPayroll_0000, PluginVersion: "1.0.0", - PolicyVersion: "1.0.0", + PolicyVersion: 1, Active: true, Signature: "0x0000000000000000000000000000000000000000000000000000000000000000", } diff --git a/storage/postgres/time_trigger_test.go b/storage/postgres/time_trigger_test.go index ad4e63b..2059a00 100644 --- a/storage/postgres/time_trigger_test.go +++ b/storage/postgres/time_trigger_test.go @@ -43,7 +43,7 @@ func TestTimeTrigger(t *testing.T) { PublicKey: "4a7b9c2f8e1d3a5b6c4f9e2d7a1b3c5f8d4e6a2b9c1f7e3d5a6b4c2f8e1d3a5b", PluginID: "vultisig-payroll-0000", PluginVersion: "1", - PolicyVersion: "1", + PolicyVersion: 1, Signature: "whatever", Recipe: "k3uaL47TpWtsT54tfhO7K6Gbyxf4H71jWma0zC2e3kQ=", Active: true,