diff --git a/bitcoin/client.go b/bitcoin/client.go index 6ac38cc..bdb1efd 100644 --- a/bitcoin/client.go +++ b/bitcoin/client.go @@ -78,6 +78,9 @@ const ( // https://developer.bitcoin.org/reference/rpc/estimatesmartfee.html requestMethodEstimateSmartFee requestMethod = "estimatesmartfee" + // https://developer.bitcoin.org/reference/rpc/getrawmempool.html + requestMethodRawMempool requestMethod = "getrawmempool" + // blockNotFoundErrCode is the RPC error code when a block cannot be found blockNotFoundErrCode = -5 ) @@ -316,6 +319,23 @@ func (b *Client) PruneBlockchain( return response.Result, nil } +// RawMempool returns an array of all transaction +// hashes currently in the mempool. +func (b *Client) RawMempool( + ctx context.Context, +) ([]string, error) { + // Parameters: + // 1. verbose + params := []interface{}{false} + + response := &rawMempoolResponse{} + if err := b.post(ctx, requestMethodRawMempool, params, response); err != nil { + return nil, fmt.Errorf("%w: error getting raw mempool", err) + } + + return response.Result, nil +} + // getPeerInfo performs the `getpeerinfo` JSON-RPC request func (b *Client) getPeerInfo( ctx context.Context, diff --git a/bitcoin/types.go b/bitcoin/types.go index 9891157..8942577 100644 --- a/bitcoin/types.go +++ b/bitcoin/types.go @@ -478,6 +478,25 @@ func (s suggestedFeeRateResponse) Err() error { ) } +// rawMempoolResponse is the response body for `getrawmempool` requests. +type rawMempoolResponse struct { + Result []string `json:"result"` + Error *responseError `json:"error"` +} + +func (r rawMempoolResponse) Err() error { + if r.Error == nil { + return nil + } + + return fmt.Errorf( + "%w: error JSON RPC response, code: %d, message: %s", + ErrJSONRPCError, + r.Error.Code, + r.Error.Message, + ) +} + // CoinIdentifier converts a tx hash and vout into // the canonical CoinIdentifier.Identifier used in // rosetta-bitcoin. diff --git a/mocks/services/client.go b/mocks/services/client.go index d6aa25d..838124f 100644 --- a/mocks/services/client.go +++ b/mocks/services/client.go @@ -38,6 +38,29 @@ func (_m *Client) NetworkStatus(_a0 context.Context) (*types.NetworkStatusRespon return r0, r1 } +// RawMempool provides a mock function with given fields: _a0 +func (_m *Client) RawMempool(_a0 context.Context) ([]string, error) { + ret := _m.Called(_a0) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SendRawTransaction provides a mock function with given fields: _a0, _a1 func (_m *Client) SendRawTransaction(_a0 context.Context, _a1 string) (string, error) { ret := _m.Called(_a0, _a1) diff --git a/services/mempool_service.go b/services/mempool_service.go index f5d232b..3e4498a 100644 --- a/services/mempool_service.go +++ b/services/mempool_service.go @@ -22,11 +22,15 @@ import ( ) // MempoolAPIService implements the server.MempoolAPIServicer interface. -type MempoolAPIService struct{} +type MempoolAPIService struct { + client Client +} // NewMempoolAPIService creates a new instance of a MempoolAPIService. -func NewMempoolAPIService() server.MempoolAPIServicer { - return &MempoolAPIService{} +func NewMempoolAPIService(client Client) server.MempoolAPIServicer { + return &MempoolAPIService{ + client: client, + } } // Mempool implements the /mempool endpoint. @@ -34,7 +38,19 @@ func (s *MempoolAPIService) Mempool( ctx context.Context, request *types.NetworkRequest, ) (*types.MempoolResponse, *types.Error) { - return nil, wrapErr(ErrUnimplemented, nil) + mempoolTransactions, err := s.client.RawMempool(ctx) + if err != nil { + return nil, wrapErr(ErrBitcoind, err) + } + + transactionIdentifiers := make([]*types.TransactionIdentifier, len(mempoolTransactions)) + for i, mempoolTransaction := range mempoolTransactions { + transactionIdentifiers[i] = &types.TransactionIdentifier{Hash: mempoolTransaction} + } + + return &types.MempoolResponse{ + TransactionIdentifiers: transactionIdentifiers, + }, nil } // MempoolTransaction implements the /mempool/transaction endpoint. diff --git a/services/mempool_service_test.go b/services/mempool_service_test.go index d1e6a4b..0484120 100644 --- a/services/mempool_service_test.go +++ b/services/mempool_service_test.go @@ -18,20 +18,37 @@ import ( "context" "testing" + mocks "github.com/coinbase/rosetta-bitcoin/mocks/services" + + "github.com/coinbase/rosetta-sdk-go/types" "github.com/stretchr/testify/assert" ) func TestMempoolEndpoints(t *testing.T) { - servicer := NewMempoolAPIService() + mockClient := &mocks.Client{} + servicer := NewMempoolAPIService(mockClient) ctx := context.Background() + mockClient.On("RawMempool", ctx).Return([]string{ + "tx1", + "tx2", + }, nil) mem, err := servicer.Mempool(ctx, nil) - assert.Nil(t, mem) - assert.Equal(t, ErrUnimplemented.Code, err.Code) - assert.Equal(t, ErrUnimplemented.Message, err.Message) + assert.Nil(t, err) + assert.Equal(t, &types.MempoolResponse{ + TransactionIdentifiers: []*types.TransactionIdentifier{ + { + Hash: "tx1", + }, + { + Hash: "tx2", + }, + }, + }, mem) memTransaction, err := servicer.MempoolTransaction(ctx, nil) assert.Nil(t, memTransaction) assert.Equal(t, ErrUnimplemented.Code, err.Code) assert.Equal(t, ErrUnimplemented.Message, err.Message) + mockClient.AssertExpectations(t) } diff --git a/services/router.go b/services/router.go index bd0e6cd..423389d 100644 --- a/services/router.go +++ b/services/router.go @@ -55,7 +55,7 @@ func NewBlockchainRouter( asserter, ) - mempoolAPIService := NewMempoolAPIService() + mempoolAPIService := NewMempoolAPIService(client) mempoolAPIController := server.NewMempoolAPIController( mempoolAPIService, asserter, diff --git a/services/types.go b/services/types.go index 7d8b8af..dad143d 100644 --- a/services/types.go +++ b/services/types.go @@ -28,6 +28,7 @@ type Client interface { NetworkStatus(context.Context) (*types.NetworkStatusResponse, error) SendRawTransaction(context.Context, string) (string, error) SuggestedFeeRate(context.Context, int64) (float64, error) + RawMempool(context.Context) ([]string, error) } // Indexer is used by the servicers to get block and account data.