From 07dc44f7c5549c957328487f3d94147433cf92a4 Mon Sep 17 00:00:00 2001 From: andrewnguyen22 Date: Fri, 16 Oct 2026 06:39:04 -0400 Subject: [PATCH] feat(rpc): return native hash from custom ETH submit --- cmd/rpc/README.md | 24 ++++++++++++++++++++++++ cmd/rpc/eth.go | 27 +++++++++++++++++++++++++-- cmd/rpc/eth_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/cmd/rpc/README.md b/cmd/rpc/README.md index 73cbde3ecf..c26ca71ea8 100644 --- a/cmd/rpc/README.md +++ b/cmd/rpc/README.md @@ -3444,6 +3444,30 @@ $ curl -X POST http://localhost:50002/v1/eth \ } ``` +### canopy_sendRawTransaction + +Submits the same signed transaction accepted by `eth_sendRawTransaction`, but returns both transaction identifiers. The standard Ethereum method remains unchanged for wallet compatibility. + +```json +{ + "jsonrpc": "2.0", + "method": "canopy_sendRawTransaction", + "params": ["0x"], + "id": 1 +} +``` + +```json +{ + "id": 1, + "jsonrpc": "2.0", + "result": { + "ethereumHash": "0x", + "nativeHash": "" + } +} +``` + # Admin 🚨**Important: All admin commands assume secure https connection** diff --git a/cmd/rpc/eth.go b/cmd/rpc/eth.go index c440aeb33c..344093b4c3 100644 --- a/cmd/rpc/eth.go +++ b/cmd/rpc/eth.go @@ -112,6 +112,8 @@ func (s *Server) handleEthereumRPCRequest(ptr *ethRPCRequest) ethRPCResponse { ethResponse, err = s.EthGetCode(args) case `eth_sendRawTransaction`: ethResponse, err = s.EthSendRawTransaction(args) + case `canopy_sendRawTransaction`: + ethResponse, err = s.CanopySendRawTransaction(args) case `eth_call`: ethResponse, err = s.EthCall(args) case `eth_estimateGas`: @@ -406,6 +408,19 @@ func (s *Server) EthGetCode(args []any) (any, error) { // EthSendRawTransaction() converts the RLP transaction into a Canopy compatible transaction and submits it // - a valid RLP signature is considered a valid signature in Canopy for send transactions func (s *Server) EthSendRawTransaction(args []any) (any, error) { + hashes, err := s.sendRawTransaction(args) + if err != nil { + return nil, err + } + return hashes.EthereumHash, nil +} + +// CanopySendRawTransaction() submits an RLP transaction and returns its Ethereum and native Canopy hashes. +func (s *Server) CanopySendRawTransaction(args []any) (any, error) { + return s.sendRawTransaction(args) +} + +func (s *Server) sendRawTransaction(args []any) (*canopySendRawTransactionResponse, error) { // extract the raw transaction bytes rawTx, err := bytesFromArgs(args) if err != nil { @@ -422,6 +437,7 @@ func (s *Server) EthSendRawTransaction(args []any) (any, error) { return nil, err } txHashString := ethHashStringFromTransaction(transaction) + nativeHashString := crypto.HashString(bz) pendingEthTxMu.Lock() defer pendingEthTxMu.Unlock() oldHash, oldBz, underpriced := s.pendingEthReplacement(transaction) @@ -438,8 +454,10 @@ func (s *Server) EthSendRawTransaction(args []any) (any, error) { return nil, err } removePendingEthTx(oldHash) - // return the transaction hash - return txHashString, nil + return &canopySendRawTransactionResponse{ + EthereumHash: txHashString, + NativeHash: nativeHashString, + }, nil } // EthCall() simulates a call to a 'smart contract' for Canopy @@ -1299,6 +1317,11 @@ func (s *Server) pendingEthReplacement(tx *lib.Transaction) (hash string, bz []b // TYPES BELOW +type canopySendRawTransactionResponse struct { + EthereumHash string `json:"ethereumHash"` + NativeHash string `json:"nativeHash"` +} + // ethRPCRequest is the JSON RPC 2.0 request structure type ethRPCRequest struct { ID any `json:"id"` diff --git a/cmd/rpc/eth_test.go b/cmd/rpc/eth_test.go index bcd8f2993c..00708b18e3 100644 --- a/cmd/rpc/eth_test.go +++ b/cmd/rpc/eth_test.go @@ -171,6 +171,36 @@ func TestEthSendRawTransactionInsertsSynchronously(t *testing.T) { require.ErrorContains(t, replacementErr, "replacement transaction underpriced") } +func TestCanopySendRawTransactionReturnsEthereumAndNativeHashes(t *testing.T) { + clearAllPendingEthTxsForTest() + defer clearAllPendingEthTxsForTest() + key, err := ethCrypto.GenerateKey() + require.NoError(t, err) + raw := mustNewSignedRawEthTxForRPCWithKey(t, key, 0) + tx, txErr := fsm.RLPToCanopyTransactionV2(raw) + require.NoError(t, txErr) + bz, marshalErr := lib.Marshal(tx) + require.NoError(t, marshalErr) + mempool := &controller.Mempool{Mempool: lib.NewMempool(lib.DefaultMempoolConfig()), L: &sync.Mutex{}} + ctrl := &controller.Controller{Mempool: mempool} + setUnexportedField(t, ctrl, "isSyncing", &atomic.Bool{}) + server := &Server{controller: ctrl} + + response := server.handleEthereumRPCRequest(ðRPCRequest{ + ID: 1, + JSONRPC: "2.0", + Method: "canopy_sendRawTransaction", + Params: json.RawMessage(`["` + hexutil.Encode(raw) + `"]`), + }) + + require.Nil(t, response.Error) + hashes, ok := response.Result.(*canopySendRawTransactionResponse) + require.True(t, ok) + require.Equal(t, ethHashStringFromTransaction(tx), hashes.EthereumHash) + require.Equal(t, crypto.HashString(bz), hashes.NativeHash) + require.True(t, mempool.Contains(hashes.NativeHash)) +} + func TestEthSendRawTransactionReplacementPolicy(t *testing.T) { clearAllPendingEthTxsForTest() defer clearAllPendingEthTxsForTest()