Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cmd/rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<signed-transaction>"],
"id": 1
}
```

```json
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"ethereumHash": "0x<ethereum-transaction-hash>",
"nativeHash": "<canopy-transaction-hash>"
}
}
```

# Admin

🚨**Important: All admin commands assume secure https connection**
Expand Down
27 changes: 25 additions & 2 deletions cmd/rpc/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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"`
Expand Down
30 changes: 30 additions & 0 deletions cmd/rpc/eth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(&ethRPCRequest{
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()
Expand Down
Loading