diff --git a/go.mod b/go.mod index d814fb5..757abaa 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/eager7/dogd v0.0.0-20200427085516-2caf59f59dbb github.com/ethereum/go-ethereum v1.15.11 github.com/go-playground/validator/v10 v10.26.0 - github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.25.1 github.com/jackc/pgx/v5 v5.7.4 @@ -17,11 +16,14 @@ require ( github.com/redis/go-redis/v9 v9.8.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.10.0 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-20250627044941-5f6e07d5d22f - github.com/vultisig/verifier v0.0.0-20250701180729-848563b4ed33 + github.com/vultisig/recipes v0.0.0-20250710152947-d15b238437ae + github.com/vultisig/verifier v0.0.0-20250710201755-2a994bd24c91 github.com/vultisig/vultiserver v0.0.0-20250515110921-82d56d3d9cc9 + golang.org/x/sync v0.12.0 + google.golang.org/protobuf v1.36.6 ) require ( @@ -85,6 +87,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/golang/glog v1.2.4 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -127,7 +130,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.14 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect @@ -136,11 +139,9 @@ require ( github.com/vultisig/go-wrappers v0.0.0-20250403041248-86911e8aa33f // indirect go.etcd.io/bbolt v1.3.8 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/sync v0.12.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect lukechampine.com/blake3 v1.2.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/go.sum b/go.sum index e879a58..455dcb7 100644 --- a/go.sum +++ b/go.sum @@ -745,32 +745,12 @@ 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-20250609134859-0655e0445c1b h1:rXhRaf40re1FdUYlYbCTEIO6JqIx8FfLlksksu8LiGE= -github.com/vultisig/recipes v0.0.0-20250609134859-0655e0445c1b/go.mod h1:JT1FTsiJ8tY5W065vSsrxvrCSl1amp5o6SsBc1VVcCQ= -github.com/vultisig/recipes v0.0.0-20250627044941-5f6e07d5d22f h1:1OaeEJ1zER9pJ8IFh674C1dpZwWhIiTV5nd729dHMzU= -github.com/vultisig/recipes v0.0.0-20250627044941-5f6e07d5d22f/go.mod h1:9ucuiGHbjFTekXNwUc9M+OQSm38P56Gr0yGNdMdE+i0= -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/verifier v0.0.0-20250614014346-e5348c16754b h1:8DFobGSwdD01Zzr8lCWW1UWGHqbd/EpwyrfhEy81xTI= -github.com/vultisig/verifier v0.0.0-20250614014346-e5348c16754b/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A= -github.com/vultisig/verifier v0.0.0-20250616174756-6ca7c9fac2d8 h1:MZBy2ame3ipaH/0V4TczTqIfW0s+gwtOsX1VSOB/5m4= -github.com/vultisig/verifier v0.0.0-20250616174756-6ca7c9fac2d8/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A= -github.com/vultisig/verifier v0.0.0-20250616181219-f7ba9f0dfa97 h1:uoeWKNKeTqEFDbqBrD9qeVKYWvzPy8kN/sA4VSNv80k= -github.com/vultisig/verifier v0.0.0-20250616181219-f7ba9f0dfa97/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A= -github.com/vultisig/verifier v0.0.0-20250616191510-cc66448469ef h1:VNDOwFI4eGJn76dP/lx3x+hoyPc/vzcZ2+fB6Ap8h1M= -github.com/vultisig/verifier v0.0.0-20250616191510-cc66448469ef/go.mod h1:35VQIKhChb6/H+3J/XkbGhPkVF6lyLuxsUG/WXHS4+A= -github.com/vultisig/verifier v0.0.0-20250617153835-9a8d53c93da1 h1:iTB/XLdeidd5v0QmQJp+oLjPP1OHmK1yiT/Ps6QB7hA= -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/verifier v0.0.0-20250626093402-fa1ecf8bd816 h1:hCTbtaqIKBsnRarJxccgcoi+DrpuNFyHspWb3PSWp40= -github.com/vultisig/verifier v0.0.0-20250626093402-fa1ecf8bd816/go.mod h1:dLmQBnF5F0auFkGtCMHOCqkUwdTRL6fWm3H4yfHfsVU= -github.com/vultisig/verifier v0.0.0-20250627125920-a555ee8da0cb h1:x2h4ud358pS5jQ5ZEVAu6Jazc2OzDrLa1E8rtdFUEFg= -github.com/vultisig/verifier v0.0.0-20250627125920-a555ee8da0cb/go.mod h1:dLmQBnF5F0auFkGtCMHOCqkUwdTRL6fWm3H4yfHfsVU= +github.com/vultisig/recipes v0.0.0-20250710152947-d15b238437ae h1:Per1QtZamO2PiQLxzTJP5SNjg6INtn83IG0c0585/s8= +github.com/vultisig/recipes v0.0.0-20250710152947-d15b238437ae/go.mod h1:Ci29kT+x5vPxLCRvIrbVNzM+JIMjDbVvvaLTzli5kYQ= github.com/vultisig/verifier v0.0.0-20250701180729-848563b4ed33 h1:PUeuOyOphzJq/NhCOwMJjqm8YC2U5v55jQtg7Fuf5t4= github.com/vultisig/verifier v0.0.0-20250701180729-848563b4ed33/go.mod h1:dLmQBnF5F0auFkGtCMHOCqkUwdTRL6fWm3H4yfHfsVU= +github.com/vultisig/verifier v0.0.0-20250710201755-2a994bd24c91 h1:P9er37oz5iGogJOB5SoWAFVoLajR7rCl/kKvqU8Hkf4= +github.com/vultisig/verifier v0.0.0-20250710201755-2a994bd24c91/go.mod h1:Qjt7mLLM992hDnjOYSrSuykAULeJUFMbD6o071EExRU= 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/internal/plugin/plugin.go b/internal/plugin/plugin.go new file mode 100644 index 0000000..3e37636 --- /dev/null +++ b/internal/plugin/plugin.go @@ -0,0 +1,30 @@ +package plugin + +import ( + "encoding/base64" + "fmt" + + "github.com/vultisig/recipes/engine" + rtypes "github.com/vultisig/recipes/types" + vtypes "github.com/vultisig/verifier/types" + "google.golang.org/protobuf/proto" +) + +func ValidatePluginPolicy(policyDoc vtypes.PluginPolicy, spec *rtypes.RecipeSchema) error { + policyBytes, err := base64.StdEncoding.DecodeString(policyDoc.Recipe) + if err != nil { + return fmt.Errorf("failed to decode policy recipe: %w", err) + } + + var rPolicy rtypes.Policy + err = proto.Unmarshal(policyBytes, &rPolicy) + if err != nil { + return fmt.Errorf("failed to unmarshal policy: %w", err) + } + + err = engine.NewEngine().ValidatePolicyWithSchema(&rPolicy, spec) + if err != nil { + return fmt.Errorf("failed to validate policy: %w", err) + } + return nil +} diff --git a/plugin/dca/dca.go b/plugin/dca/dca.go index b2d2ab0..8b5e89f 100644 --- a/plugin/dca/dca.go +++ b/plugin/dca/dca.go @@ -774,8 +774,8 @@ func (p *DCAPlugin) completePolicy(ctx context.Context, policy vtypes.PluginPoli return nil } -func (p *DCAPlugin) GetRecipeSpecification() rtypes.RecipeSchema { - return rtypes.RecipeSchema{ +func (p *DCAPlugin) GetRecipeSpecification() *rtypes.RecipeSchema { + return &rtypes.RecipeSchema{ Version: 1, // Schema version ScheduleVersion: 1, // Schedule specification version PluginId: string(vtypes.PluginVultisigDCA_0000), diff --git a/plugin/fees/policy.go b/plugin/fees/policy.go index 1ef3e43..139f364 100644 --- a/plugin/fees/policy.go +++ b/plugin/fees/policy.go @@ -1,13 +1,9 @@ package fees import ( - "encoding/base64" - "fmt" - "slices" - + "github.com/vultisig/plugin/internal/plugin" rtypes "github.com/vultisig/recipes/types" vtypes "github.com/vultisig/verifier/types" - "google.golang.org/protobuf/proto" ) /* @@ -20,63 +16,14 @@ import ( */ func (fp *FeePlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicy) error { - if policyDoc.PluginID != vtypes.PluginVultisigFees_feee { - return fmt.Errorf("policy does not match plugin type, expected: %s, got: %s", vtypes.PluginVultisigFees_feee, policyDoc.PluginID) - } - var rPolicy rtypes.Policy - policyBytes, err := base64.StdEncoding.DecodeString(policyDoc.Recipe) - if err != nil { - return fmt.Errorf("failed to decode policy recipe: %w", err) - } - if err := proto.Unmarshal(policyBytes, &rPolicy); err != nil { - return fmt.Errorf("failed to unmarshal policy: %w", err) - } - - if len(rPolicy.Rules) == 0 { - return fmt.Errorf("no rules") - } - if len(rPolicy.Rules) > 1 { - return fmt.Errorf("only one rule is allowed for the fee plugin") - } - - rule := rPolicy.Rules[0] - if rule.Id != "allow-usdc-transfer-to-collector" { - return fmt.Errorf("rule id must be allow-usdc-transfer-to-collector") - } - if rule.Resource != "ethereum.usdc.transfer" { - return fmt.Errorf("rule resource must be ethereum.usdc.transfer") - } - - // Validate that recipient address is in the whitelist - var recipient string - for _, constraint := range rule.ParameterConstraints { - if constraint.ParameterName == "recipient" { - if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED { - return fmt.Errorf("recipient constraint must be a fixed value") - } - fixedValue := constraint.Constraint.GetValue().(*rtypes.Constraint_FixedValue) - recipient = fixedValue.FixedValue - break - } - } - - if recipient == "" { - return fmt.Errorf("recipient parameter constraint is required") - } - - // Check if recipient is in the whitelist - if !slices.Contains(fp.config.CollectorWhitelistAddresses, recipient) { - return fmt.Errorf("recipient address %s is not in the whitelist: %v", recipient, fp.config.CollectorWhitelistAddresses) - } - - return nil + return plugin.ValidatePluginPolicy(policyDoc, fp.GetRecipeSpecification()) } -func (fp FeePlugin) GetRecipeSpecification() rtypes.RecipeSchema { - return rtypes.RecipeSchema{ +func (fp *FeePlugin) GetRecipeSpecification() *rtypes.RecipeSchema { + return &rtypes.RecipeSchema{ Version: 1, // Schema version ScheduleVersion: 1, // Schedule specification version - PluginId: string(vtypes.PluginVultisigFees_feee.String()), + PluginId: vtypes.PluginVultisigFees_feee.String(), PluginName: "Fee Plugin", PluginVersion: 1, SupportedResources: []*rtypes.ResourcePattern{ diff --git a/plugin/payroll/constants.go b/plugin/payroll/constants.go index 3b90dc5..f432cb3 100644 --- a/plugin/payroll/constants.go +++ b/plugin/payroll/constants.go @@ -1,12 +1,3 @@ package payroll const PLUGIN_TYPE = "payroll" -const erc20ABI = `[{ - "name": "transfer", - "type": "function", - "inputs": [ - {"name": "recipient", "type": "address"}, - {"name": "amount", "type": "uint256"} - ], - "outputs": [{"name": "", "type": "bool"}] -}]` diff --git a/plugin/payroll/nonce.go b/plugin/payroll/nonce.go deleted file mode 100644 index d070304..0000000 --- a/plugin/payroll/nonce.go +++ /dev/null @@ -1,37 +0,0 @@ -package payroll - -import ( - "context" - "fmt" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" -) - -type NonceManager struct { - rpcClient *ethclient.Client - nonceMap sync.Map - mu sync.Mutex -} - -func NewNonceManager(rpcClient *ethclient.Client) *NonceManager { - return &NonceManager{ - rpcClient: rpcClient, - } -} - -func (n *NonceManager) GetNextNonce(address string) (uint64, error) { - n.mu.Lock() - defer n.mu.Unlock() - - nonce, err := n.rpcClient.PendingNonceAt(context.Background(), common.HexToAddress(address)) - if err != nil { - return 0, fmt.Errorf("failed to get nonce from network: %w", err) - } - return nonce, nil -} - -func (n *NonceManager) ResetNonce(address string) { - n.nonceMap.Delete(address) -} diff --git a/plugin/payroll/policy.go b/plugin/payroll/policy.go index 2ac7b6e..a60c875 100644 --- a/plugin/payroll/policy.go +++ b/plugin/payroll/policy.go @@ -1,16 +1,12 @@ package payroll import ( - "encoding/base64" "fmt" - "math/big" "strings" - gcommon "github.com/ethereum/go-ethereum/common" - "github.com/golang/protobuf/proto" + "github.com/vultisig/plugin/internal/plugin" "github.com/vultisig/recipes/chain" "github.com/vultisig/recipes/engine" - rtypes "github.com/vultisig/recipes/types" vtypes "github.com/vultisig/verifier/types" ) @@ -53,150 +49,6 @@ func (p *PayrollPlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, return nil } -func (p *PayrollPlugin) validateRecipient(pc *rtypes.ParameterConstraint) error { - if pc == nil { - return fmt.Errorf("recipient parameter constraint is nil") - } - if pc.Constraint == nil { - return fmt.Errorf("recipient constraint is nil") - } - if pc.ParameterName != "recipient" { - return fmt.Errorf("expected recipient parameter, got: %s", pc.ParameterName) - } - if !pc.Constraint.Required { - return fmt.Errorf("recipient constraint is required, but not set") - } - if pc.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED { - return fmt.Errorf("recipient constraint must be fixed, got: %s", pc.Constraint.Type) - } - if _, err := gcommon.NewMixedcaseAddressFromString(pc.Constraint.GetFixedValue()); err != nil { - return fmt.Errorf("invalid recipient address: %s, error: %w", pc.Constraint.GetFixedValue(), err) - } - return nil -} - -func (p *PayrollPlugin) validateAmount(pc *rtypes.ParameterConstraint) error { - if pc == nil { - return fmt.Errorf("amount parameter constraint is nil") - } - if pc.ParameterName != "amount" { - return fmt.Errorf("expected amount parameter, got: %s", pc.ParameterName) - } - if pc.Constraint == nil { - return fmt.Errorf("amount constraint is nil") - } - if !pc.Constraint.Required { - return fmt.Errorf("amount constraint is required, but not set") - } - if pc.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED { - return fmt.Errorf("amount constraint must be fixed, got: %s", pc.Constraint.Type) - } - if _, ok := new(big.Int).SetString(pc.Constraint.GetFixedValue(), 10); !ok { - return fmt.Errorf("invalid amount: %s", pc.Constraint.GetFixedValue()) - } - - if !strings.EqualFold(pc.Constraint.DenominatedIn, "wei") { - return fmt.Errorf("amount constraint must be denominated in wei, got: %s", pc.Constraint.DenominatedIn) - } - return nil -} - -func (p *PayrollPlugin) validateToken(pc *rtypes.ParameterConstraint) error { - if pc == nil { - return fmt.Errorf("token parameter constraint is nil") - } - if pc.ParameterName != "token" { - return fmt.Errorf("expected token parameter, got: %s", pc.ParameterName) - } - if pc.Constraint == nil { - return fmt.Errorf("token constraint is nil") - } - if !pc.Constraint.Required { - return fmt.Errorf("token constraint is required, but not set") - } - if pc.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED { - return fmt.Errorf("token constraint must be fixed, got: %s", pc.Constraint.Type) - } - - return nil -} - -func (p *PayrollPlugin) validateSchedule(schedule *rtypes.Schedule) error { - if schedule == nil { - return fmt.Errorf("schedule is nil") - } - if schedule.GetFrequency() == rtypes.ScheduleFrequency_SCHEDULE_FREQUENCY_UNSPECIFIED { - return fmt.Errorf("schedule frequency is required") - } - - if schedule.GetStartTime() == nil { - return fmt.Errorf("start time is required") - } - - if schedule.GetEndTime() != nil && schedule.GetEndTime().AsTime().Before(schedule.GetStartTime().AsTime()) { - return fmt.Errorf("end time cannot be before start time") - } - - 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) - } - var seenRecipient, seenAmount bool - for _, pc := range rule.ParameterConstraints { - switch pc.ParameterName { - case "recipient": - if err := p.validateRecipient(pc); err != nil { - return fmt.Errorf("recipient validation failed: %w", err) - } - seenRecipient = true - case "amount": - if err := p.validateAmount(pc); err != nil { - return fmt.Errorf("amount validation failed: %w", err) - } - seenAmount = true - case "token": - default: - return fmt.Errorf("unknown parameter: %s", pc.ParameterName) - } - } - if !seenRecipient && !seenAmount { - return fmt.Errorf("rule must contain at least one recipient or amount parameter") - } - 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) - } - var rPolicy rtypes.Policy - - policyBytes, err := base64.StdEncoding.DecodeString(policyDoc.Recipe) - if err != nil { - return fmt.Errorf("failed to decode policy recipe: %w", err) - } - - if err := proto.Unmarshal(policyBytes, &rPolicy); err != nil { - return fmt.Errorf("failed to unmarshal policy: %w", err) - } - if rPolicy.Schedule == nil { - return fmt.Errorf("policy schedule is nil") - } - - if len(rPolicy.Rules) == 0 { - return fmt.Errorf("no rules") - } - if err := p.validateSchedule(rPolicy.Schedule); err != nil { - return fmt.Errorf("schedule validation failed: %w", err) - } - for _, rule := range rPolicy.Rules { - if err := p.checkRule(rule); err != nil { - return fmt.Errorf("rule validation failed: %w", err) - } - } - - return nil + return plugin.ValidatePluginPolicy(policyDoc, p.GetRecipeSpecification()) } diff --git a/plugin/payroll/transaction.go b/plugin/payroll/transaction.go index 24fd8fe..2782f73 100644 --- a/plugin/payroll/transaction.go +++ b/plugin/payroll/transaction.go @@ -352,8 +352,8 @@ func (p *PayrollPlugin) SigningComplete( return nil } -func (p *PayrollPlugin) GetRecipeSpecification() rtypes.RecipeSchema { - return rtypes.RecipeSchema{ +func (p *PayrollPlugin) GetRecipeSpecification() *rtypes.RecipeSchema { + return &rtypes.RecipeSchema{ Version: 1, // Schema version ScheduleVersion: 1, // Schedule specification version PluginId: string(vtypes.PluginVultisigPayroll_0000), diff --git a/service/policy.go b/service/policy.go index 7819195..1619dd2 100644 --- a/service/policy.go +++ b/service/policy.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "github.com/google/uuid" @@ -41,7 +42,7 @@ func NewPolicyService(db storage.DatabaseStorage, scheduler *scheduler.Scheduler } func (s *PolicyService) handleRollback(ctx context.Context, tx pgx.Tx) { - if err := tx.Rollback(ctx); err != nil { + if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { s.logger.WithError(err).Error("failed to rollback transaction") } }