From adb8ffa6cf37c6605b53c5bd98ce82764b50d58a Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Mon, 7 Jul 2025 20:23:32 +0400 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Adding=20more=20transa?= =?UTF-8?q?ction=20based=20functionality=20to=20fees?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/plugin.go | 2 +- plugin/common/common.go | 14 +++---- plugin/fees/fees.go | 22 +++++----- plugin/fees/transaction.go | 86 +++++++++++++++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 19 deletions(-) diff --git a/api/plugin.go b/api/plugin.go index fb33432..b122504 100644 --- a/api/plugin.go +++ b/api/plugin.go @@ -181,7 +181,7 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { if err := s.plugin.ValidatePluginPolicy(policy); err != nil { s.logger.WithError(err).Error("Failed to validate plugin policy") - return c.JSON(http.StatusBadRequest, NewErrorResponse("failed to validate policy")) + return c.JSON(http.StatusBadRequest, NewErrorResponse(fmt.Sprintf("failed to validate policy: %s", err))) } if policy.ID.String() == "" { diff --git a/plugin/common/common.go b/plugin/common/common.go index f8a9a81..05f2f62 100644 --- a/plugin/common/common.go +++ b/plugin/common/common.go @@ -44,7 +44,7 @@ func GenUnsignedTx( nonce, ) if err != nil { - return nil, fmt.Errorf("p.evmMakeUnsignedTransfer: %v", err) + return nil, fmt.Errorf("plugincommon.evmMakeUnsignedTransfer: %v", err) } return tx, nil default: @@ -100,7 +100,7 @@ func EvmMakeUnsignedTransfer( nonce, ) if err != nil { - return nil, fmt.Errorf("p.evmEstimateTx: %v", err) + return nil, fmt.Errorf("plugincommon.evmEstimateTx: %v", err) } bytes, err := EvmEncodeUnsignedDynamicFeeTx( @@ -148,7 +148,7 @@ func EvmEstimateTx( eg.Go(func() error { r, e := rpcClient.SuggestGasTipCap(ctx) if e != nil { - return fmt.Errorf("p.rpcClient.SuggestGasTipCap: %v", e) + return fmt.Errorf("plugincommon.rpcClient.SuggestGasTipCap: %v", e) } gasTipCap = r return nil @@ -158,7 +158,7 @@ func EvmEstimateTx( eg.Go(func() error { feeHistory, e := rpcClient.FeeHistory(ctx, 1, nil, nil) if e != nil { - return fmt.Errorf("p.rpcClient.FeeHistory: %v", e) + return fmt.Errorf("plugincommon.rpcClient.FeeHistory: %v", e) } if len(feeHistory.BaseFee) == 0 { return fmt.Errorf("feeHistory.BaseFee is empty") @@ -197,9 +197,9 @@ func EvmEstimateTx( From: from.Hex(), To: to.Hex(), Gas: "0x" + strconv.FormatUint(gasLimit, 16), - MaxPriorityFeePerGas: "0x" + gcommon.Bytes2Hex(gasTipCap.Bytes()), - MaxFeePerGas: "0x" + gcommon.Bytes2Hex(maxFeePerGas.Bytes()), - Value: "0x" + gcommon.Bytes2Hex(value.Bytes()), + MaxPriorityFeePerGas: "0x" + gasTipCap.Text(16), + MaxFeePerGas: "0x" + maxFeePerGas.Text(16), + Value: "0x" + value.Text(16), Data: "0x" + gcommon.Bytes2Hex(data), }, "latest", diff --git a/plugin/fees/fees.go b/plugin/fees/fees.go index dc581f4..9de8668 100644 --- a/plugin/fees/fees.go +++ b/plugin/fees/fees.go @@ -9,7 +9,6 @@ import ( "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/sirupsen/logrus" - "github.com/vultisig/mobile-tss-lib/tss" vcommon "github.com/vultisig/verifier/common" "github.com/vultisig/verifier/plugin" "github.com/vultisig/verifier/tx_indexer" @@ -22,6 +21,8 @@ import ( "github.com/vultisig/plugin/internal/verifierapi" plugincommon "github.com/vultisig/plugin/plugin/common" "github.com/vultisig/plugin/storage" + "github.com/vultisig/recipes/sdk/evm" + "github.com/vultisig/verifier/vault" ) /* @@ -37,6 +38,7 @@ type FeePlugin struct { vaultStorage *vault.BlockStorageImp db storage.DatabaseStorage rpcClient *ethclient.Client + eth *evm.SDK logger logrus.FieldLogger verifierApi *verifierapi.VerifierApi config *FeeConfig @@ -66,6 +68,12 @@ 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) } @@ -86,6 +94,7 @@ func NewFeePlugin(db storage.DatabaseStorage, return &FeePlugin{ db: db, rpcClient: rpcClient, + eth: evm.NewSDK(ethEvmChainID, rpcClient, rpcClient.Client()), logger: logger.WithField("plugin", "fees"), config: feeConfig, verifierApi: verifierApi, @@ -98,15 +107,8 @@ func NewFeePlugin(db storage.DatabaseStorage, }, nil } -func (fp *FeePlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error { - return nil -} -func (fp *FeePlugin) SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, policy vtypes.PluginPolicy) error { - return nil -} - /* -The handler of the asynq job. Fees can be initialized and collected in 3 ways: +The handler of the asynq job. Fees can initialized and collected in 3 ways: - By public key (queries the db for a single fee_policy and then kicks off the fee collection) - By policy id (queries the db for a fee_policy matching that id and then kicks off the fee collection) - All (queries the db for all fee_policies and then kicks off the fee collection for each policy) @@ -257,7 +259,7 @@ func (fp *FeePlugin) executeFeeCollection(ctx context.Context, feePolicy vtypes. for _, keySignRequest := range keySignRequests { req := keySignRequest - if err := fp.initSign(ctx, req, feePolicy); err != nil { + if err := fp.initSign(ctx, req, feePolicy, feeRun.ID); err != nil { return fmt.Errorf("failed to init sign: %w", err) } } diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index 8e87ac7..32bbbf8 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "github.com/google/uuid" @@ -17,6 +18,7 @@ import ( "github.com/vultisig/plugin/internal/tasks" plugincommon "github.com/vultisig/plugin/plugin/common" "github.com/vultisig/recipes/chain" + "github.com/vultisig/recipes/engine" reth "github.com/vultisig/recipes/ethereum" rtypes "github.com/vultisig/recipes/types" rutil "github.com/vultisig/recipes/util" @@ -24,6 +26,8 @@ import ( vcommon "github.com/vultisig/verifier/common" "github.com/vultisig/verifier/tx_indexer/pkg/storage" vtypes "github.com/vultisig/verifier/types" + + gcommon "github.com/ethereum/go-ethereum/common" ) func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.PluginKeysignRequest, error) { @@ -136,7 +140,7 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P ) if e != nil { - return []vtypes.PluginKeysignRequest{}, fmt.Errorf("plugincommon.genUnsignedTx: %w", e) + return []vtypes.PluginKeysignRequest{}, fmt.Errorf("plugincommon.GenUnsignedTx: %w", e) } txHex := hex.EncodeToString(tx) @@ -227,6 +231,7 @@ func (fp *FeePlugin) initSign( ctx context.Context, req vtypes.PluginKeysignRequest, pluginPolicy vtypes.PluginPolicy, + runId uuid.UUID, ) error { buf, e := json.Marshal(req) if e != nil { @@ -244,6 +249,20 @@ func (fp *FeePlugin) initSign( return fmt.Errorf("fp.client.Enqueue: %w", e) } + if runId != uuid.Nil { + if len(req.Messages) != 1 { + return fmt.Errorf("multiple messages in key sign request, expected 1") + } + txId, err := uuid.Parse(req.Messages[0].TxIndexerID) + if err != nil { + return fmt.Errorf("failed to parse tx indexer id: %w", err) + } + _, err = fp.db.Pool().Exec(ctx, "UPDATE fee_run SET status = 'sent', tx_id = $1 WHERE id = $2", txId, runId) + if err != nil { + return fmt.Errorf("failed to update fee run: %w", err) + } + } + for { select { case <-ctx.Done(): @@ -283,3 +302,68 @@ func (fp *FeePlugin) initSign( } } } + +func (fp *FeePlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, txs []vtypes.PluginKeysignRequest) error { + // First validate the plugin policy itself + err := fp.ValidatePluginPolicy(policy) + if err != nil { + return fmt.Errorf("failed to validate plugin policy: %v", err) + } + + // Get the recipe from the policy for transaction validation + recipe, err := policy.GetRecipe() + if err != nil { + return fmt.Errorf("failed to get recipe from policy: %v", err) + } + + // Create a recipe engine for evaluating transactions + eng := engine.NewEngine() + + // Validate each proposed transaction + for _, tx := range txs { + for _, keysignMessage := range tx.Messages { + // Get the chain for the transaction + messageChain, err := chain.GetChain(strings.ToLower(keysignMessage.Chain.String())) + if err != nil { + return fmt.Errorf("failed to get chain: %w", err) + } + + // Parse the transaction to validate its structure + decodedTx, err := messageChain.ParseTransaction(keysignMessage.Message) + if err != nil { + return fmt.Errorf("failed to parse transaction: %w", err) + } + + // Evaluate if the transaction is allowed by the policy + transactionAllowed, _, err := eng.Evaluate(recipe, messageChain, decodedTx) + if err != nil { + return fmt.Errorf("failed to evaluate transaction: %w", err) + } + + if !transactionAllowed { + return fmt.Errorf("transaction %s on %s not allowed by policy", keysignMessage.Hash, keysignMessage.Chain) + } + } + } + + return nil +} + +func (fp *FeePlugin) SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, policy vtypes.PluginPolicy) error { + // Broadcast the signed transaction to the Ethereum network + tx, err := fp.eth.Send( + ctx, + gcommon.FromHex(signRequest.Transaction), + gcommon.Hex2Bytes(signature.R), + gcommon.Hex2Bytes(signature.S), + gcommon.Hex2Bytes(signature.RecoveryID), + ) + if err != nil { + fp.logger.WithError(err).WithField("tx_hex", signRequest.Transaction).Error("fp.eth.Send") + return fmt.Errorf("fp.eth.Send(tx_hex=%s): %w", signRequest.Transaction, err) + } + + // Log successful transaction broadcast + fp.logger.WithField("hash", tx.Hash().Hex()).Info("fee collection transaction successfully broadcasted") + return nil +} From 01efdec1de8ac9e59972db6f4d17527d6f2ed83e Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Wed, 9 Jul 2025 11:18:35 +0400 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Removed=20leaking?= =?UTF-8?q?=20of=20error=20in=20api=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/plugin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/plugin.go b/api/plugin.go index b122504..88f6bab 100644 --- a/api/plugin.go +++ b/api/plugin.go @@ -180,8 +180,8 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { // We re-init plugin as verification server doesn't have plugin defined if err := s.plugin.ValidatePluginPolicy(policy); err != nil { - s.logger.WithError(err).Error("Failed to validate plugin policy") - return c.JSON(http.StatusBadRequest, NewErrorResponse(fmt.Sprintf("failed to validate policy: %s", err))) + s.logger.WithError(err).Error("failed to validate plugin policy: ", err) + return c.JSON(http.StatusBadRequest, NewErrorResponse("failed to validate policy")) } if policy.ID.String() == "" { From f9925eb630bbdfde0f78a135c7ef38244ceb09b5 Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Wed, 9 Jul 2025 11:22:56 +0400 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=94=8A=20Updated=20error=20messages?= =?UTF-8?q?=20and=20return=20of=20nil=20instead=20of=20empty=20slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/common/common.go | 28 ++++++++++++++-------------- plugin/fees/transaction.go | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/plugin/common/common.go b/plugin/common/common.go index 05f2f62..03d9562 100644 --- a/plugin/common/common.go +++ b/plugin/common/common.go @@ -44,7 +44,7 @@ func GenUnsignedTx( nonce, ) if err != nil { - return nil, fmt.Errorf("plugincommon.evmMakeUnsignedTransfer: %v", err) + return nil, err } return tx, nil default: @@ -77,12 +77,12 @@ func EvmMakeUnsignedTransfer( } else { parsedABI, err := abi.JSON(strings.NewReader(Erc20ABI)) if err != nil { - return nil, fmt.Errorf("abi.JSON(strings.NewReader(erc20ABI)): %v", err) + return nil, fmt.Errorf("error parsing erc20 abi: %v", err) } d, err := parsedABI.Pack("transfer", to, amount) if err != nil { - return nil, fmt.Errorf("parsedABI.Pack: %v", err) + return nil, fmt.Errorf("error packing transfer: %v", err) } value = big.NewInt(0) data = d @@ -100,7 +100,7 @@ func EvmMakeUnsignedTransfer( nonce, ) if err != nil { - return nil, fmt.Errorf("plugincommon.evmEstimateTx: %v", err) + return nil, err } bytes, err := EvmEncodeUnsignedDynamicFeeTx( @@ -138,7 +138,7 @@ func EvmEstimateTx( Value: value, }) if e != nil { - return fmt.Errorf("p.rpcClient.EstimateGas: %v", e) + return fmt.Errorf("estimateGas failed with error: %v", e) } gasLimit = r return nil @@ -146,9 +146,9 @@ func EvmEstimateTx( var gasTipCap *big.Int eg.Go(func() error { - r, e := rpcClient.SuggestGasTipCap(ctx) - if e != nil { - return fmt.Errorf("plugincommon.rpcClient.SuggestGasTipCap: %v", e) + r, err := rpcClient.SuggestGasTipCap(ctx) + if err != nil { + return err } gasTipCap = r return nil @@ -156,9 +156,9 @@ func EvmEstimateTx( var baseFee *big.Int eg.Go(func() error { - feeHistory, e := rpcClient.FeeHistory(ctx, 1, nil, nil) - if e != nil { - return fmt.Errorf("plugincommon.rpcClient.FeeHistory: %v", e) + feeHistory, err := rpcClient.FeeHistory(ctx, 1, nil, nil) + if err != nil { + return err } if len(feeHistory.BaseFee) == 0 { return fmt.Errorf("feeHistory.BaseFee is empty") @@ -169,7 +169,7 @@ func EvmEstimateTx( err := eg.Wait() if err != nil { - return 0, 0, nil, nil, nil, fmt.Errorf("eg.Wait: %v", err) + return 0, 0, nil, nil, nil, err } maxFeePerGas := new(big.Int).Add(gasTipCap, baseFee) @@ -206,7 +206,7 @@ func EvmEstimateTx( }, ) if err != nil { - return 0, 0, nil, nil, nil, fmt.Errorf("p.rpcClient.Client().CallContext: %v", err) + return 0, 0, nil, nil, nil, err } return nonce, gasLimit, gasTipCap, maxFeePerGas, createAccessListRes.AccessList, nil @@ -234,7 +234,7 @@ func EvmEncodeUnsignedDynamicFeeTx( AccessList: accessList, }) if err != nil { - return nil, fmt.Errorf("rlp.EncodeToBytes: %v", err) + return nil, fmt.Errorf("error encoding unsigned dynamic fee tx: %v", err) } res := append([]byte{gtypes.DynamicFeeTxType}, bytes...) diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index 32bbbf8..d22ae71 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -120,7 +120,7 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P "chain_id": chain, "token_id": token.Address, }).Info("transaction already proposed, skipping") - return []vtypes.PluginKeysignRequest{}, nil + return nil, nil } nonce, err := fp.nonceManager.GetNextNonce(ethAddress) @@ -155,7 +155,7 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P ProposedTxHex: txHex, }) if e != nil { - return []vtypes.PluginKeysignRequest{}, fmt.Errorf("fp.tx_indexer.CreateTx: %w", e) + return nil, fmt.Errorf("error creating tx indexed transaction: %w", e) } // Create signing request @@ -203,7 +203,7 @@ func (fp *FeePlugin) IsAlreadyProposed( interval, ) if err != nil { - return false, fmt.Errorf("scheduler.NewIntervalSchedule: %w", err) + return false, fmt.Errorf("failed to create interval schedule: %w", err) } fromTime, toTime := sched.ToRangeFrom(time.Now()) @@ -224,7 +224,7 @@ func (fp *FeePlugin) IsAlreadyProposed( if errors.Is(err, storage.ErrNoTx) { return false, nil } - return false, fmt.Errorf("fp.txIndexerService.GetTxInTimeRange: %w", err) + return false, fmt.Errorf("failed to get tx in time range: %w", err) } func (fp *FeePlugin) initSign( @@ -235,7 +235,7 @@ func (fp *FeePlugin) initSign( ) error { buf, e := json.Marshal(req) if e != nil { - return fmt.Errorf("json.Marshal: %w", e) + return fmt.Errorf("failed to marshal key sign request: %w", e) } task, e := fp.asynqClient.Enqueue( @@ -246,7 +246,7 @@ func (fp *FeePlugin) initSign( asynq.Queue(tasks.QUEUE_NAME), ) if e != nil { - return fmt.Errorf("fp.client.Enqueue: %w", e) + return fmt.Errorf("failed to enqueue key sign task: %w", e) } if runId != uuid.Nil { @@ -270,7 +270,7 @@ func (fp *FeePlugin) initSign( case <-time.After(3 * time.Second): taskInfo, er := fp.asynqInspector.GetTaskInfo(tasks.QUEUE_NAME, task.ID) if er != nil { - return fmt.Errorf("fp.inspector.GetTaskInfo: %w", er) + return fmt.Errorf("failed to get task info: %w", er) } if taskInfo.State != asynq.TaskStateCompleted { continue @@ -283,7 +283,7 @@ func (fp *FeePlugin) initSign( var res map[string]tss.KeysignResponse er = json.Unmarshal(taskInfo.Result, &res) if er != nil { - return fmt.Errorf("json.Unmarshal(taskInfo.Result, &res): %w", er) + return fmt.Errorf("failed to unmarshal task result: %w", er) } var sig tss.KeysignResponse @@ -293,7 +293,7 @@ func (fp *FeePlugin) initSign( er = fp.SigningComplete(ctx, sig, req, pluginPolicy) if er != nil { - return fmt.Errorf("fp.SigningComplete: %w", er) + return fmt.Errorf("failed to sign and broadcast transaction: %w", er) } fp.logger.WithField("public_key", req.PublicKey). @@ -360,7 +360,7 @@ func (fp *FeePlugin) SigningComplete(ctx context.Context, signature tss.KeysignR ) if err != nil { fp.logger.WithError(err).WithField("tx_hex", signRequest.Transaction).Error("fp.eth.Send") - return fmt.Errorf("fp.eth.Send(tx_hex=%s): %w", signRequest.Transaction, err) + return fmt.Errorf("failed to send transaction: %w", err) } // Log successful transaction broadcast From a069185a0cf7e79ebf309948c5e6862f524c902c Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Wed, 9 Jul 2025 11:24:18 +0400 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Update=20propose=20tra?= =?UTF-8?q?nsactions=20to=20use=20evm=20sdk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/fees/transaction.go | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index d22ae71..defc2b7 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -16,10 +16,10 @@ import ( "github.com/vultisig/plugin/common" "github.com/vultisig/plugin/internal/scheduler" "github.com/vultisig/plugin/internal/tasks" - plugincommon "github.com/vultisig/plugin/plugin/common" "github.com/vultisig/recipes/chain" "github.com/vultisig/recipes/engine" reth "github.com/vultisig/recipes/ethereum" + "github.com/vultisig/recipes/sdk/evm" rtypes "github.com/vultisig/recipes/types" rutil "github.com/vultisig/recipes/util" "github.com/vultisig/verifier/address" @@ -58,6 +58,8 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P ethchain := echain.(*reth.Ethereum) chain := vcommon.Ethereum txs := []vtypes.PluginKeysignRequest{} + chainID, err := chain.EvmID() + sdk := evm.NewSDK(chainID, fp.rpcClient, fp.rpcClient.Client()) // This should only return one rule, but in case there are more/fewer rules, we'll loop through them all and error if it's the case. for _, rule := range recipe.Rules { @@ -123,24 +125,13 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P return nil, nil } - nonce, err := fp.nonceManager.GetNextNonce(ethAddress) + tx, err := sdk.MakeAnyTransfer(ctx, + gcommon.HexToAddress(ethAddress), + gcommon.HexToAddress(recipient), + gcommon.HexToAddress(token.Address), + big.NewInt(int64(amount))) if err != nil { - return []vtypes.PluginKeysignRequest{}, fmt.Errorf("plugincommon.nonceManager.GetNextNonce: %w", err) - } - - tx, e := plugincommon.GenUnsignedTx( - ctx, - chain, - ethAddress, - token.Address, - fmt.Sprint(amount), - recipient, - fp.rpcClient, - nonce, - ) - - if e != nil { - return []vtypes.PluginKeysignRequest{}, fmt.Errorf("plugincommon.GenUnsignedTx: %w", e) + return nil, fmt.Errorf("failed to generate unsigned transaction: %w", err) } txHex := hex.EncodeToString(tx) From 74c83691222a6da58a53acc38f81a903534bb8f8 Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Wed, 9 Jul 2025 11:25:41 +0400 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9E=95=20Fix=20imports=20and=20white=20s?= =?UTF-8?q?pace=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/common/common.go | 6 +++--- plugin/fees/fees.go | 1 - plugin/fees/transaction.go | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/common/common.go b/plugin/common/common.go index 03d9562..510fbc7 100644 --- a/plugin/common/common.go +++ b/plugin/common/common.go @@ -197,9 +197,9 @@ func EvmEstimateTx( From: from.Hex(), To: to.Hex(), Gas: "0x" + strconv.FormatUint(gasLimit, 16), - MaxPriorityFeePerGas: "0x" + gasTipCap.Text(16), - MaxFeePerGas: "0x" + maxFeePerGas.Text(16), - Value: "0x" + value.Text(16), + MaxPriorityFeePerGas: "0x" + gasTipCap.Text(16), + MaxFeePerGas: "0x" + maxFeePerGas.Text(16), + Value: "0x" + value.Text(16), Data: "0x" + gcommon.Bytes2Hex(data), }, "latest", diff --git a/plugin/fees/fees.go b/plugin/fees/fees.go index 9de8668..47abced 100644 --- a/plugin/fees/fees.go +++ b/plugin/fees/fees.go @@ -22,7 +22,6 @@ import ( plugincommon "github.com/vultisig/plugin/plugin/common" "github.com/vultisig/plugin/storage" "github.com/vultisig/recipes/sdk/evm" - "github.com/vultisig/verifier/vault" ) /* diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index defc2b7..9a53e26 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "strings" "time" From 0c2dd94a0c36b746b3e4b2ca1e75d54171a9df10 Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Wed, 9 Jul 2025 12:11:24 +0400 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=8E=A8=20reuse=20eth=20sdk=20already?= =?UTF-8?q?=20initialised?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/fees/transaction.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index 9a53e26..4dfc19c 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -20,7 +20,6 @@ import ( "github.com/vultisig/recipes/chain" "github.com/vultisig/recipes/engine" reth "github.com/vultisig/recipes/ethereum" - "github.com/vultisig/recipes/sdk/evm" rtypes "github.com/vultisig/recipes/types" rutil "github.com/vultisig/recipes/util" "github.com/vultisig/verifier/address" @@ -59,8 +58,6 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P ethchain := echain.(*reth.Ethereum) chain := vcommon.Ethereum txs := []vtypes.PluginKeysignRequest{} - chainID, err := chain.EvmID() - sdk := evm.NewSDK(chainID, fp.rpcClient, fp.rpcClient.Client()) // This should only return one rule, but in case there are more/fewer rules, we'll loop through them all and error if it's the case. for _, rule := range recipe.Rules { @@ -126,7 +123,7 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P return nil, nil } - tx, err := sdk.MakeAnyTransfer(ctx, + tx, err := fp.eth.MakeAnyTransfer(ctx, gcommon.HexToAddress(ethAddress), gcommon.HexToAddress(recipient), gcommon.HexToAddress(token.Address), From 899ee00be5bc7a0b412cff696a6b2dc9b1ac3a62 Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Thu, 10 Jul 2025 11:48:37 +0400 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=8E=A8=20Requested=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/plugin.go | 2 +- plugin/common/common.go | 242 ------------------------------------- plugin/common/nonce.go | 35 ------ plugin/fees/fees.go | 5 - plugin/fees/transaction.go | 2 +- storage/db.go | 1 + storage/postgres/fees.go | 8 ++ 7 files changed, 11 insertions(+), 284 deletions(-) delete mode 100644 plugin/common/common.go delete mode 100644 plugin/common/nonce.go diff --git a/api/plugin.go b/api/plugin.go index 88f6bab..c57b93b 100644 --- a/api/plugin.go +++ b/api/plugin.go @@ -180,7 +180,7 @@ func (s *Server) CreatePluginPolicy(c echo.Context) error { // We re-init plugin as verification server doesn't have plugin defined if err := s.plugin.ValidatePluginPolicy(policy); err != nil { - s.logger.WithError(err).Error("failed to validate plugin policy: ", err) + s.logger.WithError(err).Error("failed to validate plugin policy") return c.JSON(http.StatusBadRequest, NewErrorResponse("failed to validate policy")) } diff --git a/plugin/common/common.go b/plugin/common/common.go deleted file mode 100644 index 510fbc7..0000000 --- a/plugin/common/common.go +++ /dev/null @@ -1,242 +0,0 @@ -package common - -import ( - "context" - "fmt" - "math/big" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum" - "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/ethclient" - "github.com/ethereum/go-ethereum/rlp" - reth "github.com/vultisig/recipes/ethereum" - vcommon "github.com/vultisig/verifier/common" - "golang.org/x/sync/errgroup" -) - -var ethereumMainnetEvmChainID *big.Int = big.NewInt(1) // 1 is the chain id for Ethereum mainnet -var ethereumZeroAddress gcommon.Address = gcommon.HexToAddress("0x0000000000000000000000000000000000000000") - -// DEFINE COMMON PLUGIN FUNCTIONS HERE (eg creating an unsigned erc20 transaction) - -// Creates an unsigned ERC20 transaction -func GenUnsignedTx( - ctx context.Context, - chain vcommon.Chain, - senderAddress, tokenID, amount, to string, - rpcClient *ethclient.Client, - nonce uint64, -) ([]byte, error) { - switch chain { - case vcommon.Ethereum: - tx, err := EvmMakeUnsignedTransfer( - ctx, - ethereumMainnetEvmChainID, - senderAddress, - tokenID, - amount, - to, - rpcClient, - nonce, - ) - if err != nil { - return nil, err - } - return tx, nil - default: - return nil, fmt.Errorf("unsupported chain: %s", chain) - } -} - -func EvmMakeUnsignedTransfer( - ctx context.Context, - evmChainID *big.Int, - senderAddress, tokenIDStr, amountStr, toStr string, - rpcClient *ethclient.Client, - nonce uint64, -) ([]byte, error) { - amount, ok := new(big.Int).SetString(amountStr, 10) - if !ok { - return nil, fmt.Errorf("new(big.Int).SetString: %s", amountStr) - } - - to := gcommon.HexToAddress(toStr) - tokenID := gcommon.HexToAddress(tokenIDStr) - - var ( - value *big.Int - data []byte - ) - if tokenID == ethereumZeroAddress { - value = amount - data = nil - } else { - parsedABI, err := abi.JSON(strings.NewReader(Erc20ABI)) - if err != nil { - return nil, fmt.Errorf("error parsing erc20 abi: %v", err) - } - - d, err := parsedABI.Pack("transfer", to, amount) - if err != nil { - return nil, fmt.Errorf("error packing transfer: %v", err) - } - value = big.NewInt(0) - data = d - } - - senderAddressHex := gcommon.HexToAddress(senderAddress) - - nonce, gasLimit, gasTipCap, maxFeePerGas, accessList, err := EvmEstimateTx( - ctx, - senderAddressHex, - tokenID, - value, - data, - rpcClient, - nonce, - ) - if err != nil { - return nil, err - } - - bytes, err := EvmEncodeUnsignedDynamicFeeTx( - evmChainID, - nonce, - tokenID, - gasTipCap, - maxFeePerGas, - gasLimit, - value, - data, - accessList, - ) - if err != nil { - return nil, fmt.Errorf("evmEncodeUnsignedDynamicFeeTx: %v", err) - } - return bytes, nil -} - -func EvmEstimateTx( - ctx context.Context, - from, to gcommon.Address, - value *big.Int, - data []byte, - rpcClient *ethclient.Client, - nonce uint64, -) (uint64, uint64, *big.Int, *big.Int, gtypes.AccessList, error) { - var eg errgroup.Group - var gasLimit uint64 - eg.Go(func() error { - r, e := rpcClient.EstimateGas(ctx, ethereum.CallMsg{ - From: from, - To: &to, - Data: data, - Value: value, - }) - if e != nil { - return fmt.Errorf("estimateGas failed with error: %v", e) - } - gasLimit = r - return nil - }) - - var gasTipCap *big.Int - eg.Go(func() error { - r, err := rpcClient.SuggestGasTipCap(ctx) - if err != nil { - return err - } - gasTipCap = r - return nil - }) - - var baseFee *big.Int - eg.Go(func() error { - feeHistory, err := rpcClient.FeeHistory(ctx, 1, nil, nil) - if err != nil { - return err - } - if len(feeHistory.BaseFee) == 0 { - return fmt.Errorf("feeHistory.BaseFee is empty") - } - baseFee = feeHistory.BaseFee[0] - return nil - }) - - err := eg.Wait() - if err != nil { - return 0, 0, nil, nil, nil, err - } - - maxFeePerGas := new(big.Int).Add(gasTipCap, baseFee) - - type createAccessListArgs struct { - From string `json:"from,omitempty"` - To string `json:"to,omitempty"` - Gas string `json:"gas,omitempty"` - GasPrice string `json:"gasPrice,omitempty"` - MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas,omitempty"` - MaxFeePerGas string `json:"maxFeePerGas,omitempty"` - Value string `json:"value,omitempty"` - Data string `json:"data,omitempty"` - } - createAccessListRes := struct { - AccessList gtypes.AccessList `json:"accessList"` - GasUsed string `json:"gasUsed"` - }{} - err = rpcClient.Client().CallContext( - ctx, - &createAccessListRes, - "eth_createAccessList", - []interface{}{ - createAccessListArgs{ - From: from.Hex(), - To: to.Hex(), - Gas: "0x" + strconv.FormatUint(gasLimit, 16), - MaxPriorityFeePerGas: "0x" + gasTipCap.Text(16), - MaxFeePerGas: "0x" + maxFeePerGas.Text(16), - Value: "0x" + value.Text(16), - Data: "0x" + gcommon.Bytes2Hex(data), - }, - "latest", - }, - ) - if err != nil { - return 0, 0, nil, nil, nil, err - } - - return nonce, gasLimit, gasTipCap, maxFeePerGas, createAccessListRes.AccessList, nil -} - -func EvmEncodeUnsignedDynamicFeeTx( - evmChainID *big.Int, - nonce uint64, - to gcommon.Address, - maxPriorityFeePerGas, maxFeePerGas *big.Int, - gas uint64, - value *big.Int, - data []byte, - accessList gtypes.AccessList, -) ([]byte, error) { - bytes, err := rlp.EncodeToBytes(reth.DynamicFeeTxWithoutSignature{ - ChainID: evmChainID, - Nonce: nonce, - GasTipCap: maxPriorityFeePerGas, - GasFeeCap: maxFeePerGas, - Gas: gas, - To: &to, - Value: value, - Data: data, - AccessList: accessList, - }) - if err != nil { - return nil, fmt.Errorf("error encoding unsigned dynamic fee tx: %v", err) - } - - res := append([]byte{gtypes.DynamicFeeTxType}, bytes...) - return res, nil -} diff --git a/plugin/common/nonce.go b/plugin/common/nonce.go deleted file mode 100644 index 8e7a52b..0000000 --- a/plugin/common/nonce.go +++ /dev/null @@ -1,35 +0,0 @@ -package common - -import ( - "context" - "fmt" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" -) - -// Copy of the nonce manager from payroll but with cache removed. Can be added later. - -type NonceManager struct { - rpcClient *ethclient.Client - 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 -} diff --git a/plugin/fees/fees.go b/plugin/fees/fees.go index 47abced..bce5584 100644 --- a/plugin/fees/fees.go +++ b/plugin/fees/fees.go @@ -19,7 +19,6 @@ import ( "github.com/vultisig/plugin/internal/types" "github.com/vultisig/plugin/internal/verifierapi" - plugincommon "github.com/vultisig/plugin/plugin/common" "github.com/vultisig/plugin/storage" "github.com/vultisig/recipes/sdk/evm" ) @@ -36,13 +35,11 @@ type FeePlugin struct { vaultService *vault.ManagementService vaultStorage *vault.BlockStorageImp db storage.DatabaseStorage - rpcClient *ethclient.Client eth *evm.SDK logger logrus.FieldLogger verifierApi *verifierapi.VerifierApi config *FeeConfig txIndexerService *tx_indexer.Service - nonceManager *plugincommon.NonceManager asynqInspector *asynq.Inspector asynqClient *asynq.Client encryptionSecret string @@ -92,7 +89,6 @@ func NewFeePlugin(db storage.DatabaseStorage, return &FeePlugin{ db: db, - rpcClient: rpcClient, eth: evm.NewSDK(ethEvmChainID, rpcClient, rpcClient.Client()), logger: logger.WithField("plugin", "fees"), config: feeConfig, @@ -101,7 +97,6 @@ func NewFeePlugin(db storage.DatabaseStorage, txIndexerService: txIndexerService, asynqInspector: inspector, asynqClient: client, - nonceManager: plugincommon.NewNonceManager(rpcClient), encryptionSecret: encryptionSecret, }, nil } diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index 4dfc19c..bec94d6 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -246,7 +246,7 @@ func (fp *FeePlugin) initSign( if err != nil { return fmt.Errorf("failed to parse tx indexer id: %w", err) } - _, err = fp.db.Pool().Exec(ctx, "UPDATE fee_run SET status = 'sent', tx_id = $1 WHERE id = $2", txId, runId) + err = fp.db.SetFeeRunSent(ctx, runId, txId) if err != nil { return fmt.Errorf("failed to update fee run: %w", err) } diff --git a/storage/db.go b/storage/db.go index 501d894..8259256 100644 --- a/storage/db.go +++ b/storage/db.go @@ -31,6 +31,7 @@ type DatabaseStorage interface { GetTriggerStatus(ctx context.Context, policyID uuid.UUID) (types.TimeTriggerStatus, error) CreateFeeRun(ctx context.Context, policyId uuid.UUID, state types.FeeRunState, fees []verifierapi.FeeDto) (*types.FeeRun, error) + SetFeeRunSent(ctx context.Context, runId uuid.UUID, txId uuid.UUID) error Pool() *pgxpool.Pool } diff --git a/storage/postgres/fees.go b/storage/postgres/fees.go index 69da6ad..1198c79 100644 --- a/storage/postgres/fees.go +++ b/storage/postgres/fees.go @@ -55,3 +55,11 @@ func (p *PostgresBackend) CreateFeeRun(ctx context.Context, policyId uuid.UUID, return &run, nil } + +func (p *PostgresBackend) SetFeeRunSent(ctx context.Context, runId uuid.UUID, txId uuid.UUID) error { + _, err := p.pool.Exec(ctx, `update fee_run set status = $1, tx_id = $2 where id = $3`, types.FeeRunStateSent, txId, runId) + if err != nil { + return fmt.Errorf("failed to update fee run: %w", err) + } + return nil +}