From ca36ae3f5cba672004c580d408167c018c6802cc Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Fri, 25 Jul 2025 12:42:45 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Stable=20version=20of=20fe?= =?UTF-8?q?es=20(with=20db=20migrations=20removed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/plugin.go | 4 +- fee.worker.example.json | 3 +- go.mod | 4 +- go.sum | 4 ++ plugin/fees/config.go | 14 +++++ plugin/fees/fees.go | 8 +-- plugin/fees/transaction.go | 96 ++++++------------------------ plugin/payroll/transaction.go | 43 ------------- storage/postgres/policy.go | 10 +--- storage/postgres/schema/schema.sql | 42 ++++++++++++- 10 files changed, 87 insertions(+), 141 deletions(-) diff --git a/api/plugin.go b/api/plugin.go index 4414660..e1eaafc 100644 --- a/api/plugin.go +++ b/api/plugin.go @@ -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, + }) } func (s *Server) GetPolicySchema(c echo.Context) error { diff --git a/fee.worker.example.json b/fee.worker.example.json index 717066b..5edcc36 100644 --- a/fee.worker.example.json +++ b/fee.worker.example.json @@ -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", diff --git a/go.mod b/go.mod index 56fe04d..2833375 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 512113e..eaf2534 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/plugin/fees/config.go b/plugin/fees/config.go index 87a97bf..36fdd74 100644 --- a/plugin/fees/config.go +++ b/plugin/fees/config.go @@ -3,6 +3,7 @@ package fees import ( "errors" "fmt" + "math/big" "slices" "strings" @@ -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/" @@ -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 { @@ -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 } diff --git a/plugin/fees/fees.go b/plugin/fees/fees.go index 09b4e97..7e90bb9 100644 --- a/plugin/fees/fees.go +++ b/plugin/fees/fees.go @@ -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) } @@ -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, diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index 6b8a7c0..cfdf72d 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -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" @@ -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) { @@ -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), @@ -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, @@ -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: "", @@ -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, diff --git a/plugin/payroll/transaction.go b/plugin/payroll/transaction.go index 99060b8..36f73e7 100644 --- a/plugin/payroll/transaction.go +++ b/plugin/payroll/transaction.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" - "errors" "fmt" "math/big" "sync" @@ -13,13 +12,11 @@ import ( 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" @@ -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") diff --git a/storage/postgres/policy.go b/storage/postgres/policy.go index 2208d6e..14f0591 100644 --- a/storage/postgres/policy.go +++ b/storage/postgres/policy.go @@ -159,13 +159,6 @@ 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) @@ -173,7 +166,8 @@ func (p *PostgresBackend) DeletePluginPolicyTx(ctx context.Context, dbTx pgx.Tx, return fmt.Errorf("failed to delete time triggers: %w", err) } _, err = dbTx.Exec(ctx, ` - DELETE FROM plugin_policies + UPDATE plugin_policies + SET deleted = true WHERE id = $1 `, id) if err != nil { diff --git a/storage/postgres/schema/schema.sql b/storage/postgres/schema/schema.sql index 8a4b7b2..53ac1d3 100644 --- a/storage/postgres/schema/schema.sql +++ b/storage/postgres/schema/schema.sql @@ -22,6 +22,39 @@ CREATE TYPE "tx_indexer_status_onchain" AS ENUM ( 'FAIL' ); +CREATE FUNCTION "prevent_insert_if_policy_deleted"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF NEW.deleted = true THEN + RAISE EXCEPTION 'Cannot insert a deleted policy'; + END IF; + RETURN NEW; +END; +$$; + +CREATE FUNCTION "prevent_update_if_policy_deleted"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF OLD.deleted = true THEN + RAISE EXCEPTION 'Cannot update a deleted policy'; + END IF; + RETURN NEW; +END; +$$; + +CREATE FUNCTION "set_policy_inactive_on_delete"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF NEW.deleted = true THEN + NEW.active := false; + END IF; + RETURN NEW; +END; +$$; + CREATE FUNCTION "update_updated_at_column"() RETURNS "trigger" LANGUAGE "plpgsql" AS $$ @@ -71,7 +104,8 @@ CREATE TABLE "plugin_policies" ( "recipe" "text" NOT NULL, "active" boolean DEFAULT true NOT NULL, "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, - "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "deleted" boolean DEFAULT false NOT NULL ); CREATE TABLE "time_triggers" ( @@ -152,6 +186,12 @@ CREATE INDEX "idx_tx_indexer_key" ON "tx_indexer" USING "btree" ("chain_id", "pl CREATE INDEX "idx_tx_indexer_status_onchain_lost" ON "tx_indexer" USING "btree" ("status_onchain", "lost"); +CREATE TRIGGER "trg_prevent_insert_if_policy_deleted" BEFORE INSERT ON "plugin_policies" FOR EACH ROW EXECUTE FUNCTION "public"."prevent_insert_if_policy_deleted"(); + +CREATE TRIGGER "trg_prevent_update_if_policy_deleted" BEFORE UPDATE ON "plugin_policies" FOR EACH ROW WHEN (("old"."deleted" = true)) EXECUTE FUNCTION "public"."prevent_update_if_policy_deleted"(); + +CREATE TRIGGER "trg_set_policy_inactive_on_delete" BEFORE INSERT OR UPDATE ON "plugin_policies" FOR EACH ROW WHEN (("new"."deleted" = true)) EXECUTE FUNCTION "public"."set_policy_inactive_on_delete"(); + CREATE TRIGGER "update_fee_run_updated_at" BEFORE UPDATE ON "fee_run" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); ALTER TABLE ONLY "fee"