diff --git a/client/main.go b/client/main.go new file mode 100644 index 00000000..0abed1ff --- /dev/null +++ b/client/main.go @@ -0,0 +1,175 @@ +package client + +import ( + "context" + "net/http" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/jhttp" + + "github.com/stellar/stellar-rpc/protocol" +) + +type Client struct { + url string + cli *jrpc2.Client + httpClient *http.Client +} + +func NewClient(url string, httpClient *http.Client) *Client { + c := &Client{url: url, httpClient: httpClient} + c.refreshClient() + return c +} + +func (c *Client) Close() error { + return c.cli.Close() +} + +func (c *Client) refreshClient() { + if c.cli != nil { + c.cli.Close() + } + var opts *jhttp.ChannelOptions + if c.httpClient != nil { + opts = &jhttp.ChannelOptions{ + Client: c.httpClient, + } + } + ch := jhttp.NewChannel(c.url, opts) + c.cli = jrpc2.NewClient(ch, nil) +} + +func (c *Client) callResult(ctx context.Context, method string, params, result any) error { + err := c.cli.CallResult(ctx, method, params, result) + if err != nil { + // This is needed because of https://github.com/creachadair/jrpc2/issues/118 + c.refreshClient() + } + return err +} + +func (c *Client) GetEvents(ctx context.Context, + request protocol.GetEventsRequest, +) (protocol.GetEventsResponse, error) { + var result protocol.GetEventsResponse + err := c.callResult(ctx, protocol.GetEventsMethodName, request, &result) + if err != nil { + return protocol.GetEventsResponse{}, err + } + return result, nil +} + +func (c *Client) GetFeeStats(ctx context.Context) (protocol.GetFeeStatsResponse, error) { + var result protocol.GetFeeStatsResponse + err := c.callResult(ctx, protocol.GetFeeStatsMethodName, nil, &result) + if err != nil { + return protocol.GetFeeStatsResponse{}, err + } + return result, nil +} + +func (c *Client) GetHealth(ctx context.Context) (protocol.GetHealthResponse, error) { + var result protocol.GetHealthResponse + err := c.callResult(ctx, protocol.GetHealthMethodName, nil, &result) + if err != nil { + return protocol.GetHealthResponse{}, err + } + return result, nil +} + +func (c *Client) GetLatestLedger(ctx context.Context) (protocol.GetLatestLedgerResponse, error) { + var result protocol.GetLatestLedgerResponse + err := c.callResult(ctx, protocol.GetLatestLedgerMethodName, nil, &result) + if err != nil { + return protocol.GetLatestLedgerResponse{}, err + } + return result, nil +} + +func (c *Client) GetLedgerEntries(ctx context.Context, + request protocol.GetLedgerEntriesRequest, +) (protocol.GetLedgerEntriesResponse, error) { + var result protocol.GetLedgerEntriesResponse + err := c.callResult(ctx, protocol.GetLedgerEntriesMethodName, request, &result) + if err != nil { + return protocol.GetLedgerEntriesResponse{}, err + } + return result, nil +} + +func (c *Client) GetLedgers(ctx context.Context, + request protocol.GetLedgersRequest, +) (protocol.GetLedgersResponse, error) { + var result protocol.GetLedgersResponse + err := c.callResult(ctx, protocol.GetLedgersMethodName, request, &result) + if err != nil { + return protocol.GetLedgersResponse{}, err + } + return result, nil +} + +func (c *Client) GetNetwork(ctx context.Context, +) (protocol.GetNetworkResponse, error) { + // phony + var request protocol.GetNetworkRequest + var result protocol.GetNetworkResponse + err := c.callResult(ctx, protocol.GetNetworkMethodName, request, &result) + if err != nil { + return protocol.GetNetworkResponse{}, err + } + return result, nil +} + +func (c *Client) GetTransaction(ctx context.Context, + request protocol.GetTransactionRequest, +) (protocol.GetTransactionResponse, error) { + var result protocol.GetTransactionResponse + err := c.callResult(ctx, protocol.GetTransactionMethodName, request, &result) + if err != nil { + return protocol.GetTransactionResponse{}, err + } + return result, nil +} + +func (c *Client) GetTransactions(ctx context.Context, + request protocol.GetTransactionsRequest, +) (protocol.GetTransactionsResponse, error) { + var result protocol.GetTransactionsResponse + err := c.callResult(ctx, protocol.GetTransactionsMethodName, request, &result) + if err != nil { + return protocol.GetTransactionsResponse{}, err + } + return result, nil +} + +func (c *Client) GetVersionInfo(ctx context.Context) (protocol.GetVersionInfoResponse, error) { + var result protocol.GetVersionInfoResponse + err := c.callResult(ctx, protocol.GetVersionInfoMethodName, nil, &result) + if err != nil { + return protocol.GetVersionInfoResponse{}, err + } + return result, nil +} + +func (c *Client) SendTransaction(ctx context.Context, + request protocol.SendTransactionRequest, +) (protocol.SendTransactionResponse, error) { + var result protocol.SendTransactionResponse + err := c.callResult(ctx, protocol.SendTransactionMethodName, request, &result) + if err != nil { + return protocol.SendTransactionResponse{}, err + } + return result, nil +} + +func (c *Client) SimulateTransaction(ctx context.Context, + request protocol.SimulateTransactionRequest, +) (protocol.SimulateTransactionResponse, error) { + var result protocol.SimulateTransactionResponse + err := c.callResult(ctx, protocol.SimulateTransactionMethodName, request, &result) + if err != nil { + return protocol.SimulateTransactionResponse{}, err + } + return result, nil +} diff --git a/cmd/stellar-rpc/internal/db/event.go b/cmd/stellar-rpc/internal/db/event.go index faeae767..d6ba34e0 100644 --- a/cmd/stellar-rpc/internal/db/event.go +++ b/cmd/stellar-rpc/internal/db/event.go @@ -14,13 +14,13 @@ import ( "github.com/stellar/go/support/db" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" + + "github.com/stellar/stellar-rpc/protocol" ) const ( eventTableName = "events" firstLedger = uint32(2) - MinTopicCount = 1 - MaxTopicCount = 4 ) type NestedTopicArray [][][]byte @@ -34,7 +34,7 @@ type EventWriter interface { type EventReader interface { GetEvents( ctx context.Context, - cursorRange CursorRange, + cursorRange protocol.CursorRange, contractIDs [][]byte, topics NestedTopicArray, eventTypes []int, @@ -53,7 +53,6 @@ func NewEventReader(log *log.Entry, db db.SessionInterface, passphrase string) E return &eventHandler{log: log, db: db, passphrase: passphrase} } -//nolint:gocognit,cyclop,funlen func (eventHandler *eventHandler) InsertEvents(lcm xdr.LedgerCloseMeta) error { txCount := lcm.CountTransactions() @@ -115,8 +114,8 @@ func (eventHandler *eventHandler) InsertEvents(lcm xdr.LedgerCloseMeta) error { if e.Event.ContractId != nil { contractID = e.Event.ContractId[:] } - - id := Cursor{Ledger: lcm.LedgerSequence(), Tx: tx.Index, Op: 0, Event: uint32(index)}.String() + index32 := uint32(index) //nolint:gosec + id := protocol.Cursor{Ledger: lcm.LedgerSequence(), Tx: tx.Index, Op: 0, Event: index32}.String() eventBlob, err := e.MarshalBinary() if err != nil { return err @@ -128,8 +127,8 @@ func (eventHandler *eventHandler) InsertEvents(lcm xdr.LedgerCloseMeta) error { } // Encode the topics - topicList := make([][]byte, MaxTopicCount) - for index := 0; index < len(v0.Topics) && index < MaxTopicCount; index++ { + topicList := make([][]byte, protocol.MaxTopicCount) + for index := 0; index < len(v0.Topics) && index < protocol.MaxTopicCount; index++ { segment := v0.Topics[index] seg, err := segment.MarshalBinary() if err != nil { @@ -160,7 +159,7 @@ func (eventHandler *eventHandler) InsertEvents(lcm xdr.LedgerCloseMeta) error { type ScanFunction func( event xdr.DiagnosticEvent, - cursor Cursor, + cursor protocol.Cursor, ledgerCloseTimestamp int64, txHash *xdr.Hash, ) bool @@ -171,7 +170,7 @@ func (eventHandler *eventHandler) trimEvents(latestLedgerSeq uint32, retentionWi return nil } cutoff := latestLedgerSeq + 1 - retentionWindow - id := Cursor{Ledger: cutoff}.String() + id := protocol.Cursor{Ledger: cutoff}.String() _, err := sq.StatementBuilder. RunWith(eventHandler.stmtCache). @@ -189,7 +188,7 @@ func (eventHandler *eventHandler) trimEvents(latestLedgerSeq uint32, retentionWi //nolint:funlen,cyclop func (eventHandler *eventHandler) GetEvents( ctx context.Context, - cursorRange CursorRange, + cursorRange protocol.CursorRange, contractIDs [][]byte, topics NestedTopicArray, eventTypes []int, @@ -268,7 +267,7 @@ func (eventHandler *eventHandler) GetEvents( id, eventData, ledgerCloseTime := row.eventCursorID, row.eventData, row.ledgerCloseTime transactionHash := row.transactionHash - cur, err := ParseCursor(id) + cur, err := protocol.ParseCursor(id) if err != nil { return errors.Join(err, errors.New("failed to parse cursor")) } diff --git a/cmd/stellar-rpc/internal/db/event_test.go b/cmd/stellar-rpc/internal/db/event_test.go index f62c2fa6..b5a3cef4 100644 --- a/cmd/stellar-rpc/internal/db/event_test.go +++ b/cmd/stellar-rpc/internal/db/event_test.go @@ -14,6 +14,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" + "github.com/stellar/stellar-rpc/protocol" ) func transactionMetaWithEvents(events ...xdr.ContractEvent) xdr.TransactionMeta { @@ -170,9 +171,9 @@ func TestInsertEvents(t *testing.T) { require.NoError(t, err) eventReader := NewEventReader(log, db, passphrase) - start := Cursor{Ledger: 1} - end := Cursor{Ledger: 100} - cursorRange := CursorRange{Start: start, End: end} + start := protocol.Cursor{Ledger: 1} + end := protocol.Cursor{Ledger: 100} + cursorRange := protocol.CursorRange{Start: start, End: end} err = eventReader.GetEvents(ctx, cursorRange, nil, nil, nil, nil) require.NoError(t, err) diff --git a/cmd/stellar-rpc/internal/db/transaction_test.go b/cmd/stellar-rpc/internal/db/transaction_test.go index 4400c98c..bec43a02 100644 --- a/cmd/stellar-rpc/internal/db/transaction_test.go +++ b/cmd/stellar-rpc/internal/db/transaction_test.go @@ -15,6 +15,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" + "github.com/stellar/stellar-rpc/protocol" ) func TestTransactionNotFound(t *testing.T) { @@ -95,9 +96,9 @@ func TestTransactionFound(t *testing.T) { require.ErrorIs(t, err, ErrNoTransaction) eventReader := NewEventReader(log, db, passphrase) - start := Cursor{Ledger: 1} - end := Cursor{Ledger: 1000} - cursorRange := CursorRange{Start: start, End: end} + start := protocol.Cursor{Ledger: 1} + end := protocol.Cursor{Ledger: 1000} + cursorRange := protocol.CursorRange{Start: start, End: end} err = eventReader.GetEvents(ctx, cursorRange, nil, nil, nil, nil) require.NoError(t, err) diff --git a/cmd/stellar-rpc/internal/integrationtest/get_fee_stats_test.go b/cmd/stellar-rpc/internal/integrationtest/get_fee_stats_test.go index c17019f2..29e16b77 100644 --- a/cmd/stellar-rpc/internal/integrationtest/get_fee_stats_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/get_fee_stats_test.go @@ -11,7 +11,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) func TestGetFeeStats(t *testing.T) { @@ -37,12 +37,12 @@ func TestGetFeeStats(t *testing.T) { require.NoError(t, xdr.SafeUnmarshalBase64(classicTxResponse.ResultXDR, &classicTxResult)) classicFee := uint64(classicTxResult.FeeCharged) - var result methods.GetFeeStatsResult - if err := test.GetRPCLient().CallResult(context.Background(), "getFeeStats", nil, &result); err != nil { + result, err := test.GetRPCLient().GetFeeStats(context.Background()) + if err != nil { t.Fatalf("rpc call failed: %v", err) } - expectedResult := methods.GetFeeStatsResult{ - SorobanInclusionFee: methods.FeeDistribution{ + expectedResult := protocol.GetFeeStatsResponse{ + SorobanInclusionFee: protocol.FeeDistribution{ Max: sorobanInclusionFee, Min: sorobanInclusionFee, Mode: sorobanInclusionFee, @@ -60,7 +60,7 @@ func TestGetFeeStats(t *testing.T) { TransactionCount: 1, LedgerCount: result.SorobanInclusionFee.LedgerCount, }, - InclusionFee: methods.FeeDistribution{ + InclusionFee: protocol.FeeDistribution{ Max: classicFee, Min: classicFee, Mode: classicFee, diff --git a/cmd/stellar-rpc/internal/integrationtest/get_ledger_entries_test.go b/cmd/stellar-rpc/internal/integrationtest/get_ledger_entries_test.go index 5ef59646..0314baee 100644 --- a/cmd/stellar-rpc/internal/integrationtest/get_ledger_entries_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/get_ledger_entries_test.go @@ -11,7 +11,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) func TestGetLedgerEntriesNotFound(t *testing.T) { @@ -36,16 +36,15 @@ func TestGetLedgerEntriesNotFound(t *testing.T) { var keys []string keys = append(keys, keyB64) - request := methods.GetLedgerEntriesRequest{ + request := protocol.GetLedgerEntriesRequest{ Keys: keys, } - var result methods.GetLedgerEntriesResponse - err = client.CallResult(context.Background(), "getLedgerEntries", request, &result) + result, err := client.GetLedgerEntries(context.Background(), request) require.NoError(t, err) - assert.Equal(t, 0, len(result.Entries)) - assert.Greater(t, result.LatestLedger, uint32(0)) + assert.Empty(t, result.Entries) + assert.Positive(t, result.LatestLedger) } func TestGetLedgerEntriesInvalidParams(t *testing.T) { @@ -55,12 +54,13 @@ func TestGetLedgerEntriesInvalidParams(t *testing.T) { var keys []string keys = append(keys, "<>@@#$") - request := methods.GetLedgerEntriesRequest{ + request := protocol.GetLedgerEntriesRequest{ Keys: keys, } - var result methods.GetLedgerEntriesResponse - jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntries", request, &result).(*jrpc2.Error) + _, err := client.GetLedgerEntries(context.Background(), request) + var jsonRPCErr *jrpc2.Error + require.ErrorAs(t, err, &jsonRPCErr) assert.Contains(t, jsonRPCErr.Message, "cannot unmarshal key value") assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) } @@ -75,6 +75,7 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { Hash: contractHash, }, }) + require.NoError(t, err) // Doesn't exist. notFoundKeyB64, err := xdr.MarshalBase64(getCounterLedgerKey(contractID)) @@ -97,17 +98,16 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { require.NoError(t, err) keys := []string{contractCodeKeyB64, notFoundKeyB64, contractInstanceKeyB64} - request := methods.GetLedgerEntriesRequest{ + request := protocol.GetLedgerEntriesRequest{ Keys: keys, } - var result methods.GetLedgerEntriesResponse - err = test.GetRPCLient().CallResult(context.Background(), "getLedgerEntries", request, &result) + result, err := test.GetRPCLient().GetLedgerEntries(context.Background(), request) require.NoError(t, err) - require.Equal(t, 2, len(result.Entries)) - require.Greater(t, result.LatestLedger, uint32(0)) + require.Len(t, result.Entries, 2) + require.Positive(t, result.LatestLedger) - require.Greater(t, result.Entries[0].LastModifiedLedger, uint32(0)) + require.Positive(t, result.Entries[0].LastModifiedLedger) require.LessOrEqual(t, result.Entries[0].LastModifiedLedger, result.LatestLedger) require.NotNil(t, result.Entries[0].LiveUntilLedgerSeq) require.Greater(t, *result.Entries[0].LiveUntilLedgerSeq, result.LatestLedger) diff --git a/cmd/stellar-rpc/internal/integrationtest/get_ledgers_test.go b/cmd/stellar-rpc/internal/integrationtest/get_ledgers_test.go index 200b0cd9..30477ddd 100644 --- a/cmd/stellar-rpc/internal/integrationtest/get_ledgers_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/get_ledgers_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) func TestGetLedgers(t *testing.T) { @@ -16,56 +16,56 @@ func TestGetLedgers(t *testing.T) { client := test.GetRPCLient() // Get all ledgers - request := methods.GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 8, - Pagination: &methods.LedgerPaginationOptions{ + Pagination: &protocol.LedgerPaginationOptions{ Limit: 3, }, } - var result methods.GetLedgersResponse - err := client.CallResult(context.Background(), "getLedgers", request, &result) + + result, err := client.GetLedgers(context.Background(), request) require.NoError(t, err) assert.Len(t, result.Ledgers, 3) prevLedgers := result.Ledgers // Get ledgers using previous result's cursor - request = methods.GetLedgersRequest{ - Pagination: &methods.LedgerPaginationOptions{ + request = protocol.GetLedgersRequest{ + Pagination: &protocol.LedgerPaginationOptions{ Cursor: result.Cursor, Limit: 2, }, } - err = client.CallResult(context.Background(), "getLedgers", request, &result) + result, err = client.GetLedgers(context.Background(), request) require.NoError(t, err) assert.Len(t, result.Ledgers, 2) assert.Equal(t, prevLedgers[len(prevLedgers)-1].Sequence+1, result.Ledgers[0].Sequence) // Test with JSON format - request = methods.GetLedgersRequest{ + request = protocol.GetLedgersRequest{ StartLedger: 8, - Pagination: &methods.LedgerPaginationOptions{ + Pagination: &protocol.LedgerPaginationOptions{ Limit: 1, }, - Format: methods.FormatJSON, + Format: protocol.FormatJSON, } - err = client.CallResult(context.Background(), "getLedgers", request, &result) + result, err = client.GetLedgers(context.Background(), request) require.NoError(t, err) assert.NotEmpty(t, result.Ledgers[0].LedgerHeaderJSON) assert.NotEmpty(t, result.Ledgers[0].LedgerMetadataJSON) // Test invalid requests - invalidRequests := []methods.GetLedgersRequest{ + invalidRequests := []protocol.GetLedgersRequest{ {StartLedger: result.OldestLedger - 1}, {StartLedger: result.LatestLedger + 1}, { - Pagination: &methods.LedgerPaginationOptions{ + Pagination: &protocol.LedgerPaginationOptions{ Cursor: "invalid", }, }, } for _, req := range invalidRequests { - err = client.CallResult(context.Background(), "getLedgers", req, &result) + _, err = client.GetLedgers(context.Background(), req) assert.Error(t, err) } } diff --git a/cmd/stellar-rpc/internal/integrationtest/get_network_test.go b/cmd/stellar-rpc/internal/integrationtest/get_network_test.go index 633ad3ab..2c78baeb 100644 --- a/cmd/stellar-rpc/internal/integrationtest/get_network_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/get_network_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" ) func TestGetNetworkSucceeds(t *testing.T) { @@ -15,10 +14,7 @@ func TestGetNetworkSucceeds(t *testing.T) { client := test.GetRPCLient() - request := methods.GetNetworkRequest{} - - var result methods.GetNetworkResponse - err := client.CallResult(context.Background(), "getNetwork", request, &result) + result, err := client.GetNetwork(context.Background()) assert.NoError(t, err) assert.Equal(t, infrastructure.FriendbotURL, result.FriendbotURL) assert.Equal(t, infrastructure.StandaloneNetworkPassphrase, result.Passphrase) diff --git a/cmd/stellar-rpc/internal/integrationtest/get_transactions_test.go b/cmd/stellar-rpc/internal/integrationtest/get_transactions_test.go index 86f28e5e..20dfbc7a 100644 --- a/cmd/stellar-rpc/internal/integrationtest/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/get_transactions_test.go @@ -9,8 +9,9 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" + "github.com/stellar/stellar-rpc/client" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) // buildSetOptionsTxParams constructs the parameters necessary for creating a transaction from the given account. @@ -33,7 +34,7 @@ func buildSetOptionsTxParams(account txnbuild.SimpleAccount) txnbuild.Transactio // client - the JSON-RPC client used to send the transactions. // // Returns a slice of ledger numbers corresponding to where each transaction was recorded. -func sendTransactions(t *testing.T, client *infrastructure.Client) []uint32 { +func sendTransactions(t *testing.T, client *client.Client) []uint32 { kp := keypair.Root(infrastructure.StandaloneNetworkPassphrase) address := kp.Address() @@ -57,11 +58,10 @@ func TestGetTransactions(t *testing.T) { test.MasterAccount() // Get transactions across multiple ledgers - var result methods.GetTransactionsResponse - request := methods.GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: ledgers[0], } - err := client.CallResult(context.Background(), "getTransactions", request, &result) + result, err := client.GetTransactions(context.Background(), request) assert.NoError(t, err) assert.Len(t, result.Transactions, 3) assert.Equal(t, result.Transactions[0].Ledger, ledgers[0]) @@ -69,25 +69,25 @@ func TestGetTransactions(t *testing.T) { assert.Equal(t, result.Transactions[2].Ledger, ledgers[2]) // Get transactions with limit - request = methods.GetTransactionsRequest{ + request = protocol.GetTransactionsRequest{ StartLedger: ledgers[0], - Pagination: &methods.TransactionsPaginationOptions{ + Pagination: &protocol.TransactionsPaginationOptions{ Limit: 1, }, } - err = client.CallResult(context.Background(), "getTransactions", request, &result) + result, err = client.GetTransactions(context.Background(), request) assert.NoError(t, err) assert.Len(t, result.Transactions, 1) assert.Equal(t, result.Transactions[0].Ledger, ledgers[0]) // Get transactions using previous result's cursor - request = methods.GetTransactionsRequest{ - Pagination: &methods.TransactionsPaginationOptions{ + request = protocol.GetTransactionsRequest{ + Pagination: &protocol.TransactionsPaginationOptions{ Cursor: result.Cursor, Limit: 5, }, } - err = client.CallResult(context.Background(), "getTransactions", request, &result) + result, err = client.GetTransactions(context.Background(), request) assert.NoError(t, err) assert.Len(t, result.Transactions, 2) assert.Equal(t, result.Transactions[0].Ledger, ledgers[1]) diff --git a/cmd/stellar-rpc/internal/integrationtest/get_version_info_test.go b/cmd/stellar-rpc/internal/integrationtest/get_version_info_test.go index 3ba24694..14f2522b 100644 --- a/cmd/stellar-rpc/internal/integrationtest/get_version_info_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/get_version_info_test.go @@ -8,7 +8,6 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/config" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" ) func init() { @@ -20,8 +19,7 @@ func init() { func TestGetVersionInfoSucceeds(t *testing.T) { test := infrastructure.NewTest(t, nil) - var result methods.GetVersionInfoResponse - err := test.GetRPCLient().CallResult(context.Background(), "getVersionInfo", nil, &result) + result, err := test.GetRPCLient().GetVersionInfo(context.Background()) assert.NoError(t, err) assert.Equal(t, "0.0.0", result.Version) diff --git a/cmd/stellar-rpc/internal/integrationtest/health_test.go b/cmd/stellar-rpc/internal/integrationtest/health_test.go index daae79e0..87dcb750 100644 --- a/cmd/stellar-rpc/internal/integrationtest/health_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/health_test.go @@ -1,6 +1,7 @@ package integrationtest import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -12,7 +13,7 @@ import ( func TestHealth(t *testing.T) { test := infrastructure.NewTest(t, nil) - result, err := test.GetRPCHealth() + result, err := test.GetRPCLient().GetHealth(context.Background()) require.NoError(t, err) assert.Equal(t, "healthy", result.Status) assert.Equal(t, uint32(config.OneDayOfLedgers), result.LedgerRetentionWindow) diff --git a/cmd/stellar-rpc/internal/integrationtest/infrastructure/client.go b/cmd/stellar-rpc/internal/integrationtest/infrastructure/client.go index b787ddc8..544efb36 100644 --- a/cmd/stellar-rpc/internal/integrationtest/infrastructure/client.go +++ b/cmd/stellar-rpc/internal/integrationtest/infrastructure/client.go @@ -5,8 +5,6 @@ import ( "testing" "time" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,51 +13,19 @@ import ( "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/client" + "github.com/stellar/stellar-rpc/protocol" ) -// Client is a jrpc2 client which tolerates errors -type Client struct { - url string - cli *jrpc2.Client - opts *jrpc2.ClientOptions -} - -func NewClient(url string, opts *jrpc2.ClientOptions) *Client { - c := &Client{url: url, opts: opts} - c.refreshClient() - return c -} - -func (c *Client) refreshClient() { - if c.cli != nil { - c.cli.Close() - } - ch := jhttp.NewChannel(c.url, nil) - c.cli = jrpc2.NewClient(ch, c.opts) -} - -func (c *Client) CallResult(ctx context.Context, method string, params, result any) error { - err := c.cli.CallResult(ctx, method, params, result) - if err != nil { - // This is needed because of https://github.com/creachadair/jrpc2/issues/118 - c.refreshClient() - } - return err -} - -func (c *Client) Close() error { - return c.cli.Close() -} - -func getTransaction(t *testing.T, client *Client, hash string) methods.GetTransactionResponse { - var result methods.GetTransactionResponse +func getTransaction(t *testing.T, client *client.Client, hash string) protocol.GetTransactionResponse { + var result protocol.GetTransactionResponse for i := 0; i < 60; i++ { - request := methods.GetTransactionRequest{Hash: hash} - err := client.CallResult(context.Background(), "getTransaction", request, &result) + var err error + request := protocol.GetTransactionRequest{Hash: hash} + result, err = client.GetTransaction(context.Background(), request) require.NoError(t, err) - if result.Status == methods.TransactionStatusNotFound { + if result.Status == protocol.TransactionStatusNotFound { time.Sleep(time.Second) continue } @@ -70,15 +36,42 @@ func getTransaction(t *testing.T, client *Client, hash string) methods.GetTransa return result } -func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { +func logTransactionResult(t *testing.T, response protocol.GetTransactionResponse) { + var txResult xdr.TransactionResult + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &txResult)) + t.Logf("error: %#v\n", txResult) + + var txMeta xdr.TransactionMeta + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXDR, &txMeta)) + + if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { + if len(txMeta.V3.SorobanMeta.Events) > 0 { + t.Log("Contract events:") + for i, e := range txMeta.V3.SorobanMeta.Events { + t.Logf(" %d: %s\n", i, e) + } + } + + if len(txMeta.V3.SorobanMeta.DiagnosticEvents) > 0 { + t.Log("Diagnostic events:") + for i, d := range txMeta.V3.SorobanMeta.DiagnosticEvents { + t.Logf(" %d: %s\n", i, d) + } + } + } +} + +func SendSuccessfulTransaction(t *testing.T, client *client.Client, kp *keypair.Full, + transaction *txnbuild.Transaction, +) protocol.GetTransactionResponse { tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) require.NoError(t, err) b64, err := tx.Base64() require.NoError(t, err) - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - require.NoError(t, client.CallResult(context.Background(), "sendTransaction", request, &result)) + request := protocol.SendTransactionRequest{Transaction: b64} + result, err := client.SendTransaction(context.Background(), request) + require.NoError(t, err) expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) require.NoError(t, err) @@ -94,29 +87,8 @@ func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, t require.NotZero(t, result.LatestLedgerCloseTime) response := getTransaction(t, client, expectedHashHex) - if !assert.Equal(t, methods.TransactionStatusSuccess, response.Status) { - var txResult xdr.TransactionResult - require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &txResult)) - t.Logf("error: %#v\n", txResult) - - var txMeta xdr.TransactionMeta - require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXDR, &txMeta)) - - if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { - if len(txMeta.V3.SorobanMeta.Events) > 0 { - t.Log("Contract events:") - for i, e := range txMeta.V3.SorobanMeta.Events { - t.Logf(" %d: %s\n", i, e) - } - } - - if len(txMeta.V3.SorobanMeta.DiagnosticEvents) > 0 { - t.Log("Diagnostic events:") - for i, d := range txMeta.V3.SorobanMeta.DiagnosticEvents { - t.Logf(" %d: %s\n", i, d) - } - } - } + if !assert.Equal(t, protocol.TransactionStatusSuccess, response.Status) { + logTransactionResult(t, response) } require.NotNil(t, response.ResultXDR) @@ -127,7 +99,9 @@ func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, t return response } -func SimulateTransactionFromTxParams(t *testing.T, client *Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { +func SimulateTransactionFromTxParams(t *testing.T, client *client.Client, + params txnbuild.TransactionParams, +) protocol.SimulateTransactionResponse { savedAutoIncrement := params.IncrementSequenceNum params.IncrementSequenceNum = false tx, err := txnbuild.NewTransaction(params) @@ -135,14 +109,15 @@ func SimulateTransactionFromTxParams(t *testing.T, client *Client, params txnbui params.IncrementSequenceNum = savedAutoIncrement txB64, err := tx.Base64() require.NoError(t, err) - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) + request := protocol.SimulateTransactionRequest{Transaction: txB64} + response, err := client.SimulateTransaction(context.Background(), request) require.NoError(t, err) return response } -func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.TransactionParams, response methods.SimulateTransactionResponse) txnbuild.TransactionParams { +func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.TransactionParams, + response protocol.SimulateTransactionResponse, +) txnbuild.TransactionParams { if !assert.Empty(t, response.Error) { t.Log(response.Error) } @@ -190,7 +165,8 @@ func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.Transaction return params } -func PreflightTransactionParams(t *testing.T, client *Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { +func PreflightTransactionParams(t *testing.T, client *client.Client, params txnbuild.TransactionParams, +) txnbuild.TransactionParams { response := SimulateTransactionFromTxParams(t, client, params) // The preamble should be zero except for the special restore case require.Nil(t, response.RestorePreamble) diff --git a/cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go b/cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go index 938b14e5..9450175c 100644 --- a/cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/stellar-rpc/internal/integrationtest/infrastructure/test.go @@ -29,9 +29,10 @@ import ( "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" + "github.com/stellar/stellar-rpc/client" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/config" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) const ( @@ -101,7 +102,7 @@ type Test struct { rpcContainerSQLiteMountDir string rpcContainerLogsCommand *exec.Cmd - rpcClient *Client + rpcClient *client.Client coreClient *stellarcore.Client daemon *daemon.Daemon @@ -165,7 +166,7 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.spawnRPCDaemon() } - i.rpcClient = NewClient(i.GetSorobanRPCURL(), nil) + i.rpcClient = client.NewClient(i.GetSorobanRPCURL(), nil) if shouldWaitForRPC { i.waitForRPC() } @@ -220,7 +221,7 @@ func (i *Test) runRPCInContainer() bool { return i.rpcContainerVersion != "" } -func (i *Test) GetRPCLient() *Client { +func (i *Test) GetRPCLient() *client.Client { return i.rpcClient } @@ -333,7 +334,7 @@ func (i *Test) waitForRPC() { require.Eventually(i.t, func() bool { - result, err := i.GetRPCHealth() + result, err := i.GetRPCLient().GetHealth(context.Background()) return err == nil && result.Status == "healthy" }, 30*time.Second, @@ -626,23 +627,23 @@ func (i *Test) GetDaemon() *daemon.Daemon { return i.daemon } -func (i *Test) SendMasterOperation(op txnbuild.Operation) methods.GetTransactionResponse { +func (i *Test) SendMasterOperation(op txnbuild.Operation) protocol.GetTransactionResponse { params := CreateTransactionParams(i.MasterAccount(), op) tx, err := txnbuild.NewTransaction(params) assert.NoError(i.t, err) return i.SendMasterTransaction(tx) } -func (i *Test) SendMasterTransaction(tx *txnbuild.Transaction) methods.GetTransactionResponse { +func (i *Test) SendMasterTransaction(tx *txnbuild.Transaction) protocol.GetTransactionResponse { kp := keypair.Root(StandaloneNetworkPassphrase) return SendSuccessfulTransaction(i.t, i.rpcClient, kp, tx) } -func (i *Test) GetTransaction(hash string) methods.GetTransactionResponse { +func (i *Test) GetTransaction(hash string) protocol.GetTransactionResponse { return getTransaction(i.t, i.rpcClient, hash) } -func (i *Test) PreflightAndSendMasterOperation(op txnbuild.Operation) methods.GetTransactionResponse { +func (i *Test) PreflightAndSendMasterOperation(op txnbuild.Operation) protocol.GetTransactionResponse { params := CreateTransactionParams( i.MasterAccount(), op, @@ -653,23 +654,23 @@ func (i *Test) PreflightAndSendMasterOperation(op txnbuild.Operation) methods.Ge return i.SendMasterTransaction(tx) } -func (i *Test) UploadHelloWorldContract() (methods.GetTransactionResponse, xdr.Hash) { +func (i *Test) UploadHelloWorldContract() (protocol.GetTransactionResponse, xdr.Hash) { contractBinary := GetHelloWorldContract() return i.uploadContract(contractBinary) } -func (i *Test) UploadNoArgConstructorContract() (methods.GetTransactionResponse, xdr.Hash) { +func (i *Test) UploadNoArgConstructorContract() (protocol.GetTransactionResponse, xdr.Hash) { contractBinary := GetNoArgConstructorContract() return i.uploadContract(contractBinary) } -func (i *Test) uploadContract(contractBinary []byte) (methods.GetTransactionResponse, xdr.Hash) { +func (i *Test) uploadContract(contractBinary []byte) (protocol.GetTransactionResponse, xdr.Hash) { contractHash := xdr.Hash(sha256.Sum256(contractBinary)) op := CreateUploadWasmOperation(i.MasterAccount().GetAccountID(), contractBinary) return i.PreflightAndSendMasterOperation(op), contractHash } -func (i *Test) CreateHelloWorldContract() (methods.GetTransactionResponse, [32]byte, xdr.Hash) { +func (i *Test) CreateHelloWorldContract() (protocol.GetTransactionResponse, [32]byte, xdr.Hash) { contractBinary := GetHelloWorldContract() _, contractHash := i.uploadContract(contractBinary) salt := xdr.Uint256(testSalt) @@ -679,17 +680,11 @@ func (i *Test) CreateHelloWorldContract() (methods.GetTransactionResponse, [32]b return i.PreflightAndSendMasterOperation(op), contractID, contractHash } -func (i *Test) InvokeHostFunc(contractID xdr.Hash, method string, args ...xdr.ScVal) methods.GetTransactionResponse { +func (i *Test) InvokeHostFunc(contractID xdr.Hash, method string, args ...xdr.ScVal) protocol.GetTransactionResponse { op := CreateInvokeHostOperation(i.MasterAccount().GetAccountID(), contractID, method, args...) return i.PreflightAndSendMasterOperation(op) } -func (i *Test) GetRPCHealth() (methods.HealthCheckResult, error) { - var result methods.HealthCheckResult - err := i.rpcClient.CallResult(context.Background(), "getHealth", nil, &result) - return result, err -} - func (i *Test) fillContainerPorts() { getPublicPort := func(service string, privatePort int) uint16 { var port uint16 diff --git a/cmd/stellar-rpc/internal/integrationtest/migrate_test.go b/cmd/stellar-rpc/internal/integrationtest/migrate_test.go index dc6cbff2..eeabbb1a 100644 --- a/cmd/stellar-rpc/internal/integrationtest/migrate_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/migrate_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) // Test that every Stellar RPC version (within the current protocol) can migrate cleanly to the current version @@ -58,26 +58,24 @@ func testMigrateFromVersion(t *testing.T, version string) { }) // make sure that the transaction submitted before and its events exist in current RPC - var transactionsResult methods.GetTransactionsResponse - getTransactions := methods.GetTransactionsRequest{ + getTransactions := protocol.GetTransactionsRequest{ StartLedger: submitTransactionResponse.Ledger, - Pagination: &methods.TransactionsPaginationOptions{ + Pagination: &protocol.TransactionsPaginationOptions{ Limit: 1, }, } - err := test.GetRPCLient().CallResult(context.Background(), "getTransactions", getTransactions, &transactionsResult) + transactionsResult, err := test.GetRPCLient().GetTransactions(context.Background(), getTransactions) require.NoError(t, err) require.Len(t, transactionsResult.Transactions, 1) require.Equal(t, submitTransactionResponse.Ledger, transactionsResult.Transactions[0].Ledger) - var eventsResult methods.GetEventsResponse - getEventsRequest := methods.GetEventsRequest{ + getEventsRequest := protocol.GetEventsRequest{ StartLedger: submitTransactionResponse.Ledger, - Pagination: &methods.PaginationOptions{ + Pagination: &protocol.PaginationOptions{ Limit: 1, }, } - err = test.GetRPCLient().CallResult(context.Background(), "getEvents", getEventsRequest, &eventsResult) + eventsResult, err := test.GetRPCLient().GetEvents(context.Background(), getEventsRequest) require.NoError(t, err) require.Len(t, eventsResult.Events, 1) require.Equal(t, submitTransactionResponse.Ledger, uint32(eventsResult.Events[0].Ledger)) diff --git a/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go b/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go index a9d4bf73..93e8c6d6 100644 --- a/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/simulate_transaction_test.go @@ -13,8 +13,9 @@ import ( "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" + "github.com/stellar/stellar-rpc/client" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) func TestSimulateTransactionSucceeds(t *testing.T) { @@ -77,7 +78,7 @@ func TestSimulateTransactionSucceeds(t *testing.T) { require.Len(t, result.StateChanges, 1) require.Nil(t, result.StateChanges[0].BeforeXDR) require.NotNil(t, result.StateChanges[0].AfterXDR) - require.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) + require.Equal(t, protocol.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) var after xdr.LedgerEntry require.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].AfterXDR, &after)) require.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) @@ -183,9 +184,8 @@ func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { txB64, err := tx.Base64() require.NoError(t, err) - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) + request := protocol.SimulateTransactionRequest{Transaction: txB64} + response, err := test.GetRPCLient().SimulateTransaction(context.Background(), request) require.NoError(t, err) require.Empty(t, response.Error) @@ -321,7 +321,7 @@ func TestSimulateTransactionMultipleOperations(t *testing.T) { result := infrastructure.SimulateTransactionFromTxParams(t, client, params) require.Equal( t, - methods.SimulateTransactionResponse{ + protocol.SimulateTransactionResponse{ Error: "Transaction contains more than one operation", }, result, @@ -340,7 +340,7 @@ func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { result := infrastructure.SimulateTransactionFromTxParams(t, client, params) require.Equal( t, - methods.SimulateTransactionResponse{ + protocol.SimulateTransactionResponse{ Error: "Transaction contains unsupported operation type: OperationTypeBumpSequence", }, result, @@ -352,9 +352,8 @@ func TestSimulateTransactionUnmarshalError(t *testing.T) { client := test.GetRPCLient() - request := methods.SimulateTransactionRequest{Transaction: "invalid"} - var result methods.SimulateTransactionResponse - err := client.CallResult(context.Background(), "simulateTransaction", request, &result) + request := protocol.SimulateTransactionRequest{Transaction: "invalid"} + result, err := client.SimulateTransaction(context.Background(), request) require.NoError(t, err) require.Equal( t, @@ -377,12 +376,11 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { keyB64, err := xdr.MarshalBase64(key) require.NoError(t, err) - getLedgerEntriesRequest := methods.GetLedgerEntriesRequest{ + getLedgerEntriesRequest := protocol.GetLedgerEntriesRequest{ Keys: []string{keyB64}, } - var getLedgerEntriesResult methods.GetLedgerEntriesResponse client := test.GetRPCLient() - err = client.CallResult(context.Background(), "getLedgerEntries", getLedgerEntriesRequest, &getLedgerEntriesResult) + getLedgerEntriesResult, err := client.GetLedgerEntries(context.Background(), getLedgerEntriesRequest) require.NoError(t, err) var entry xdr.LedgerEntryData @@ -409,7 +407,7 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { }, ) - err = client.CallResult(context.Background(), "getLedgerEntries", getLedgerEntriesRequest, &getLedgerEntriesResult) + getLedgerEntriesResult, err = client.GetLedgerEntries(context.Background(), getLedgerEntriesRequest) require.NoError(t, err) ledgerEntry = getLedgerEntriesResult.Entries[0] @@ -455,7 +453,7 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { test.MasterAccount(), &txnbuild.RestoreFootprint{}, ), - methods.SimulateTransactionResponse{ + protocol.SimulateTransactionResponse{ TransactionDataXDR: simulationResult.RestorePreamble.TransactionDataXDR, MinResourceFee: simulationResult.RestorePreamble.MinResourceFee, }, @@ -492,17 +490,16 @@ func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { return key } -func waitUntilLedgerEntryTTL(t *testing.T, client *infrastructure.Client, ledgerKey xdr.LedgerKey) { +func waitUntilLedgerEntryTTL(t *testing.T, client *client.Client, ledgerKey xdr.LedgerKey) { keyB64, err := xdr.MarshalBase64(ledgerKey) require.NoError(t, err) - request := methods.GetLedgerEntriesRequest{ + request := protocol.GetLedgerEntriesRequest{ Keys: []string{keyB64}, } ttled := false for i := 0; i < 50; i++ { - var result methods.GetLedgerEntriesResponse var entry xdr.LedgerEntryData - err := client.CallResult(context.Background(), "getLedgerEntries", request, &result) + result, err := client.GetLedgerEntries(context.Background(), request) require.NoError(t, err) require.NotEmpty(t, result.Entries) require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].DataXDR, &entry)) @@ -558,9 +555,8 @@ func TestSimulateInvokePrng_u64_in_range(t *testing.T) { txB64, err := tx.Base64() require.NoError(t, err) - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) + request := protocol.SimulateTransactionRequest{Transaction: txB64} + response, err := test.GetRPCLient().SimulateTransaction(context.Background(), request) require.NoError(t, err) require.Empty(t, response.Error) @@ -606,9 +602,8 @@ func TestSimulateSystemEvent(t *testing.T) { txB64, err := tx.Base64() require.NoError(t, err) - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) + request := protocol.SimulateTransactionRequest{Transaction: txB64} + response, err := test.GetRPCLient().SimulateTransaction(context.Background(), request) require.NoError(t, err) require.Empty(t, response.Error) diff --git a/cmd/stellar-rpc/internal/integrationtest/transaction_test.go b/cmd/stellar-rpc/internal/integrationtest/transaction_test.go index 996e9251..cafc4ed1 100644 --- a/cmd/stellar-rpc/internal/integrationtest/transaction_test.go +++ b/cmd/stellar-rpc/internal/integrationtest/transaction_test.go @@ -14,7 +14,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/integrationtest/infrastructure" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" + "github.com/stellar/stellar-rpc/protocol" ) func TestSendTransactionSucceedsWithoutResults(t *testing.T) { @@ -81,10 +81,9 @@ func TestSendTransactionBadSequence(t *testing.T) { b64, err := tx.Base64() require.NoError(t, err) - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse + request := protocol.SendTransactionRequest{Transaction: b64} client := test.GetRPCLient() - err = client.CallResult(context.Background(), "sendTransaction", request, &result) + result, err := client.SendTransaction(context.Background(), request) require.NoError(t, err) require.NotZero(t, result.LatestLedger) @@ -122,9 +121,8 @@ func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { b64, err := tx.Base64() require.NoError(t, err) - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", request, &result) + request := protocol.SendTransactionRequest{Transaction: b64} + result, err := client.SendTransaction(context.Background(), request) require.NoError(t, err) require.Equal(t, proto.TXStatusError, result.Status) @@ -162,9 +160,8 @@ func TestSendTransactionFailedInLedger(t *testing.T) { b64, err := tx.Base64() require.NoError(t, err) - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", request, &result) + request := protocol.SendTransactionRequest{Transaction: b64} + result, err := client.SendTransaction(context.Background(), request) require.NoError(t, err) expectedHashHex, err := tx.HashHex(infrastructure.StandaloneNetworkPassphrase) @@ -181,7 +178,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { require.NotZero(t, result.LatestLedgerCloseTime) response := test.GetTransaction(expectedHashHex) - require.Equal(t, methods.TransactionStatusFailed, response.Status) + require.Equal(t, protocol.TransactionStatusFailed, response.Status) var transactionResult xdr.TransactionResult require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &transactionResult)) require.Equal(t, xdr.TransactionResultCodeTxFailed, transactionResult.Result.Code) @@ -196,9 +193,10 @@ func TestSendTransactionFailedInvalidXDR(t *testing.T) { client := test.GetRPCLient() - request := methods.SendTransactionRequest{Transaction: "abcdef"} - var response methods.SendTransactionResponse - jsonRPCErr := client.CallResult(context.Background(), "sendTransaction", request, &response).(*jrpc2.Error) + request := protocol.SendTransactionRequest{Transaction: "abcdef"} + _, err := client.SendTransaction(context.Background(), request) + var jsonRPCErr *jrpc2.Error + require.ErrorAs(t, err, &jsonRPCErr) require.Equal(t, "invalid_xdr", jsonRPCErr.Message) require.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) } diff --git a/cmd/stellar-rpc/internal/jsonrpc.go b/cmd/stellar-rpc/internal/jsonrpc.go index b8e26ad0..9285d14a 100644 --- a/cmd/stellar-rpc/internal/jsonrpc.go +++ b/cmd/stellar-rpc/internal/jsonrpc.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/handler" @@ -24,6 +25,7 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/feewindow" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/methods" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/network" + "github.com/stellar/stellar-rpc/protocol" ) const ( @@ -79,7 +81,7 @@ func decorateHandlers(daemon interfaces.Daemon, logger *log.Entry, m handler.Map result, err := h(ctx, r) duration := time.Since(startTime) label := prometheus.Labels{"endpoint": r.Method(), "status": "ok"} - simulateTransactionResponse, ok := result.(methods.SimulateTransactionResponse) + simulateTransactionResponse, ok := result.(protocol.SimulateTransactionResponse) if ok && simulateTransactionResponse.Error != "" { label["status"] = "error" } else if err != nil { @@ -133,6 +135,17 @@ func logResponse(logger *log.Entry, reqID string, duration time.Duration, status } } +func toSnakeCase(s string) string { + var result string + for _, v := range s { + if unicode.IsUpper(v) { + result += "_" + } + result += string(v) + } + return strings.ToLower(result) +} + // NewJSONRPCHandler constructs a Handler instance func NewJSONRPCHandler(cfg *config.Config, params HandlerParams) Handler { bridgeOptions := jhttp.BridgeOptions{ @@ -151,15 +164,15 @@ func NewJSONRPCHandler(cfg *config.Config, params HandlerParams) Handler { requestDurationLimit time.Duration }{ { - methodName: "getHealth", + methodName: protocol.GetHealthMethodName, underlyingHandler: methods.NewHealthCheck( retentionWindow, params.LedgerReader, cfg.MaxHealthyLedgerLatency), - longName: "get_health", + longName: toSnakeCase(protocol.GetHealthMethodName), queueLimit: cfg.RequestBacklogGetHealthQueueLimit, requestDurationLimit: cfg.MaxGetHealthExecutionDuration, }, { - methodName: "getEvents", + methodName: protocol.GetEventsMethodName, underlyingHandler: methods.NewGetEventsHandler( params.Logger, params.EventReader, @@ -168,88 +181,88 @@ func NewJSONRPCHandler(cfg *config.Config, params HandlerParams) Handler { params.LedgerReader, ), - longName: "get_events", + longName: toSnakeCase(protocol.GetEventsMethodName), queueLimit: cfg.RequestBacklogGetEventsQueueLimit, requestDurationLimit: cfg.MaxGetEventsExecutionDuration, }, { - methodName: "getNetwork", + methodName: protocol.GetNetworkMethodName, underlyingHandler: methods.NewGetNetworkHandler( cfg.NetworkPassphrase, cfg.FriendbotURL, params.LedgerEntryReader, params.LedgerReader, ), - longName: "get_network", + longName: toSnakeCase(protocol.GetNetworkMethodName), queueLimit: cfg.RequestBacklogGetNetworkQueueLimit, requestDurationLimit: cfg.MaxGetNetworkExecutionDuration, }, { - methodName: "getVersionInfo", + methodName: protocol.GetVersionInfoMethodName, underlyingHandler: methods.NewGetVersionInfoHandler(params.Logger, params.LedgerEntryReader, params.LedgerReader, params.Daemon), - longName: "get_version_info", + longName: toSnakeCase(protocol.GetVersionInfoMethodName), queueLimit: cfg.RequestBacklogGetVersionInfoQueueLimit, requestDurationLimit: cfg.MaxGetVersionInfoExecutionDuration, }, { - methodName: "getLatestLedger", + methodName: protocol.GetLatestLedgerMethodName, underlyingHandler: methods.NewGetLatestLedgerHandler(params.LedgerEntryReader, params.LedgerReader), - longName: "get_latest_ledger", + longName: toSnakeCase(protocol.GetLatestLedgerMethodName), queueLimit: cfg.RequestBacklogGetLatestLedgerQueueLimit, requestDurationLimit: cfg.MaxGetLatestLedgerExecutionDuration, }, { - methodName: "getLedgers", + methodName: protocol.GetLedgersMethodName, underlyingHandler: methods.NewGetLedgersHandler(params.LedgerReader, cfg.MaxLedgersLimit, cfg.DefaultLedgersLimit), - longName: "get_ledgers", + longName: toSnakeCase(protocol.GetLedgersMethodName), queueLimit: cfg.RequestBacklogGetLedgersQueueLimit, requestDurationLimit: cfg.MaxGetLedgersExecutionDuration, }, { - methodName: "getLedgerEntries", + methodName: protocol.GetLedgerEntriesMethodName, underlyingHandler: methods.NewGetLedgerEntriesHandler(params.Logger, params.LedgerEntryReader), - longName: "get_ledger_entries", + longName: toSnakeCase(protocol.GetLedgerEntriesMethodName), queueLimit: cfg.RequestBacklogGetLedgerEntriesQueueLimit, requestDurationLimit: cfg.MaxGetLedgerEntriesExecutionDuration, }, { - methodName: "getTransaction", + methodName: protocol.GetTransactionMethodName, underlyingHandler: methods.NewGetTransactionHandler(params.Logger, params.TransactionReader, params.LedgerReader), - longName: "get_transaction", + longName: toSnakeCase(protocol.GetTransactionMethodName), queueLimit: cfg.RequestBacklogGetTransactionQueueLimit, requestDurationLimit: cfg.MaxGetTransactionExecutionDuration, }, { - methodName: "getTransactions", + methodName: protocol.GetTransactionsMethodName, underlyingHandler: methods.NewGetTransactionsHandler(params.Logger, params.LedgerReader, cfg.MaxTransactionsLimit, cfg.DefaultTransactionsLimit, cfg.NetworkPassphrase), - longName: "get_transactions", + longName: toSnakeCase(protocol.GetTransactionsMethodName), queueLimit: cfg.RequestBacklogGetTransactionsQueueLimit, requestDurationLimit: cfg.MaxGetTransactionsExecutionDuration, }, { - methodName: "sendTransaction", + methodName: protocol.SendTransactionMethodName, underlyingHandler: methods.NewSendTransactionHandler( params.Daemon, params.Logger, params.LedgerReader, cfg.NetworkPassphrase), - longName: "send_transaction", + longName: toSnakeCase(protocol.SendTransactionMethodName), queueLimit: cfg.RequestBacklogSendTransactionQueueLimit, requestDurationLimit: cfg.MaxSendTransactionExecutionDuration, }, { - methodName: "simulateTransaction", + methodName: protocol.SimulateTransactionMethodName, underlyingHandler: methods.NewSimulateTransactionHandler( params.Logger, params.LedgerEntryReader, params.LedgerReader, params.Daemon, params.PreflightGetter), - longName: "simulate_transaction", + longName: toSnakeCase(protocol.SimulateTransactionMethodName), queueLimit: cfg.RequestBacklogSimulateTransactionQueueLimit, requestDurationLimit: cfg.MaxSimulateTransactionExecutionDuration, }, { - methodName: "getFeeStats", + methodName: protocol.GetFeeStatsMethodName, underlyingHandler: methods.NewGetFeeStatsHandler(params.FeeStatWindows, params.LedgerReader, params.Logger), - longName: "get_fee_stats", + longName: toSnakeCase(protocol.GetFeeStatsMethodName), queueLimit: cfg.RequestBacklogGetFeeStatsTransactionQueueLimit, requestDurationLimit: cfg.MaxGetFeeStatsExecutionDuration, }, diff --git a/cmd/stellar-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go b/cmd/stellar-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go index 0f3ca5e2..719ea986 100644 --- a/cmd/stellar-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go +++ b/cmd/stellar-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go @@ -2,6 +2,8 @@ package ledgerbucketwindow import ( "fmt" + + "github.com/stellar/stellar-rpc/protocol" ) // LedgerBucketWindow is a sequence of buckets associated to a ledger window. @@ -73,6 +75,13 @@ type LedgerRange struct { LastLedger LedgerInfo } +func (lr LedgerRange) ToLedgerSeqRange() protocol.LedgerSeqRange { + return protocol.LedgerSeqRange{ + FirstLedger: lr.FirstLedger.Sequence, + LastLedger: lr.LastLedger.Sequence, + } +} + func (w *LedgerBucketWindow[T]) GetLedgerRange() LedgerRange { length := w.Len() if length == 0 { diff --git a/cmd/stellar-rpc/internal/methods/get_events.go b/cmd/stellar-rpc/internal/methods/get_events.go index 77ec25eb..55b04332 100644 --- a/cmd/stellar-rpc/internal/methods/get_events.go +++ b/cmd/stellar-rpc/internal/methods/get_events.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "time" "github.com/creachadair/jrpc2" @@ -17,333 +16,14 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) const ( - LedgerScanLimit = 10000 - maxContractIDsLimit = 5 - maxTopicsLimit = 5 - maxFiltersLimit = 5 - maxEventTypes = 3 + LedgerScanLimit = 10000 + maxEventTypes = 3 ) -type eventTypeSet map[string]interface{} - -func (e eventTypeSet) valid() error { - for key := range e { - switch key { - case EventTypeSystem, EventTypeContract, EventTypeDiagnostic: - // ok - default: - return errors.New("if set, type must be either 'system', 'contract' or 'diagnostic'") - } - } - return nil -} - -func (e *eventTypeSet) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - *e = map[string]interface{}{} - return nil - } - var joined string - if err := json.Unmarshal(data, &joined); err != nil { - return err - } - *e = map[string]interface{}{} - if len(joined) == 0 { - return nil - } - for _, key := range strings.Split(joined, ",") { - (*e)[key] = nil - } - return nil -} - -func (e eventTypeSet) MarshalJSON() ([]byte, error) { - keys := make([]string, 0, len(e)) - for key := range e { - keys = append(keys, key) - } - return json.Marshal(strings.Join(keys, ",")) -} - -func (e eventTypeSet) Keys() []string { - keys := make([]string, 0, len(e)) - for key := range e { - keys = append(keys, key) - } - return keys -} - -func (e eventTypeSet) matches(event xdr.ContractEvent) bool { - if len(e) == 0 { - return true - } - _, ok := e[getEventTypeFromEventTypeXDR()[event.Type]] - return ok -} - -type EventInfo struct { - EventType string `json:"type"` - Ledger int32 `json:"ledger"` - LedgerClosedAt string `json:"ledgerClosedAt"` - ContractID string `json:"contractId"` - ID string `json:"id"` - - // Deprecated: PagingToken field is deprecated, please use Cursor at top level for pagination - PagingToken string `json:"pagingToken"` - InSuccessfulContractCall bool `json:"inSuccessfulContractCall"` - TransactionHash string `json:"txHash"` - - // TopicXDR is a base64-encoded list of ScVals - TopicXDR []string `json:"topic,omitempty"` - TopicJSON []json.RawMessage `json:"topicJson,omitempty"` - - // ValueXDR is a base64-encoded ScVal - ValueXDR string `json:"value,omitempty"` - ValueJSON json.RawMessage `json:"valueJson,omitempty"` -} - -type GetEventsRequest struct { - StartLedger uint32 `json:"startLedger,omitempty"` - EndLedger uint32 `json:"endLedger,omitempty"` - Filters []EventFilter `json:"filters"` - Pagination *PaginationOptions `json:"pagination,omitempty"` - Format string `json:"xdrFormat,omitempty"` -} - -func (g *GetEventsRequest) Valid(maxLimit uint) error { - if err := IsValidFormat(g.Format); err != nil { - return err - } - - // Validate the paging limit (if it exists) - if g.Pagination != nil && g.Pagination.Cursor != nil { - if g.StartLedger != 0 || g.EndLedger != 0 { - return errors.New("ledger ranges and cursor cannot both be set") - } - } else if g.StartLedger <= 0 { - // Note: Endledger == 0 indicates it's unset (unlimited) - return errors.New("startLedger must be positive") - } - - if g.Pagination != nil && g.Pagination.Limit > maxLimit { - return fmt.Errorf("limit must not exceed %d", maxLimit) - } - - // Validate filters - if len(g.Filters) > maxFiltersLimit { - return errors.New("maximum 5 filters per request") - } - for i, filter := range g.Filters { - if err := filter.Valid(); err != nil { - return fmt.Errorf("filter %d invalid: %w", i+1, err) - } - } - - return nil -} - -func (g *GetEventsRequest) Matches(event xdr.DiagnosticEvent) bool { - if len(g.Filters) == 0 { - return true - } - for _, filter := range g.Filters { - if filter.Matches(event) { - return true - } - } - return false -} - -const ( - EventTypeSystem = "system" - EventTypeContract = "contract" - EventTypeDiagnostic = "diagnostic" -) - -func getEventTypeFromEventTypeXDR() map[xdr.ContractEventType]string { - return map[xdr.ContractEventType]string{ - xdr.ContractEventTypeSystem: EventTypeSystem, - xdr.ContractEventTypeContract: EventTypeContract, - xdr.ContractEventTypeDiagnostic: EventTypeDiagnostic, - } -} - -func getEventTypeXDRFromEventType() map[string]xdr.ContractEventType { - return map[string]xdr.ContractEventType{ - EventTypeSystem: xdr.ContractEventTypeSystem, - EventTypeContract: xdr.ContractEventTypeContract, - EventTypeDiagnostic: xdr.ContractEventTypeDiagnostic, - } -} - -type EventFilter struct { - EventType eventTypeSet `json:"type,omitempty"` - ContractIDs []string `json:"contractIds,omitempty"` - Topics []TopicFilter `json:"topics,omitempty"` -} - -func (e *EventFilter) Valid() error { - if err := e.EventType.valid(); err != nil { - return errors.Wrap(err, "filter type invalid") - } - if len(e.ContractIDs) > maxContractIDsLimit { - return errors.New("maximum 5 contract IDs per filter") - } - if len(e.Topics) > maxTopicsLimit { - return errors.New("maximum 5 topics per filter") - } - for i, id := range e.ContractIDs { - _, err := strkey.Decode(strkey.VersionByteContract, id) - if err != nil { - return fmt.Errorf("contract ID %d invalid", i+1) - } - } - for i, topic := range e.Topics { - if err := topic.Valid(); err != nil { - return fmt.Errorf("topic %d invalid: %w", i+1, err) - } - } - return nil -} - -func (e *EventFilter) Matches(event xdr.DiagnosticEvent) bool { - return e.EventType.matches(event.Event) && e.matchesContractIDs(event.Event) && e.matchesTopics(event.Event) -} - -func (e *EventFilter) matchesContractIDs(event xdr.ContractEvent) bool { - if len(e.ContractIDs) == 0 { - return true - } - if event.ContractId == nil { - return false - } - needle := strkey.MustEncode(strkey.VersionByteContract, (*event.ContractId)[:]) - for _, id := range e.ContractIDs { - if id == needle { - return true - } - } - return false -} - -func (e *EventFilter) matchesTopics(event xdr.ContractEvent) bool { - if len(e.Topics) == 0 { - return true - } - v0, ok := event.Body.GetV0() - if !ok { - return false - } - for _, topicFilter := range e.Topics { - if topicFilter.Matches(v0.Topics) { - return true - } - } - return false -} - -type TopicFilter []SegmentFilter - -func (t *TopicFilter) Valid() error { - if len(*t) < db.MinTopicCount { - return errors.New("topic must have at least one segment") - } - if len(*t) > db.MaxTopicCount { - return errors.New("topic cannot have more than 4 segments") - } - for i, segment := range *t { - if err := segment.Valid(); err != nil { - return fmt.Errorf("segment %d invalid: %w", i+1, err) - } - } - return nil -} - -// An event matches a topic filter iff: -// - the event has EXACTLY as many topic segments as the filter AND -// - each segment either: matches exactly OR is a wildcard. -func (t TopicFilter) Matches(event []xdr.ScVal) bool { - if len(event) != len(t) { - return false - } - - for i, segmentFilter := range t { - if !segmentFilter.Matches(event[i]) { - return false - } - } - - return true -} - -type SegmentFilter struct { - wildcard *string - scval *xdr.ScVal -} - -func (s *SegmentFilter) Matches(segment xdr.ScVal) bool { - if s.wildcard != nil && *s.wildcard == "*" { - return true - } else if s.scval != nil { - if !s.scval.Equals(segment) { - return false - } - } else { - panic("invalid segmentFilter") - } - - return true -} - -func (s *SegmentFilter) Valid() error { - if s.wildcard != nil && s.scval != nil { - return errors.New("cannot set both wildcard and scval") - } - if s.wildcard == nil && s.scval == nil { - return errors.New("must set either wildcard or scval") - } - if s.wildcard != nil && *s.wildcard != "*" { - return errors.New("wildcard must be '*'") - } - return nil -} - -func (s *SegmentFilter) UnmarshalJSON(p []byte) error { - s.wildcard = nil - s.scval = nil - - var tmp string - if err := json.Unmarshal(p, &tmp); err != nil { - return err - } - if tmp == "*" { - s.wildcard = &tmp - } else { - var out xdr.ScVal - if err := xdr.SafeUnmarshalBase64(tmp, &out); err != nil { - return err - } - s.scval = &out - } - return nil -} - -type PaginationOptions struct { - Cursor *db.Cursor `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` -} - -type GetEventsResponse struct { - Events []EventInfo `json:"events"` - LatestLedger uint32 `json:"latestLedger"` - // Cursor represents last populated event ID if total events reach the limit - // or end of the search window - Cursor string `json:"cursor"` -} - type eventsRPCHandler struct { dbReader db.EventReader maxLimit uint @@ -352,8 +32,8 @@ type eventsRPCHandler struct { ledgerReader db.LedgerReader } -func combineContractIDs(filters []EventFilter) ([][]byte, error) { - contractIDSet := set.NewSet[string](maxFiltersLimit * maxContractIDsLimit) +func combineContractIDs(filters []protocol.EventFilter) ([][]byte, error) { + contractIDSet := set.NewSet[string](protocol.MaxFiltersLimit * protocol.MaxContractIDsLimit) contractIDs := make([][]byte, 0, len(contractIDSet)) for _, filter := range filters { @@ -372,12 +52,12 @@ func combineContractIDs(filters []EventFilter) ([][]byte, error) { return contractIDs, nil } -func combineEventTypes(filters []EventFilter) []int { +func combineEventTypes(filters []protocol.EventFilter) []int { eventTypes := set.NewSet[int](maxEventTypes) for _, filter := range filters { for _, eventType := range filter.EventType.Keys() { - eventTypeXDR := getEventTypeXDRFromEventType()[eventType] + eventTypeXDR := protocol.GetEventTypeXDRFromEventType()[eventType] eventTypes.Add(int(eventTypeXDR)) } } @@ -388,8 +68,8 @@ func combineEventTypes(filters []EventFilter) []int { return uniqueEventTypes } -func combineTopics(filters []EventFilter) ([][][]byte, error) { - encodedTopicsList := make([][][]byte, db.MaxTopicCount) +func combineTopics(filters []protocol.EventFilter) ([][][]byte, error) { + encodedTopicsList := make([][][]byte, protocol.MaxTopicCount) for _, filter := range filters { if len(filter.Topics) == 0 { @@ -398,8 +78,8 @@ func combineTopics(filters []EventFilter) ([][][]byte, error) { for _, topicFilter := range filter.Topics { for i, segmentFilter := range topicFilter { - if segmentFilter.wildcard == nil && segmentFilter.scval != nil { - encodedTopic, err := segmentFilter.scval.MarshalBinary() + if segmentFilter.Wildcard == nil && segmentFilter.ScVal != nil { + encodedTopic, err := segmentFilter.ScVal.MarshalBinary() if err != nil { return [][][]byte{}, fmt.Errorf("failed to marshal segment: %w", err) } @@ -413,27 +93,31 @@ func combineTopics(filters []EventFilter) ([][][]byte, error) { } type entry struct { - cursor db.Cursor + cursor protocol.Cursor ledgerCloseTimestamp int64 event xdr.DiagnosticEvent txHash *xdr.Hash } -func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsRequest) (GetEventsResponse, error) { +// TODO: remove this linter exclusions +// +//nolint:cyclop,funlen +func (h eventsRPCHandler) getEvents(ctx context.Context, request protocol.GetEventsRequest, +) (protocol.GetEventsResponse, error) { if err := request.Valid(h.maxLimit); err != nil { - return GetEventsResponse{}, &jrpc2.Error{ + return protocol.GetEventsResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } } ledgerRange, err := h.ledgerReader.GetLedgerRange(ctx) if err != nil { - return GetEventsResponse{}, &jrpc2.Error{ + return protocol.GetEventsResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } - start := db.Cursor{Ledger: request.StartLedger} + start := protocol.Cursor{Ledger: request.StartLedger} limit := h.defaultLimit if request.Pagination != nil { if request.Pagination.Cursor != nil { @@ -452,11 +136,11 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques endLedger = min(request.EndLedger, endLedger) } - end := db.Cursor{Ledger: endLedger} - cursorRange := db.CursorRange{Start: start, End: end} + end := protocol.Cursor{Ledger: endLedger} + cursorRange := protocol.CursorRange{Start: start, End: end} if start.Ledger < ledgerRange.FirstLedger.Sequence || start.Ledger > ledgerRange.LastLedger.Sequence { - return GetEventsResponse{}, &jrpc2.Error{ + return protocol.GetEventsResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidRequest, Message: fmt.Sprintf( "startLedger must be within the ledger range: %d - %d", @@ -470,14 +154,14 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques contractIDs, err := combineContractIDs(request.Filters) if err != nil { - return GetEventsResponse{}, &jrpc2.Error{ + return protocol.GetEventsResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } } topics, err := combineTopics(request.Filters) if err != nil { - return GetEventsResponse{}, &jrpc2.Error{ + return protocol.GetEventsResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } } @@ -486,7 +170,7 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques // Scan function to apply filters eventScanFunction := func( - event xdr.DiagnosticEvent, cursor db.Cursor, ledgerCloseTimestamp int64, txHash *xdr.Hash, + event xdr.DiagnosticEvent, cursor protocol.Cursor, ledgerCloseTimestamp int64, txHash *xdr.Hash, ) bool { if request.Matches(event) { found = append(found, entry{cursor, ledgerCloseTimestamp, event, txHash}) @@ -496,12 +180,12 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques err = h.dbReader.GetEvents(ctx, cursorRange, contractIDs, topics, eventTypes, eventScanFunction) if err != nil { - return GetEventsResponse{}, &jrpc2.Error{ + return protocol.GetEventsResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidRequest, Message: err.Error(), } } - results := make([]EventInfo, 0, len(found)) + results := make([]protocol.EventInfo, 0, len(found)) for _, entry := range found { info, err := eventInfoForEvent( entry.event, @@ -511,7 +195,7 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques request.Format, ) if err != nil { - return GetEventsResponse{}, errors.Wrap(err, "could not parse event") + return protocol.GetEventsResponse{}, errors.Wrap(err, "could not parse event") } results = append(results, info) } @@ -524,12 +208,12 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques // cursor represents end of the search window if events does not reach limit // here endLedger is always exclusive when fetching events // so search window is max Cursor value with endLedger - 1 - maxCursor := db.MaxCursor + maxCursor := protocol.MaxCursor maxCursor.Ledger = endLedger - 1 cursor = maxCursor.String() } - return GetEventsResponse{ + return protocol.GetEventsResponse{ LatestLedger: ledgerRange.LastLedger.Sequence, Events: results, Cursor: cursor, @@ -538,20 +222,20 @@ func (h eventsRPCHandler) getEvents(ctx context.Context, request GetEventsReques func eventInfoForEvent( event xdr.DiagnosticEvent, - cursor db.Cursor, + cursor protocol.Cursor, ledgerClosedAt, txHash, format string, -) (EventInfo, error) { +) (protocol.EventInfo, error) { v0, ok := event.Event.Body.GetV0() if !ok { - return EventInfo{}, errors.New("unknown event version") + return protocol.EventInfo{}, errors.New("unknown event version") } - eventType, ok := getEventTypeFromEventTypeXDR()[event.Event.Type] + eventType, ok := protocol.GetEventTypeFromEventTypeXDR()[event.Event.Type] if !ok { - return EventInfo{}, fmt.Errorf("unknown XDR ContractEventType type: %d", event.Event.Type) + return protocol.EventInfo{}, fmt.Errorf("unknown XDR ContractEventType type: %d", event.Event.Type) } - info := EventInfo{ + info := protocol.EventInfo{ EventType: eventType, Ledger: int32(cursor.Ledger), LedgerClosedAt: ledgerClosedAt, @@ -562,13 +246,13 @@ func eventInfoForEvent( } switch format { - case FormatJSON: + case protocol.FormatJSON: // json encode the topic - info.TopicJSON = make([]json.RawMessage, 0, db.MaxTopicCount) + info.TopicJSON = make([]json.RawMessage, 0, protocol.MaxTopicCount) for _, topic := range v0.Topics { topic, err := xdr2json.ConvertInterface(topic) if err != nil { - return EventInfo{}, err + return protocol.EventInfo{}, err } info.TopicJSON = append(info.TopicJSON, topic) } @@ -576,16 +260,16 @@ func eventInfoForEvent( var convErr error info.ValueJSON, convErr = xdr2json.ConvertInterface(v0.Data) if convErr != nil { - return EventInfo{}, convErr + return protocol.EventInfo{}, convErr } default: // base64-xdr encode the topic - topic := make([]string, 0, db.MaxTopicCount) + topic := make([]string, 0, protocol.MaxTopicCount) for _, segment := range v0.Topics { seg, err := xdr.MarshalBase64(segment) if err != nil { - return EventInfo{}, err + return protocol.EventInfo{}, err } topic = append(topic, seg) } @@ -593,7 +277,7 @@ func eventInfoForEvent( // base64-xdr encode the data data, err := xdr.MarshalBase64(v0.Data) if err != nil { - return EventInfo{}, err + return protocol.EventInfo{}, err } info.TopicXDR = topic diff --git a/cmd/stellar-rpc/internal/methods/get_events_test.go b/cmd/stellar-rpc/internal/methods/get_events_test.go index 55cb64c8..68c3d8b0 100644 --- a/cmd/stellar-rpc/internal/methods/get_events_test.go +++ b/cmd/stellar-rpc/internal/methods/get_events_test.go @@ -3,10 +3,8 @@ package methods import ( "context" "encoding/json" - "fmt" "path" "strconv" - "strings" "testing" "time" @@ -23,508 +21,11 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) var passphrase = "passphrase" -func TestEventTypeSetMatches(t *testing.T) { - var defaultSet eventTypeSet - all := eventTypeSet{} - all[EventTypeContract] = nil - all[EventTypeDiagnostic] = nil - all[EventTypeSystem] = nil - - onlyContract := eventTypeSet{} - onlyContract[EventTypeContract] = nil - - contractEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeContract} - diagnosticEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeDiagnostic} - systemEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeSystem} - - for _, testCase := range []struct { - name string - set eventTypeSet - event xdr.ContractEvent - matches bool - }{ - { - "all matches Contract events", - all, - contractEvent, - true, - }, - { - "all matches System events", - all, - systemEvent, - true, - }, - { - "all matches Diagnostic events", - all, - systemEvent, - true, - }, - { - "defaultSet matches Contract events", - defaultSet, - contractEvent, - true, - }, - { - "defaultSet matches System events", - defaultSet, - systemEvent, - true, - }, - { - "defaultSet matches Diagnostic events", - defaultSet, - systemEvent, - true, - }, - { - "onlyContract set matches Contract events", - onlyContract, - contractEvent, - true, - }, - { - "onlyContract does not match System events", - onlyContract, - systemEvent, - false, - }, - { - "onlyContract does not match Diagnostic events", - defaultSet, - diagnosticEvent, - true, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.matches, testCase.set.matches(testCase.event)) - }) - } -} - -func TestEventTypeSetValid(t *testing.T) { - for _, testCase := range []struct { - name string - keys []string - expectedError bool - }{ - { - "empty set", - []string{}, - false, - }, - { - "set with one valid element", - []string{EventTypeSystem}, - false, - }, - { - "set with two valid elements", - []string{EventTypeSystem, EventTypeContract}, - false, - }, - { - "set with three valid elements", - []string{EventTypeSystem, EventTypeContract, EventTypeDiagnostic}, - false, - }, - { - "set with one invalid element", - []string{"abc"}, - true, - }, - { - "set with multiple invalid elements", - []string{"abc", "def"}, - true, - }, - { - "set with valid elements mixed with invalid elements", - []string{EventTypeSystem, "abc"}, - true, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - set := eventTypeSet{} - for _, key := range testCase.keys { - set[key] = nil - } - if testCase.expectedError { - assert.Error(t, set.valid()) - } else { - require.NoError(t, set.valid()) - } - }) - } -} - -func TestEventTypeSetMarshaling(t *testing.T) { - for _, testCase := range []struct { - name string - input string - expected []string - }{ - { - "empty set", - "", - []string{}, - }, - { - "set with one element", - "a", - []string{"a"}, - }, - { - "set with more than one element", - "a,b,c", - []string{"a", "b", "c"}, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - var set eventTypeSet - input, err := json.Marshal(testCase.input) - require.NoError(t, err) - err = set.UnmarshalJSON(input) - require.NoError(t, err) - assert.Equal(t, len(testCase.expected), len(set)) - for _, val := range testCase.expected { - _, ok := set[val] - assert.True(t, ok) - } - }) - } -} - -func TestTopicFilterMatches(t *testing.T) { - transferSym := xdr.ScSymbol("transfer") - transfer := xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &transferSym, - } - sixtyfour := xdr.Uint64(64) - number := xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &sixtyfour, - } - star := "*" - for _, tc := range []struct { - name string - filter TopicFilter - includes []xdr.ScVec - excludes []xdr.ScVec - }{ - { - name: "", - filter: nil, - includes: []xdr.ScVec{ - {}, - }, - excludes: []xdr.ScVec{ - {transfer}, - }, - }, - - // Exact matching - { - name: "ScSymbol(transfer)", - filter: []SegmentFilter{ - {scval: &transfer}, - }, - includes: []xdr.ScVec{ - {transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {transfer, transfer}, - }, - }, - - // Star - { - name: "*", - filter: []SegmentFilter{ - {wildcard: &star}, - }, - includes: []xdr.ScVec{ - {transfer}, - }, - excludes: []xdr.ScVec{ - {transfer, transfer}, - }, - }, - { - name: "*/transfer", - filter: []SegmentFilter{ - {wildcard: &star}, - {scval: &transfer}, - }, - includes: []xdr.ScVec{ - {number, transfer}, - {transfer, transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, transfer, number}, - {transfer}, - {transfer, number}, - {transfer, transfer, transfer}, - }, - }, - { - name: "transfer/*", - filter: []SegmentFilter{ - {scval: &transfer}, - {wildcard: &star}, - }, - includes: []xdr.ScVec{ - {transfer, number}, - {transfer, transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, transfer, number}, - {transfer}, - {number, transfer}, - {transfer, transfer, transfer}, - }, - }, - { - name: "transfer/*/*", - filter: []SegmentFilter{ - {scval: &transfer}, - {wildcard: &star}, - {wildcard: &star}, - }, - includes: []xdr.ScVec{ - {transfer, number, number}, - {transfer, transfer, transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, transfer}, - {number, transfer, number, number}, - {transfer}, - {transfer, transfer, transfer, transfer}, - }, - }, - { - name: "transfer/*/number", - filter: []SegmentFilter{ - {scval: &transfer}, - {wildcard: &star}, - {scval: &number}, - }, - includes: []xdr.ScVec{ - {transfer, number, number}, - {transfer, transfer, number}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, number, number}, - {number, transfer, number}, - {transfer}, - {number, transfer}, - {transfer, transfer, transfer}, - {transfer, number, transfer}, - }, - }, - } { - name := tc.name - if name == "" { - name = topicFilterToString(tc.filter) - } - t.Run(name, func(t *testing.T) { - for _, include := range tc.includes { - assert.True( - t, - tc.filter.Matches(include), - "Expected %v filter to include %v", - name, - include, - ) - } - for _, exclude := range tc.excludes { - assert.False( - t, - tc.filter.Matches(exclude), - "Expected %v filter to exclude %v", - name, - exclude, - ) - } - }) - } -} - -func TestTopicFilterJSON(t *testing.T) { - var got TopicFilter - - require.NoError(t, json.Unmarshal([]byte("[]"), &got)) - assert.Equal(t, TopicFilter{}, got) - - star := "*" - require.NoError(t, json.Unmarshal([]byte("[\"*\"]"), &got)) - assert.Equal(t, TopicFilter{{wildcard: &star}}, got) - - sixtyfour := xdr.Uint64(64) - scval := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &sixtyfour} - scvalstr, err := xdr.MarshalBase64(scval) - require.NoError(t, err) - require.NoError(t, json.Unmarshal([]byte(fmt.Sprintf("[%q]", scvalstr)), &got)) - assert.Equal(t, TopicFilter{{scval: &scval}}, got) -} - -func topicFilterToString(t TopicFilter) string { - var s []string - for _, segment := range t { - if segment.wildcard != nil { - s = append(s, *segment.wildcard) - } else if segment.scval != nil { - out, err := xdr.MarshalBase64(*segment.scval) - if err != nil { - panic(err) - } - s = append(s, out) - } else { - panic("Invalid topic filter") - } - } - if len(s) == 0 { - s = append(s, "") - } - return strings.Join(s, "/") -} - -func TestGetEventsRequestValid(t *testing.T) { - // omit startLedger but include cursor - var request GetEventsRequest - require.NoError(t, json.Unmarshal( - []byte("{ \"filters\": [], \"pagination\": { \"cursor\": \"0000000021474840576-0000000000\"} }"), - &request, - )) - assert.Equal(t, uint32(0), request.StartLedger) - require.NoError(t, request.Valid(1000)) - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: &PaginationOptions{Cursor: &db.Cursor{}}, - }).Valid(1000), "ledger ranges and cursor cannot both be set") - - require.NoError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: nil, - }).Valid(1000)) - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: &PaginationOptions{Limit: 1001}, - }).Valid(1000), "limit must not exceed 1000") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 0, - Filters: []EventFilter{}, - Pagination: nil, - }).Valid(1000), "startLedger must be positive") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {}, {}, {}, {}, {}, {}, - }, - Pagination: nil, - }).Valid(1000), "maximum 5 filters per request") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {EventType: map[string]interface{}{"foo": nil}}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: filter type invalid: if set, type must be either 'system', 'contract' or 'diagnostic'") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{ - "CCVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKUD2U", - "CC53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53WQD5", - "CDGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZLND", - "CDO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53YUK", - "CDXO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO4M7R", - "CD7777777777777777777777777777777777777777777777777767GY", - }}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: maximum 5 contract IDs per filter") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{"a"}}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: contract ID 1 invalid") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{"CCVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVINVALID"}}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: contract ID 1 invalid") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - { - Topics: []TopicFilter{ - {}, {}, {}, {}, {}, {}, - }, - }, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: maximum 5 topics per filter") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - {}, - }}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: topic 1 invalid: topic must have at least one segment") - - require.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - { - {}, - {}, - {}, - {}, - {}, - }, - }}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: topic 1 invalid: topic cannot have more than 4 segments") -} - func TestGetEvents(t *testing.T) { now := time.Now().UTC() counter := xdr.ScSymbol("COUNTER") @@ -571,12 +72,12 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - _, err = handler.getEvents(context.TODO(), GetEventsRequest{ + _, err = handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, }) require.EqualError(t, err, "[-32600] startLedger must be within the ledger range: 2 - 2") - _, err = handler.getEvents(context.TODO(), GetEventsRequest{ + _, err = handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 3, }) require.EqualError(t, err, "[-32600] startLedger must be within the ledger range: 2 - 2") @@ -624,14 +125,14 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, }) require.NoError(t, err) - var expected []EventInfo + var expected []protocol.EventInfo for i := range txMeta { - id := db.Cursor{ + id := protocol.Cursor{ Ledger: 1, Tx: uint32(i + 1), Op: 0, @@ -642,8 +143,8 @@ func TestGetEvents(t *testing.T) { Sym: &counter, }) require.NoError(t, err) - expected = append(expected, EventInfo{ - EventType: EventTypeContract, + expected = append(expected, protocol.EventInfo{ + EventType: protocol.EventTypeContract, Ledger: 1, LedgerClosedAt: now.Format(time.RFC3339), ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", @@ -655,10 +156,15 @@ func TestGetEvents(t *testing.T) { TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), }) } - cursor := db.MaxCursor + cursor := protocol.MaxCursor cursor.Ledger = 1 cursorStr := cursor.String() - assert.Equal(t, GetEventsResponse{expected, 1, cursorStr}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 1, Cursor: cursorStr, + }, + results, + ) }) t.Run("filtering by contract id", func(t *testing.T) { @@ -706,25 +212,25 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, - Filters: []EventFilter{ + Filters: []protocol.EventFilter{ {ContractIDs: []string{strkey.MustEncode(strkey.VersionByteContract, contractIDs[0][:])}}, }, }) require.NoError(t, err) assert.Equal(t, uint32(1), results.LatestLedger) - expectedIds := []string{ - db.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 0}.String(), - db.Cursor{Ledger: 1, Tx: 3, Op: 0, Event: 0}.String(), - db.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String(), + expectedIDs := []string{ + protocol.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 0}.String(), + protocol.Cursor{Ledger: 1, Tx: 3, Op: 0, Event: 0}.String(), + protocol.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String(), } - eventIds := []string{} + eventIDs := []string{} for _, event := range results.Events { - eventIds = append(eventIds, event.ID) + eventIDs = append(eventIDs, event.ID) } - assert.Equal(t, expectedIds, eventIds) + assert.Equal(t, expectedIDs, eventIDs) }) t.Run("filtering by topic", func(t *testing.T) { @@ -769,20 +275,20 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - []SegmentFilter{ - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, + Filters: []protocol.EventFilter{ + {Topics: []protocol.TopicFilter{ + []protocol.SegmentFilter{ + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, }, }}, }, }) require.NoError(t, err) - id := db.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String() + id := protocol.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String() require.NoError(t, err) scVal := xdr.ScVal{ Type: xdr.ScValTypeScvU64, @@ -790,9 +296,9 @@ func TestGetEvents(t *testing.T) { } value, err := xdr.MarshalBase64(scVal) require.NoError(t, err) - expected := []EventInfo{ + expected := []protocol.EventInfo{ { - EventType: EventTypeContract, + EventType: protocol.EventTypeContract, Ledger: 1, LedgerClosedAt: now.Format(time.RFC3339), ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", @@ -805,19 +311,24 @@ func TestGetEvents(t *testing.T) { }, } - cursor := db.MaxCursor + cursor := protocol.MaxCursor cursor.Ledger = 1 cursorStr := cursor.String() - assert.Equal(t, GetEventsResponse{expected, 1, cursorStr}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 1, Cursor: cursorStr, + }, + results, + ) - results, err = handler.getEvents(ctx, GetEventsRequest{ + results, err = handler.getEvents(ctx, protocol.GetEventsRequest{ StartLedger: 1, - Format: FormatJSON, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - []SegmentFilter{ - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, + Format: protocol.FormatJSON, + Filters: []protocol.EventFilter{ + {Topics: []protocol.TopicFilter{ + []protocol.SegmentFilter{ + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, }, }}, }, @@ -842,7 +353,12 @@ func TestGetEvents(t *testing.T) { expected[0].ValueJSON = valueJs expected[0].TopicJSON = topicsJs - require.Equal(t, GetEventsResponse{expected, 1, cursorStr}, results) + require.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 1, Cursor: cursorStr, + }, + results, + ) }) t.Run("filtering by both contract id and topic", func(t *testing.T) { @@ -917,15 +433,15 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, - Filters: []EventFilter{ + Filters: []protocol.EventFilter{ { ContractIDs: []string{strkey.MustEncode(strkey.VersionByteContract, contractID[:])}, - Topics: []TopicFilter{ - []SegmentFilter{ - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, + Topics: []protocol.TopicFilter{ + []protocol.SegmentFilter{ + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, }, }, }, @@ -933,15 +449,15 @@ func TestGetEvents(t *testing.T) { }) require.NoError(t, err) - id := db.Cursor{Ledger: 1, Tx: 4, Op: 0, Event: 0}.String() + id := protocol.Cursor{Ledger: 1, Tx: 4, Op: 0, Event: 0}.String() value, err := xdr.MarshalBase64(xdr.ScVal{ Type: xdr.ScValTypeScvU64, U64: &number, }) require.NoError(t, err) - expected := []EventInfo{ + expected := []protocol.EventInfo{ { - EventType: EventTypeContract, + EventType: protocol.EventTypeContract, Ledger: 1, LedgerClosedAt: now.Format(time.RFC3339), ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), @@ -953,10 +469,15 @@ func TestGetEvents(t *testing.T) { TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), }, } - cursor := db.MaxCursor + cursor := protocol.MaxCursor cursor.Ledger = 1 cursorStr := cursor.String() - assert.Equal(t, GetEventsResponse{expected, 1, cursorStr}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 1, Cursor: cursorStr, + }, + results, + ) }) t.Run("filtering by event type", func(t *testing.T) { @@ -1008,18 +529,18 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, - Filters: []EventFilter{ - {EventType: map[string]interface{}{EventTypeSystem: nil}}, + Filters: []protocol.EventFilter{ + {EventType: map[string]interface{}{protocol.EventTypeSystem: nil}}, }, }) require.NoError(t, err) - id := db.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 1}.String() - expected := []EventInfo{ + id := protocol.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 1}.String() + expected := []protocol.EventInfo{ { - EventType: EventTypeSystem, + EventType: protocol.EventTypeSystem, Ledger: 1, LedgerClosedAt: now.Format(time.RFC3339), ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), @@ -1031,10 +552,15 @@ func TestGetEvents(t *testing.T) { TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), }, } - cursor := db.MaxCursor + cursor := protocol.MaxCursor cursor.Ledger = 1 cursorStr := cursor.String() - assert.Equal(t, GetEventsResponse{expected, 1, cursorStr}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 1, Cursor: cursorStr, + }, + results, + ) }) t.Run("with limit", func(t *testing.T) { @@ -1075,16 +601,16 @@ func TestGetEvents(t *testing.T) { defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ StartLedger: 1, - Filters: []EventFilter{}, - Pagination: &PaginationOptions{Limit: 10}, + Filters: []protocol.EventFilter{}, + Pagination: &protocol.PaginationOptions{Limit: 10}, }) require.NoError(t, err) - var expected []EventInfo + var expected []protocol.EventInfo for i := range 10 { - id := db.Cursor{ + id := protocol.Cursor{ Ledger: 1, Tx: uint32(i + 1), Op: 0, @@ -1092,8 +618,8 @@ func TestGetEvents(t *testing.T) { }.String() value, err := xdr.MarshalBase64(txMeta[i].MustV3().SorobanMeta.Events[0].Body.MustV0().Data) require.NoError(t, err) - expected = append(expected, EventInfo{ - EventType: EventTypeContract, + expected = append(expected, protocol.EventInfo{ + EventType: protocol.EventTypeContract, Ledger: 1, LedgerClosedAt: now.Format(time.RFC3339), ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", @@ -1107,7 +633,12 @@ func TestGetEvents(t *testing.T) { } cursor := expected[len(expected)-1].ID - assert.Equal(t, GetEventsResponse{expected, 1, cursor}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 1, Cursor: cursor, + }, + results, + ) }) t.Run("with cursor", func(t *testing.T) { @@ -1170,32 +701,32 @@ func TestGetEvents(t *testing.T) { require.NoError(t, eventW.InsertEvents(ledgerCloseMeta), "ingestion failed for events ") require.NoError(t, write.Commit(ledgerCloseMeta)) - id := &db.Cursor{Ledger: 5, Tx: 1, Op: 0, Event: 0} + id := &protocol.Cursor{Ledger: 5, Tx: 1, Op: 0, Event: 0} handler := eventsRPCHandler{ dbReader: store, maxLimit: 10000, defaultLimit: 100, ledgerReader: db.NewLedgerReader(dbx), } - results, err := handler.getEvents(context.TODO(), GetEventsRequest{ - Pagination: &PaginationOptions{ + results, err := handler.getEvents(context.TODO(), protocol.GetEventsRequest{ + Pagination: &protocol.PaginationOptions{ Cursor: id, Limit: 2, }, }) require.NoError(t, err) - var expected []EventInfo + var expected []protocol.EventInfo expectedIDs := []string{ - db.Cursor{Ledger: 5, Tx: 1, Op: 0, Event: 1}.String(), - db.Cursor{Ledger: 5, Tx: 2, Op: 0, Event: 0}.String(), + protocol.Cursor{Ledger: 5, Tx: 1, Op: 0, Event: 1}.String(), + protocol.Cursor{Ledger: 5, Tx: 2, Op: 0, Event: 0}.String(), } symbols := datas[1:3] for i, id := range expectedIDs { expectedXdr, err := xdr.MarshalBase64(xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &symbols[i]}) require.NoError(t, err) - expected = append(expected, EventInfo{ - EventType: EventTypeContract, + expected = append(expected, protocol.EventInfo{ + EventType: protocol.EventTypeContract, Ledger: 5, LedgerClosedAt: now.Format(time.RFC3339), ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), @@ -1208,11 +739,17 @@ func TestGetEvents(t *testing.T) { }) } cursor := expected[len(expected)-1].ID - assert.Equal(t, GetEventsResponse{expected, 5, cursor}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: expected, LatestLedger: 5, + Cursor: cursor, + }, + results, + ) - results, err = handler.getEvents(context.TODO(), GetEventsRequest{ - Pagination: &PaginationOptions{ - Cursor: &db.Cursor{Ledger: 5, Tx: 2, Op: 0, Event: 1}, + results, err = handler.getEvents(context.TODO(), protocol.GetEventsRequest{ + Pagination: &protocol.PaginationOptions{ + Cursor: &protocol.Cursor{Ledger: 5, Tx: 2, Op: 0, Event: 1}, Limit: 2, }, }) @@ -1223,10 +760,15 @@ func TestGetEvents(t *testing.T) { // Note: endLedger is always exclusive when fetching events // so search window is always max Cursor value with endLedger - 1 - rawCursor := db.MaxCursor + rawCursor := protocol.MaxCursor rawCursor.Ledger = uint32(endLedger - 1) cursor = rawCursor.String() - assert.Equal(t, GetEventsResponse{[]EventInfo{}, 5, cursor}, results) + assert.Equal(t, + protocol.GetEventsResponse{ + Events: []protocol.EventInfo{}, LatestLedger: 5, Cursor: cursor, + }, + results, + ) }) } @@ -1265,13 +807,13 @@ func BenchmarkGetEvents(b *testing.B) { ledgerReader: db.NewLedgerReader(dbx), } - request := GetEventsRequest{ + request := protocol.GetEventsRequest{ StartLedger: 1, - Filters: []EventFilter{ + Filters: []protocol.EventFilter{ { - Topics: []TopicFilter{ - []SegmentFilter{ - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counters[1]}}, + Topics: []protocol.TopicFilter{ + []protocol.SegmentFilter{ + {ScVal: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counters[1]}}, }, }, }, @@ -1281,7 +823,7 @@ func BenchmarkGetEvents(b *testing.B) { b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { _, err := handler.getEvents(ctx, request) if err != nil { b.Errorf("getEvents failed: %v", err) @@ -1320,7 +862,8 @@ func getTxMetaWithContractEvents(contractID xdr.Hash) []xdr.TransactionMeta { return txMeta } -func ledgerCloseMetaWithEvents(sequence uint32, closeTimestamp int64, txMeta ...xdr.TransactionMeta) xdr.LedgerCloseMeta { +func ledgerCloseMetaWithEvents(sequence uint32, closeTimestamp int64, txMeta ...xdr.TransactionMeta, +) xdr.LedgerCloseMeta { var txProcessing []xdr.TransactionResultMeta var phases []xdr.TransactionPhase diff --git a/cmd/stellar-rpc/internal/methods/get_fee_stats.go b/cmd/stellar-rpc/internal/methods/get_fee_stats.go index fc160737..6f1c572f 100644 --- a/cmd/stellar-rpc/internal/methods/get_fee_stats.go +++ b/cmd/stellar-rpc/internal/methods/get_fee_stats.go @@ -9,29 +9,11 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/feewindow" + "github.com/stellar/stellar-rpc/protocol" ) -type FeeDistribution struct { - Max uint64 `json:"max,string"` - Min uint64 `json:"min,string"` - Mode uint64 `json:"mode,string"` - P10 uint64 `json:"p10,string"` - P20 uint64 `json:"p20,string"` - P30 uint64 `json:"p30,string"` - P40 uint64 `json:"p40,string"` - P50 uint64 `json:"p50,string"` - P60 uint64 `json:"p60,string"` - P70 uint64 `json:"p70,string"` - P80 uint64 `json:"p80,string"` - P90 uint64 `json:"p90,string"` - P95 uint64 `json:"p95,string"` - P99 uint64 `json:"p99,string"` - TransactionCount uint32 `json:"transactionCount,string"` - LedgerCount uint32 `json:"ledgerCount"` -} - -func convertFeeDistribution(distribution feewindow.FeeDistribution) FeeDistribution { - return FeeDistribution{ +func convertFeeDistribution(distribution feewindow.FeeDistribution) protocol.FeeDistribution { + return protocol.FeeDistribution{ Max: distribution.Max, Min: distribution.Min, Mode: distribution.Mode, @@ -51,24 +33,18 @@ func convertFeeDistribution(distribution feewindow.FeeDistribution) FeeDistribut } } -type GetFeeStatsResult struct { - SorobanInclusionFee FeeDistribution `json:"sorobanInclusionFee"` - InclusionFee FeeDistribution `json:"inclusionFee"` - LatestLedger uint32 `json:"latestLedger"` -} - // NewGetFeeStatsHandler returns a handler obtaining fee statistics func NewGetFeeStatsHandler(windows *feewindow.FeeWindows, ledgerReader db.LedgerReader, logger *log.Entry, ) jrpc2.Handler { - return NewHandler(func(ctx context.Context) (GetFeeStatsResult, error) { + return NewHandler(func(ctx context.Context) (protocol.GetFeeStatsResponse, error) { ledgerRange, err := ledgerReader.GetLedgerRange(ctx) if err != nil { // still not fatal logger.WithError(err). Error("could not fetch ledger range") } - result := GetFeeStatsResult{ + result := protocol.GetFeeStatsResponse{ SorobanInclusionFee: convertFeeDistribution(windows.SorobanInclusionFeeWindow.GetFeeDistribution()), InclusionFee: convertFeeDistribution(windows.ClassicFeeWindow.GetFeeDistribution()), LatestLedger: ledgerRange.LastLedger.Sequence, diff --git a/cmd/stellar-rpc/internal/methods/health.go b/cmd/stellar-rpc/internal/methods/get_health.go similarity index 74% rename from cmd/stellar-rpc/internal/methods/health.go rename to cmd/stellar-rpc/internal/methods/get_health.go index edfc9b25..fb99f05b 100644 --- a/cmd/stellar-rpc/internal/methods/health.go +++ b/cmd/stellar-rpc/internal/methods/get_health.go @@ -8,29 +8,23 @@ import ( "github.com/creachadair/jrpc2" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) -type HealthCheckResult struct { - Status string `json:"status"` - LatestLedger uint32 `json:"latestLedger"` - OldestLedger uint32 `json:"oldestLedger"` - LedgerRetentionWindow uint32 `json:"ledgerRetentionWindow"` -} - // NewHealthCheck returns a health check json rpc handler func NewHealthCheck( retentionWindow uint32, ledgerReader db.LedgerReader, maxHealthyLedgerLatency time.Duration, ) jrpc2.Handler { - return NewHandler(func(ctx context.Context) (HealthCheckResult, error) { + return NewHandler(func(ctx context.Context) (protocol.GetHealthResponse, error) { ledgerRange, err := ledgerReader.GetLedgerRange(ctx) if err != nil || ledgerRange.LastLedger.Sequence < 1 { extra := "" if err != nil { extra = ": " + err.Error() } - return HealthCheckResult{}, jrpc2.Error{ + return protocol.GetHealthResponse{}, jrpc2.Error{ Code: jrpc2.InternalError, Message: "data stores are not initialized" + extra, } @@ -42,12 +36,12 @@ func NewHealthCheck( roundedLatency := lastKnownLedgerLatency.Round(time.Second) msg := fmt.Sprintf("latency (%s) since last known ledger closed is too high (>%s)", roundedLatency, maxHealthyLedgerLatency) - return HealthCheckResult{}, jrpc2.Error{ + return protocol.GetHealthResponse{}, jrpc2.Error{ Code: jrpc2.InternalError, Message: msg, } } - result := HealthCheckResult{ + result := protocol.GetHealthResponse{ Status: "healthy", LatestLedger: ledgerRange.LastLedger.Sequence, OldestLedger: ledgerRange.FirstLedger.Sequence, diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go index db69ee18..43e47f77 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger.go @@ -6,23 +6,15 @@ import ( "github.com/creachadair/jrpc2" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) -type GetLatestLedgerResponse struct { - // Hash of the latest ledger as a hex-encoded string - Hash string `json:"id"` - // Stellar Core protocol version associated with the ledger. - ProtocolVersion uint32 `json:"protocolVersion"` - // Sequence number of the latest ledger. - Sequence uint32 `json:"sequence"` -} - // NewGetLatestLedgerHandler returns a JSON RPC handler to retrieve the latest ledger entry from Stellar core. func NewGetLatestLedgerHandler(ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader) jrpc2.Handler { - return NewHandler(func(ctx context.Context) (GetLatestLedgerResponse, error) { + return NewHandler(func(ctx context.Context) (protocol.GetLatestLedgerResponse, error) { latestSequence, err := ledgerEntryReader.GetLatestLedgerSequence(ctx) if err != nil { - return GetLatestLedgerResponse{}, &jrpc2.Error{ + return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not get latest ledger sequence", } @@ -30,13 +22,13 @@ func NewGetLatestLedgerHandler(ledgerEntryReader db.LedgerEntryReader, ledgerRea latestLedger, found, err := ledgerReader.GetLedger(ctx, latestSequence) if (err != nil) || (!found) { - return GetLatestLedgerResponse{}, &jrpc2.Error{ + return protocol.GetLatestLedgerResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not get latest ledger", } } - response := GetLatestLedgerResponse{ + response := protocol.GetLatestLedgerResponse{ Hash: latestLedger.LedgerHash().HexString(), ProtocolVersion: latestLedger.ProtocolVersion(), Sequence: latestSequence, diff --git a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go index a2284fb7..031466ab 100644 --- a/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go +++ b/cmd/stellar-rpc/internal/methods/get_latest_ledger_test.go @@ -13,6 +13,7 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/ledgerbucketwindow" + "github.com/stellar/stellar-rpc/protocol" ) const ( @@ -92,8 +93,10 @@ func createLedger(ledgerSequence uint32, protocolVersion uint32, hash byte) xdr. func TestGetLatestLedger(t *testing.T) { getLatestLedgerHandler := NewGetLatestLedgerHandler(&ConstantLedgerEntryReader{}, &ConstantLedgerReader{}) latestLedgerRespI, err := getLatestLedgerHandler(context.Background(), &jrpc2.Request{}) - latestLedgerResp := latestLedgerRespI.(GetLatestLedgerResponse) require.NoError(t, err) + require.IsType(t, protocol.GetLatestLedgerResponse{}, latestLedgerRespI) + latestLedgerResp, ok := latestLedgerRespI.(protocol.GetLatestLedgerResponse) + require.True(t, ok) expectedLatestLedgerHashStr := xdr.Hash{expectedLatestLedgerHashBytes}.HexString() assert.Equal(t, expectedLatestLedgerHashStr, latestLedgerResp.Hash) diff --git a/cmd/stellar-rpc/internal/methods/get_ledger_entries.go b/cmd/stellar-rpc/internal/methods/get_ledger_entries.go index 929b8a5f..d2dcf0ce 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledger_entries.go +++ b/cmd/stellar-rpc/internal/methods/get_ledger_entries.go @@ -2,7 +2,6 @@ package methods import ( "context" - "encoding/json" "fmt" "github.com/creachadair/jrpc2" @@ -12,50 +11,27 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) //nolint:gochecknoglobals var ErrLedgerTTLEntriesCannotBeQueriedDirectly = "ledger ttl entries cannot be queried directly" -type GetLedgerEntriesRequest struct { - Keys []string `json:"keys"` - Format string `json:"xdrFormat,omitempty"` -} - -type LedgerEntryResult struct { - // Original request key matching this LedgerEntryResult. - KeyXDR string `json:"key,omitempty"` - KeyJSON json.RawMessage `json:"keyJson,omitempty"` - // Ledger entry data encoded in base 64. - DataXDR string `json:"xdr,omitempty"` - DataJSON json.RawMessage `json:"dataJson,omitempty"` - // Last modified ledger for this entry. - LastModifiedLedger uint32 `json:"lastModifiedLedgerSeq"` - // The ledger sequence until the entry is live, available for entries that have associated ttl ledger entries. - LiveUntilLedgerSeq *uint32 `json:"liveUntilLedgerSeq,omitempty"` -} - -type GetLedgerEntriesResponse struct { - // All found ledger entries. - Entries []LedgerEntryResult `json:"entries"` - // Sequence number of the latest ledger at time of request. - LatestLedger uint32 `json:"latestLedger"` -} - const getLedgerEntriesMaxKeys = 200 // NewGetLedgerEntriesHandler returns a JSON RPC handler to retrieve the specified ledger entries from Stellar Core. func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { - return NewHandler(func(ctx context.Context, request GetLedgerEntriesRequest) (GetLedgerEntriesResponse, error) { - if err := IsValidFormat(request.Format); err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return NewHandler(func(ctx context.Context, request protocol.GetLedgerEntriesRequest, + ) (protocol.GetLedgerEntriesResponse, error) { + if err := protocol.IsValidFormat(request.Format); err != nil { + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } } if len(request.Keys) > getLedgerEntriesMaxKeys { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: fmt.Sprintf("key count (%d) exceeds maximum supported (%d)", len(request.Keys), getLedgerEntriesMaxKeys), } @@ -66,7 +42,7 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn if err := xdr.SafeUnmarshalBase64(requestKey, &ledgerKey); err != nil { logger.WithError(err).WithField("request", request). Infof("could not unmarshal requestKey %s at index %d from getLedgerEntries request", requestKey, i) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: fmt.Sprintf("cannot unmarshal key value %s at index %d", requestKey, i), } @@ -74,7 +50,7 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn if ledgerKey.Type == xdr.LedgerEntryTypeTtl { logger.WithField("request", request). Infof("could not provide ledger ttl entry %s at index %d from getLedgerEntries request", requestKey, i) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: ErrLedgerTTLEntriesCannotBeQueriedDirectly, } @@ -84,7 +60,7 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn tx, err := ledgerEntryReader.NewTx(ctx, false) if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not create read transaction", } @@ -95,18 +71,18 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn latestLedger, err := tx.GetLatestLedgerSequence() if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not get latest ledger", } } - ledgerEntryResults := make([]LedgerEntryResult, 0, len(ledgerKeys)) + ledgerEntryResults := make([]protocol.LedgerEntryResult, 0, len(ledgerKeys)) ledgerKeysAndEntries, err := tx.GetLedgerEntries(ledgerKeys...) if err != nil { logger.WithError(err).WithField("request", request). Info("could not obtain ledger entries from storage") - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not obtain ledger entries from storage", } @@ -114,23 +90,23 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn for _, ledgerKeyAndEntry := range ledgerKeysAndEntries { switch request.Format { - case FormatJSON: + case protocol.FormatJSON: keyJs, err := xdr2json.ConvertInterface(ledgerKeyAndEntry.Key) if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } entryJs, err := xdr2json.ConvertInterface(ledgerKeyAndEntry.Entry.Data) if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } - ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ + ledgerEntryResults = append(ledgerEntryResults, protocol.LedgerEntryResult{ KeyJSON: keyJs, DataJSON: entryJs, LastModifiedLedger: uint32(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), @@ -140,7 +116,7 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn default: keyXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Key) if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: fmt.Sprintf("could not serialize ledger key %v", ledgerKeyAndEntry.Key), } @@ -148,13 +124,13 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn entryXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data) if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ + return protocol.GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: fmt.Sprintf("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry), } } - ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ + ledgerEntryResults = append(ledgerEntryResults, protocol.LedgerEntryResult{ KeyXDR: keyXDR, DataXDR: entryXDR, LastModifiedLedger: uint32(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), @@ -163,7 +139,7 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn } } - response := GetLedgerEntriesResponse{ + response := protocol.GetLedgerEntriesResponse{ Entries: ledgerEntryResults, LatestLedger: latestLedger, } diff --git a/cmd/stellar-rpc/internal/methods/get_ledgers.go b/cmd/stellar-rpc/internal/methods/get_ledgers.go index 8d6a5707..0c3f2080 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledgers.go +++ b/cmd/stellar-rpc/internal/methods/get_ledgers.go @@ -3,8 +3,6 @@ package methods import ( "context" "encoding/base64" - "encoding/json" - "errors" "fmt" "strconv" @@ -14,70 +12,9 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/ledgerbucketwindow" + "github.com/stellar/stellar-rpc/protocol" ) -type LedgerPaginationOptions struct { - Cursor string `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` -} - -// isStartLedgerWithinBounds checks whether the request start ledger/cursor is within the max/min ledger -// for the current RPC instance. -func isStartLedgerWithinBounds(startLedger uint32, ledgerRange ledgerbucketwindow.LedgerRange) bool { - return startLedger >= ledgerRange.FirstLedger.Sequence && startLedger <= ledgerRange.LastLedger.Sequence -} - -// GetLedgersRequest represents the request parameters for fetching ledgers. -type GetLedgersRequest struct { - StartLedger uint32 `json:"startLedger"` - Pagination *LedgerPaginationOptions `json:"pagination,omitempty"` - Format string `json:"xdrFormat,omitempty"` -} - -// validate checks the validity of the request parameters. -func (req *GetLedgersRequest) validate(maxLimit uint, ledgerRange ledgerbucketwindow.LedgerRange) error { - switch { - case req.Pagination != nil: - switch { - case req.Pagination.Cursor != "" && req.StartLedger != 0: - return errors.New("startLedger and cursor cannot both be set") - case req.Pagination.Limit > maxLimit: - return fmt.Errorf("limit must not exceed %d", maxLimit) - } - case req.StartLedger != 0 && !isStartLedgerWithinBounds(req.StartLedger, ledgerRange): - return fmt.Errorf( - "start ledger must be between the oldest ledger: %d and the latest ledger: %d for this rpc instance", - ledgerRange.FirstLedger.Sequence, - ledgerRange.LastLedger.Sequence, - ) - } - - return IsValidFormat(req.Format) -} - -// LedgerInfo represents a single ledger in the response. -type LedgerInfo struct { - Hash string `json:"hash"` - Sequence uint32 `json:"sequence"` - LedgerCloseTime int64 `json:"ledgerCloseTime,string"` - - LedgerHeader string `json:"headerXdr"` - LedgerHeaderJSON json.RawMessage `json:"headerJson,omitempty"` - - LedgerMetadata string `json:"metadataXdr"` - LedgerMetadataJSON json.RawMessage `json:"metadataJson,omitempty"` -} - -// GetLedgersResponse encapsulates the response structure for getLedgers queries. -type GetLedgersResponse struct { - Ledgers []LedgerInfo `json:"ledgers"` - LatestLedger uint32 `json:"latestLedger"` - LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime"` - OldestLedger uint32 `json:"oldestLedger"` - OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTime"` - Cursor string `json:"cursor"` -} - type ledgersHandler struct { ledgerReader db.LedgerReader maxLimit uint @@ -94,10 +31,11 @@ func NewGetLedgersHandler(ledgerReader db.LedgerReader, maxLimit, defaultLimit u } // getLedgers fetch ledgers and relevant metadata from DB. -func (h ledgersHandler) getLedgers(ctx context.Context, request GetLedgersRequest) (GetLedgersResponse, error) { +func (h ledgersHandler) getLedgers(ctx context.Context, request protocol.GetLedgersRequest, +) (protocol.GetLedgersResponse, error) { readTx, err := h.ledgerReader.NewTx(ctx) if err != nil { - return GetLedgersResponse{}, &jrpc2.Error{ + return protocol.GetLedgersResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } @@ -108,14 +46,14 @@ func (h ledgersHandler) getLedgers(ctx context.Context, request GetLedgersReques ledgerRange, err := readTx.GetLedgerRange(ctx) if err != nil { - return GetLedgersResponse{}, &jrpc2.Error{ + return protocol.GetLedgersResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } - if err := request.validate(h.maxLimit, ledgerRange); err != nil { - return GetLedgersResponse{}, &jrpc2.Error{ + if err := request.Validate(h.maxLimit, ledgerRange.ToLedgerSeqRange()); err != nil { + return protocol.GetLedgersResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidRequest, Message: err.Error(), } @@ -123,7 +61,7 @@ func (h ledgersHandler) getLedgers(ctx context.Context, request GetLedgersReques start, limit, err := h.initializePagination(request, ledgerRange) if err != nil { - return GetLedgersResponse{}, &jrpc2.Error{ + return protocol.GetLedgersResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } @@ -131,11 +69,11 @@ func (h ledgersHandler) getLedgers(ctx context.Context, request GetLedgersReques ledgers, err := h.fetchLedgers(ctx, start, limit, request.Format, readTx) if err != nil { - return GetLedgersResponse{}, err + return protocol.GetLedgersResponse{}, err } cursor := strconv.Itoa(int(ledgers[len(ledgers)-1].Sequence)) - return GetLedgersResponse{ + return protocol.GetLedgersResponse{ Ledgers: ledgers, LatestLedger: ledgerRange.LastLedger.Sequence, LatestLedgerCloseTime: ledgerRange.LastLedger.CloseTime, @@ -146,7 +84,7 @@ func (h ledgersHandler) getLedgers(ctx context.Context, request GetLedgersReques } // initializePagination parses the request pagination details and initializes the cursor. -func (h ledgersHandler) initializePagination(request GetLedgersRequest, +func (h ledgersHandler) initializePagination(request protocol.GetLedgersRequest, ledgerRange ledgerbucketwindow.LedgerRange, ) (uint32, uint, error) { if request.Pagination == nil { @@ -176,7 +114,7 @@ func (h ledgersHandler) parseCursor(cursor string, ledgerRange ledgerbucketwindo } start := uint32(cursorInt) + 1 - if !isStartLedgerWithinBounds(start, ledgerRange) { + if !protocol.IsStartLedgerWithinBounds(start, ledgerRange.ToLedgerSeqRange()) { return 0, fmt.Errorf( "cursor must be between the oldest ledger: %d and the latest ledger: %d for this rpc instance", ledgerRange.FirstLedger.Sequence, @@ -190,7 +128,7 @@ func (h ledgersHandler) parseCursor(cursor string, ledgerRange ledgerbucketwindo // fetchLedgers fetches ledgers from the DB for the range [start, start+limit-1] func (h ledgersHandler) fetchLedgers(ctx context.Context, start uint32, limit uint, format string, readTx db.LedgerReaderTx, -) ([]LedgerInfo, error) { +) ([]protocol.LedgerInfo, error) { ledgers, err := readTx.BatchGetLedgers(ctx, start, limit) if err != nil { return nil, &jrpc2.Error{ @@ -199,7 +137,7 @@ func (h ledgersHandler) fetchLedgers(ctx context.Context, start uint32, } } - result := make([]LedgerInfo, 0, limit) + result := make([]protocol.LedgerInfo, 0, limit) for _, ledger := range ledgers { if uint(len(result)) >= limit { break @@ -219,8 +157,8 @@ func (h ledgersHandler) fetchLedgers(ctx context.Context, start uint32, } // parseLedgerInfo extracts and formats the ledger metadata and header information. -func (h ledgersHandler) parseLedgerInfo(ledger xdr.LedgerCloseMeta, format string) (LedgerInfo, error) { - ledgerInfo := LedgerInfo{ +func (h ledgersHandler) parseLedgerInfo(ledger xdr.LedgerCloseMeta, format string) (protocol.LedgerInfo, error) { + ledgerInfo := protocol.LedgerInfo{ Hash: ledger.LedgerHash().HexString(), Sequence: ledger.LedgerSequence(), LedgerCloseTime: ledger.LedgerCloseTime(), @@ -228,7 +166,7 @@ func (h ledgersHandler) parseLedgerInfo(ledger xdr.LedgerCloseMeta, format strin // Format the data according to the requested format (JSON or XDR) switch format { - case FormatJSON: + case protocol.FormatJSON: var convErr error ledgerInfo.LedgerMetadataJSON, ledgerInfo.LedgerHeaderJSON, convErr = ledgerToJSON(&ledger) if convErr != nil { @@ -237,12 +175,12 @@ func (h ledgersHandler) parseLedgerInfo(ledger xdr.LedgerCloseMeta, format strin default: closeMetaB, err := ledger.MarshalBinary() if err != nil { - return LedgerInfo{}, fmt.Errorf("error marshaling ledger close meta: %w", err) + return protocol.LedgerInfo{}, fmt.Errorf("error marshaling ledger close meta: %w", err) } headerB, err := ledger.LedgerHeaderHistoryEntry().MarshalBinary() if err != nil { - return LedgerInfo{}, fmt.Errorf("error marshaling ledger header: %w", err) + return protocol.LedgerInfo{}, fmt.Errorf("error marshaling ledger header: %w", err) } ledgerInfo.LedgerMetadata = base64.StdEncoding.EncodeToString(closeMetaB) diff --git a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go index dec24f3f..a6018726 100644 --- a/cmd/stellar-rpc/internal/methods/get_ledgers_test.go +++ b/cmd/stellar-rpc/internal/methods/get_ledgers_test.go @@ -13,9 +13,10 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) -var expectedLedgerInfo = LedgerInfo{ +var expectedLedgerInfo = protocol.LedgerInfo{ Hash: "0000000000000000000000000000000000000000000000000000000000000000", Sequence: 1, LedgerCloseTime: 125, @@ -44,7 +45,7 @@ func TestGetLedgers_DefaultLimit(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 1, } @@ -70,9 +71,9 @@ func TestGetLedgers_CustomLimit(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 1, - Pagination: &LedgerPaginationOptions{ + Pagination: &protocol.LedgerPaginationOptions{ Limit: 50, }, } @@ -95,8 +96,8 @@ func TestGetLedgers_WithCursor(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ - Pagination: &LedgerPaginationOptions{ + request := protocol.GetLedgersRequest{ + Pagination: &protocol.LedgerPaginationOptions{ Cursor: "5", Limit: 3, }, @@ -120,7 +121,7 @@ func TestGetLedgers_InvalidStartLedger(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 12, } @@ -137,9 +138,9 @@ func TestGetLedgers_LimitExceedsMaxLimit(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 1, - Pagination: &LedgerPaginationOptions{ + Pagination: &protocol.LedgerPaginationOptions{ Limit: 101, }, } @@ -157,8 +158,8 @@ func TestGetLedgers_InvalidCursor(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ - Pagination: &LedgerPaginationOptions{ + request := protocol.GetLedgersRequest{ + Pagination: &protocol.LedgerPaginationOptions{ Cursor: "invalid", }, } @@ -176,9 +177,9 @@ func TestGetLedgers_JSONFormat(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 1, - Format: FormatJSON, + Format: protocol.FormatJSON, } response, err := handler.getLedgers(context.TODO(), request) @@ -211,7 +212,7 @@ func TestGetLedgers_NoLedgers(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 1, } @@ -228,8 +229,8 @@ func TestGetLedgers_CursorGreaterThanLatestLedger(t *testing.T) { defaultLimit: 5, } - request := GetLedgersRequest{ - Pagination: &LedgerPaginationOptions{ + request := protocol.GetLedgersRequest{ + Pagination: &protocol.LedgerPaginationOptions{ Cursor: "15", }, } @@ -247,9 +248,9 @@ func BenchmarkGetLedgers(b *testing.B) { defaultLimit: 5, } - request := GetLedgersRequest{ + request := protocol.GetLedgersRequest{ StartLedger: 1334, - Pagination: &LedgerPaginationOptions{ + Pagination: &protocol.LedgerPaginationOptions{ Limit: 200, // using the current maximum request limit for getLedgers endpoint }, } diff --git a/cmd/stellar-rpc/internal/methods/get_network.go b/cmd/stellar-rpc/internal/methods/get_network.go index 8bd72b69..2c7efc67 100644 --- a/cmd/stellar-rpc/internal/methods/get_network.go +++ b/cmd/stellar-rpc/internal/methods/get_network.go @@ -6,16 +6,9 @@ import ( "github.com/creachadair/jrpc2" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) -type GetNetworkRequest struct{} - -type GetNetworkResponse struct { - FriendbotURL string `json:"friendbotUrl,omitempty"` - Passphrase string `json:"passphrase"` - ProtocolVersion int `json:"protocolVersion"` -} - // NewGetNetworkHandler returns a json rpc handler to for the getNetwork method func NewGetNetworkHandler( networkPassphrase string, @@ -23,16 +16,16 @@ func NewGetNetworkHandler( ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader, ) jrpc2.Handler { - return NewHandler(func(ctx context.Context, request GetNetworkRequest) (GetNetworkResponse, error) { + return NewHandler(func(ctx context.Context, _ protocol.GetNetworkRequest) (protocol.GetNetworkResponse, error) { protocolVersion, err := getProtocolVersion(ctx, ledgerEntryReader, ledgerReader) if err != nil { - return GetNetworkResponse{}, &jrpc2.Error{ + return protocol.GetNetworkResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } - return GetNetworkResponse{ + return protocol.GetNetworkResponse{ FriendbotURL: friendbotURL, Passphrase: networkPassphrase, ProtocolVersion: int(protocolVersion), diff --git a/cmd/stellar-rpc/internal/methods/get_transaction.go b/cmd/stellar-rpc/internal/methods/get_transaction.go index 9c31f092..104622f1 100644 --- a/cmd/stellar-rpc/internal/methods/get_transaction.go +++ b/cmd/stellar-rpc/internal/methods/get_transaction.go @@ -13,55 +13,18 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) -const ( - // TransactionStatusSuccess indicates the transaction was included in the ledger and - // it was executed without errors. - TransactionStatusSuccess = "SUCCESS" - // TransactionStatusNotFound indicates the transaction was not found in Stellar-RPC's - // transaction store. - TransactionStatusNotFound = "NOT_FOUND" - // TransactionStatusFailed indicates the transaction was included in the ledger and - // it was executed with an error. - TransactionStatusFailed = "FAILED" -) - -// GetTransactionResponse is the response for the Stellar-RPC getTransaction() endpoint -type GetTransactionResponse struct { - // LatestLedger is the latest ledger stored in Stellar-RPC. - LatestLedger uint32 `json:"latestLedger"` - // LatestLedgerCloseTime is the unix timestamp of when the latest ledger was closed. - LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime,string"` - // LatestLedger is the oldest ledger stored in Stellar-RPC. - OldestLedger uint32 `json:"oldestLedger"` - // LatestLedgerCloseTime is the unix timestamp of when the oldest ledger was closed. - OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTime,string"` - - // Many of the fields below are only present if Status is not - // TransactionNotFound. - TransactionDetails - // LedgerCloseTime is the unix timestamp of when the transaction was - // included in the ledger. It isn't part of `TransactionInfo` because of a - // bug in which `createdAt` in getTransactions is encoded as a number - // whereas in getTransaction (singular) it's encoded as a string. - LedgerCloseTime int64 `json:"createdAt,string"` -} - -type GetTransactionRequest struct { - Hash string `json:"hash"` - Format string `json:"xdrFormat,omitempty"` -} - func GetTransaction( ctx context.Context, log *log.Entry, reader db.TransactionReader, ledgerReader db.LedgerReader, - request GetTransactionRequest, -) (GetTransactionResponse, error) { - if err := IsValidFormat(request.Format); err != nil { - return GetTransactionResponse{}, &jrpc2.Error{ + request protocol.GetTransactionRequest, +) (protocol.GetTransactionResponse, error) { + if err := protocol.IsValidFormat(request.Format); err != nil { + return protocol.GetTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } @@ -69,7 +32,7 @@ func GetTransaction( // parse hash if hex.DecodedLen(len(request.Hash)) != len(xdr.Hash{}) { - return GetTransactionResponse{}, &jrpc2.Error{ + return protocol.GetTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: fmt.Sprintf("unexpected hash length (%d)", len(request.Hash)), } @@ -78,7 +41,7 @@ func GetTransaction( var txHash xdr.Hash _, err := hex.Decode(txHash[:], []byte(request.Hash)) if err != nil { - return GetTransactionResponse{}, &jrpc2.Error{ + return protocol.GetTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: fmt.Sprintf("incorrect hash: %v", err), } @@ -86,7 +49,7 @@ func GetTransaction( storeRange, err := ledgerReader.GetLedgerRange(ctx) if err != nil { - return GetTransactionResponse{}, &jrpc2.Error{ + return protocol.GetTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: fmt.Sprintf("unable to get ledger range: %v", err), } @@ -94,7 +57,7 @@ func GetTransaction( tx, err := reader.GetTransaction(ctx, txHash) - response := GetTransactionResponse{ + response := protocol.GetTransactionResponse{ LatestLedger: storeRange.LastLedger.Sequence, LatestLedgerCloseTime: storeRange.LastLedger.CloseTime, OldestLedger: storeRange.FirstLedger.Sequence, @@ -102,7 +65,7 @@ func GetTransaction( } response.TransactionHash = request.Hash if errors.Is(err, db.ErrNoTransaction) { - response.Status = TransactionStatusNotFound + response.Status = protocol.TransactionStatusNotFound return response, nil } else if err != nil { log.WithError(err). @@ -120,7 +83,7 @@ func GetTransaction( response.LedgerCloseTime = tx.Ledger.CloseTime switch request.Format { - case FormatJSON: + case protocol.FormatJSON: result, envelope, meta, convErr := transactionToJSON(tx) if convErr != nil { return response, &jrpc2.Error{ @@ -148,9 +111,9 @@ func GetTransaction( response.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) } - response.Status = TransactionStatusFailed + response.Status = protocol.TransactionStatusFailed if tx.Successful { - response.Status = TransactionStatusSuccess + response.Status = protocol.TransactionStatusSuccess } return response, nil } @@ -160,7 +123,8 @@ func GetTransaction( func NewGetTransactionHandler(logger *log.Entry, getter db.TransactionReader, ledgerReader db.LedgerReader, ) jrpc2.Handler { - return NewHandler(func(ctx context.Context, request GetTransactionRequest) (GetTransactionResponse, error) { + return NewHandler(func(ctx context.Context, request protocol.GetTransactionRequest, + ) (protocol.GetTransactionResponse, error) { return GetTransaction(ctx, logger, getter, ledgerReader, request) }) } diff --git a/cmd/stellar-rpc/internal/methods/get_transaction_test.go b/cmd/stellar-rpc/internal/methods/get_transaction_test.go index 74c334c4..f88ebbd6 100644 --- a/cmd/stellar-rpc/internal/methods/get_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transaction_test.go @@ -15,6 +15,7 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) func TestGetTransaction(t *testing.T) { @@ -26,18 +27,28 @@ func TestGetTransaction(t *testing.T) { ) log.SetLevel(logrus.DebugLevel) - _, err := GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{"ab", ""}) + _, err := GetTransaction(ctx, log, store, ledgerReader, + protocol.GetTransactionRequest{ + Hash: "ab", + Format: "", + }, + ) require.EqualError(t, err, "[-32602] unexpected hash length (2)") _, err = GetTransaction(ctx, log, store, ledgerReader, - GetTransactionRequest{"foo ", ""}) + protocol.GetTransactionRequest{ + Hash: "foo ", + Format: "", + }) require.EqualError(t, err, "[-32602] incorrect hash: encoding/hex: invalid byte: U+006F 'o'") hash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - tx, err := GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) + tx, err := GetTransaction(ctx, log, store, ledgerReader, + protocol.GetTransactionRequest{Hash: hash, Format: ""}, + ) require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ - TransactionDetails: TransactionDetails{ - Status: TransactionStatusNotFound, + require.Equal(t, protocol.GetTransactionResponse{ + TransactionDetails: protocol.TransactionDetails{ + Status: protocol.TransactionStatusNotFound, TransactionHash: hash, }, }, tx) @@ -47,7 +58,9 @@ func TestGetTransaction(t *testing.T) { xdrHash := txHash(1) hash = hex.EncodeToString(xdrHash[:]) - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, + protocol.GetTransactionRequest{Hash: hash, Format: ""}, + ) require.NoError(t, err) expectedTxResult, err := xdr.MarshalBase64(meta.V1.TxProcessing[0].Result.Result) @@ -56,13 +69,13 @@ func TestGetTransaction(t *testing.T) { require.NoError(t, err) expectedTxMeta, err := xdr.MarshalBase64(meta.V1.TxProcessing[0].TxApplyProcessing) require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ + require.Equal(t, protocol.GetTransactionResponse{ LatestLedger: 101, LatestLedgerCloseTime: 2625, OldestLedger: 101, OldestLedgerCloseTime: 2625, - TransactionDetails: TransactionDetails{ - Status: TransactionStatusSuccess, + TransactionDetails: protocol.TransactionDetails{ + Status: protocol.TransactionStatusSuccess, ApplicationOrder: 1, FeeBump: false, TransactionHash: hash, @@ -80,15 +93,20 @@ func TestGetTransaction(t *testing.T) { require.NoError(t, store.InsertTransactions(meta)) // the first transaction should still be there - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, + protocol.GetTransactionRequest{ + Hash: hash, + Format: "", + }, + ) require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ + require.Equal(t, protocol.GetTransactionResponse{ LatestLedger: 102, LatestLedgerCloseTime: 2650, OldestLedger: 101, OldestLedgerCloseTime: 2625, - TransactionDetails: TransactionDetails{ - Status: TransactionStatusSuccess, + TransactionDetails: protocol.TransactionDetails{ + Status: protocol.TransactionStatusSuccess, ApplicationOrder: 1, TransactionHash: hash, FeeBump: false, @@ -112,15 +130,20 @@ func TestGetTransaction(t *testing.T) { expectedTxMeta, err = xdr.MarshalBase64(meta.V1.TxProcessing[0].TxApplyProcessing) require.NoError(t, err) - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, + protocol.GetTransactionRequest{ + Hash: hash, + Format: "", + }, + ) require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ + require.Equal(t, protocol.GetTransactionResponse{ LatestLedger: 102, LatestLedgerCloseTime: 2650, OldestLedger: 101, OldestLedgerCloseTime: 2625, - TransactionDetails: TransactionDetails{ - Status: TransactionStatusFailed, + TransactionDetails: protocol.TransactionDetails{ + Status: protocol.TransactionStatusFailed, ApplicationOrder: 1, FeeBump: false, TransactionHash: hash, @@ -152,11 +175,15 @@ func TestGetTransaction(t *testing.T) { expectedEventsMeta, err := xdr.MarshalBase64(diagnosticEvents[0]) require.NoError(t, err) - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, + protocol.GetTransactionRequest{ + Hash: hash, + Format: "", + }) require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ - TransactionDetails: TransactionDetails{ - Status: TransactionStatusSuccess, + require.Equal(t, protocol.GetTransactionResponse{ + TransactionDetails: protocol.TransactionDetails{ + Status: protocol.TransactionStatusSuccess, ApplicationOrder: 1, FeeBump: false, TransactionHash: hash, @@ -328,8 +355,8 @@ func TestGetTransaction_JSONFormat(t *testing.T) { } } - request := GetTransactionRequest{ - Format: FormatJSON, + request := protocol.GetTransactionRequest{ + Format: protocol.FormatJSON, Hash: lookupHash, } @@ -382,8 +409,8 @@ func BenchmarkJSONTransactions(b *testing.B) { b.ResetTimer() b.Run("JSON format", func(bb *testing.B) { - request := GetTransactionRequest{ - Format: FormatJSON, + request := protocol.GetTransactionRequest{ + Format: protocol.FormatJSON, Hash: lookupHash, } bb.ResetTimer() @@ -401,7 +428,7 @@ func BenchmarkJSONTransactions(b *testing.B) { b.ResetTimer() b.Run("XDR format", func(bb *testing.B) { - request := GetTransactionRequest{Hash: lookupHash} + request := protocol.GetTransactionRequest{Hash: lookupHash} bb.ResetTimer() for range bb.N { diff --git a/cmd/stellar-rpc/internal/methods/get_transactions.go b/cmd/stellar-rpc/internal/methods/get_transactions.go index 20d4c6a2..bb8c2bf4 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions.go @@ -3,7 +3,6 @@ package methods import ( "context" "encoding/base64" - "encoding/json" "errors" "fmt" "io" @@ -18,90 +17,9 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" - "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/ledgerbucketwindow" + "github.com/stellar/stellar-rpc/protocol" ) -// TransactionsPaginationOptions defines the available options for paginating through transactions. -type TransactionsPaginationOptions struct { - Cursor string `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` -} - -// GetTransactionsRequest represents the request parameters for fetching transactions within a range of ledgers. -type GetTransactionsRequest struct { - StartLedger uint32 `json:"startLedger"` - Pagination *TransactionsPaginationOptions `json:"pagination,omitempty"` - Format string `json:"xdrFormat,omitempty"` -} - -// isValid checks the validity of the request parameters. -func (req GetTransactionsRequest) isValid(maxLimit uint, ledgerRange ledgerbucketwindow.LedgerRange) error { - if req.Pagination != nil && req.Pagination.Cursor != "" { - if req.StartLedger != 0 { - return errors.New("startLedger and cursor cannot both be set") - } - } else if req.StartLedger < ledgerRange.FirstLedger.Sequence || req.StartLedger > ledgerRange.LastLedger.Sequence { - return fmt.Errorf( - "start ledger must be between the oldest ledger: %d and the latest ledger: %d for this rpc instance", - ledgerRange.FirstLedger.Sequence, - ledgerRange.LastLedger.Sequence, - ) - } - - if req.Pagination != nil && req.Pagination.Limit > maxLimit { - return fmt.Errorf("limit must not exceed %d", maxLimit) - } - - return IsValidFormat(req.Format) -} - -type TransactionDetails struct { - // Status is one of: TransactionSuccess, TransactionFailed, TransactionNotFound. - Status string `json:"status"` - // TransactionHash is the hex encoded hash of the transaction. Note that for - // fee-bump transaction this will be the hash of the fee-bump transaction - // instead of the inner transaction hash. - TransactionHash string `json:"txHash"` - // ApplicationOrder is the index of the transaction among all the - // transactions for that ledger. - ApplicationOrder int32 `json:"applicationOrder"` - // FeeBump indicates whether the transaction is a feebump transaction - FeeBump bool `json:"feeBump"` - // EnvelopeXDR is the TransactionEnvelope XDR value. - EnvelopeXDR string `json:"envelopeXdr,omitempty"` - EnvelopeJSON json.RawMessage `json:"envelopeJson,omitempty"` - // ResultXDR is the TransactionResult XDR value. - ResultXDR string `json:"resultXdr,omitempty"` - ResultJSON json.RawMessage `json:"resultJson,omitempty"` - // ResultMetaXDR is the TransactionMeta XDR value. - ResultMetaXDR string `json:"resultMetaXdr,omitempty"` - ResultMetaJSON json.RawMessage `json:"resultMetaJson,omitempty"` - // DiagnosticEventsXDR is present only if transaction was not successful. - // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent - DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` - DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` - // Ledger is the sequence of the ledger which included the transaction. - Ledger uint32 `json:"ledger"` -} - -type TransactionInfo struct { - TransactionDetails - - // LedgerCloseTime is the unix timestamp of when the transaction was - // included in the ledger. - LedgerCloseTime int64 `json:"createdAt"` -} - -// GetTransactionsResponse encapsulates the response structure for getTransactions queries. -type GetTransactionsResponse struct { - Transactions []TransactionInfo `json:"transactions"` - LatestLedger uint32 `json:"latestLedger"` - LatestLedgerCloseTime int64 `json:"latestLedgerCloseTimestamp"` - OldestLedger uint32 `json:"oldestLedger"` - OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTimestamp"` - Cursor string `json:"cursor"` -} - type transactionsRPCHandler struct { ledgerReader db.LedgerReader maxLimit uint @@ -111,7 +29,7 @@ type transactionsRPCHandler struct { } // initializePagination sets the pagination limit and cursor -func (h transactionsRPCHandler) initializePagination(request GetTransactionsRequest) (toid.ID, uint, error) { +func (h transactionsRPCHandler) initializePagination(request protocol.GetTransactionsRequest) (toid.ID, uint, error) { start := toid.New(int32(request.StartLedger), 1, 1) limit := h.defaultLimit if request.Pagination != nil { @@ -158,7 +76,7 @@ func (h transactionsRPCHandler) fetchLedgerData(ctx context.Context, ledgerSeq u // and builds the list of transactions. func (h transactionsRPCHandler) processTransactionsInLedger( ledger xdr.LedgerCloseMeta, start toid.ID, - txns *[]TransactionInfo, limit uint, + txns *[]protocol.TransactionInfo, limit uint, format string, ) (*toid.ID, bool, error) { reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(h.networkPassphrase, ledger) @@ -205,8 +123,8 @@ func (h transactionsRPCHandler) processTransactionsInLedger( } } - txInfo := TransactionInfo{ - TransactionDetails: TransactionDetails{ + txInfo := protocol.TransactionInfo{ + TransactionDetails: protocol.TransactionDetails{ TransactionHash: tx.TransactionHash, ApplicationOrder: tx.ApplicationOrder, FeeBump: tx.FeeBump, @@ -216,7 +134,7 @@ func (h transactionsRPCHandler) processTransactionsInLedger( } switch format { - case FormatJSON: + case protocol.FormatJSON: result, envelope, meta, convErr := transactionToJSON(tx) if convErr != nil { return nil, false, &jrpc2.Error{ @@ -245,9 +163,9 @@ func (h transactionsRPCHandler) processTransactionsInLedger( txInfo.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) } - txInfo.Status = TransactionStatusFailed + txInfo.Status = protocol.TransactionStatusFailed if tx.Successful { - txInfo.Status = TransactionStatusSuccess + txInfo.Status = protocol.TransactionStatusSuccess } *txns = append(*txns, txInfo) @@ -262,11 +180,11 @@ func (h transactionsRPCHandler) processTransactionsInLedger( // getTransactionsByLedgerSequence fetches transactions between the start and end ledgers, inclusive of both. // The number of ledgers returned can be tuned using the pagination options - cursor and limit. func (h transactionsRPCHandler) getTransactionsByLedgerSequence(ctx context.Context, - request GetTransactionsRequest, -) (GetTransactionsResponse, error) { + request protocol.GetTransactionsRequest, +) (protocol.GetTransactionsResponse, error) { readTx, err := h.ledgerReader.NewTx(ctx) if err != nil { - return GetTransactionsResponse{}, &jrpc2.Error{ + return protocol.GetTransactionsResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } @@ -277,15 +195,15 @@ func (h transactionsRPCHandler) getTransactionsByLedgerSequence(ctx context.Cont ledgerRange, err := readTx.GetLedgerRange(ctx) if err != nil { - return GetTransactionsResponse{}, &jrpc2.Error{ + return protocol.GetTransactionsResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: err.Error(), } } - err = request.isValid(h.maxLimit, ledgerRange) + err = request.IsValid(h.maxLimit, ledgerRange.ToLedgerSeqRange()) if err != nil { - return GetTransactionsResponse{}, &jrpc2.Error{ + return protocol.GetTransactionsResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidRequest, Message: err.Error(), } @@ -293,30 +211,30 @@ func (h transactionsRPCHandler) getTransactionsByLedgerSequence(ctx context.Cont start, limit, err := h.initializePagination(request) if err != nil { - return GetTransactionsResponse{}, err + return protocol.GetTransactionsResponse{}, err } // Iterate through each ledger and its transactions until limit or end range is reached. // The latest ledger acts as the end ledger range for the request. - var txns []TransactionInfo + var txns []protocol.TransactionInfo var done bool cursor := toid.New(0, 0, 0) for ledgerSeq := start.LedgerSequence; ledgerSeq <= int32(ledgerRange.LastLedger.Sequence); ledgerSeq++ { ledger, err := h.fetchLedgerData(ctx, uint32(ledgerSeq), readTx) if err != nil { - return GetTransactionsResponse{}, err + return protocol.GetTransactionsResponse{}, err } cursor, done, err = h.processTransactionsInLedger(ledger, start, &txns, limit, request.Format) if err != nil { - return GetTransactionsResponse{}, err + return protocol.GetTransactionsResponse{}, err } if done { break } } - return GetTransactionsResponse{ + return protocol.GetTransactionsResponse{ Transactions: txns, LatestLedger: ledgerRange.LastLedger.Sequence, LatestLedgerCloseTime: ledgerRange.LastLedger.CloseTime, diff --git a/cmd/stellar-rpc/internal/methods/get_transactions_test.go b/cmd/stellar-rpc/internal/methods/get_transactions_test.go index f8a91bc5..839daaee 100644 --- a/cmd/stellar-rpc/internal/methods/get_transactions_test.go +++ b/cmd/stellar-rpc/internal/methods/get_transactions_test.go @@ -16,14 +16,15 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) const ( NetworkPassphrase string = "passphrase" ) -var expectedTransactionInfo = TransactionInfo{ - TransactionDetails{ +var expectedTransactionInfo = protocol.TransactionInfo{ + TransactionDetails: protocol.TransactionDetails{ Status: "SUCCESS", TransactionHash: "b0d0b35dcaed0152d62fbbaa28ed3fa4991c87e7e169a8fca2687b17ee26ca2d", ApplicationOrder: 1, @@ -34,7 +35,7 @@ var expectedTransactionInfo = TransactionInfo{ ResultXDR: "AAAAAAAAAGQAAAAAAAAAAAAAAAA=", DiagnosticEventsXDR: []string{}, }, - 125, + LedgerCloseTime: 125, } // createTestLedger Creates a test ledger with 2 transactions @@ -80,7 +81,7 @@ func TestGetTransactions_DefaultLimit(t *testing.T) { //nolint:dupl networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: 1, } @@ -110,7 +111,7 @@ func TestGetTransactions_DefaultLimitExceedsLatestLedger(t *testing.T) { //nolin networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: 1, } @@ -132,9 +133,9 @@ func TestGetTransactions_CustomLimit(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: 1, - Pagination: &TransactionsPaginationOptions{ + Pagination: &protocol.TransactionsPaginationOptions{ Limit: 2, }, } @@ -159,8 +160,8 @@ func TestGetTransactions_CustomLimitAndCursor(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ - Pagination: &TransactionsPaginationOptions{ + request := protocol.GetTransactionsRequest{ + Pagination: &protocol.TransactionsPaginationOptions{ Cursor: toid.New(1, 2, 1).String(), Limit: 3, }, @@ -186,7 +187,7 @@ func TestGetTransactions_InvalidStartLedger(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: 4, } @@ -208,7 +209,7 @@ func TestGetTransactions_LedgerNotFound(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: 1, } @@ -227,9 +228,9 @@ func TestGetTransactions_LimitGreaterThanMaxLimit(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ + request := protocol.GetTransactionsRequest{ StartLedger: 1, - Pagination: &TransactionsPaginationOptions{ + Pagination: &protocol.TransactionsPaginationOptions{ Limit: 200, }, } @@ -248,8 +249,8 @@ func TestGetTransactions_InvalidCursorString(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ - Pagination: &TransactionsPaginationOptions{ + request := protocol.GetTransactionsRequest{ + Pagination: &protocol.TransactionsPaginationOptions{ Cursor: "abc", }, } @@ -268,8 +269,8 @@ func TestGetTransactions_JSONFormat(t *testing.T) { networkPassphrase: NetworkPassphrase, } - request := GetTransactionsRequest{ - Format: FormatJSON, + request := protocol.GetTransactionsRequest{ + Format: protocol.FormatJSON, StartLedger: 1, } diff --git a/cmd/stellar-rpc/internal/methods/get_version_info.go b/cmd/stellar-rpc/internal/methods/get_version_info.go index ad5d9d79..25bfce4b 100644 --- a/cmd/stellar-rpc/internal/methods/get_version_info.go +++ b/cmd/stellar-rpc/internal/methods/get_version_info.go @@ -11,24 +11,9 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/config" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) -type GetVersionInfoResponse struct { - Version string `json:"version"` - CommitHash string `json:"commitHash"` - BuildTimestamp string `json:"buildTimestamp"` - CaptiveCoreVersion string `json:"captiveCoreVersion"` - ProtocolVersion uint32 `json:"protocolVersion"` - //nolint:tagliatelle - CommitHashDeprecated string `json:"commit_hash"` - //nolint:tagliatelle - BuildTimestampDeprecated string `json:"build_time_stamp"` - //nolint:tagliatelle - CaptiveCoreVersionDeprecated string `json:"captive_core_version"` - //nolint:tagliatelle - ProtocolVersionDeprecated uint32 `json:"protocol_version"` -} - func NewGetVersionInfoHandler( logger *log.Entry, ledgerEntryReader db.LedgerEntryReader, @@ -37,14 +22,14 @@ func NewGetVersionInfoHandler( ) jrpc2.Handler { core := daemon.GetCore() - return handler.New(func(ctx context.Context) (GetVersionInfoResponse, error) { + return handler.New(func(ctx context.Context) (protocol.GetVersionInfoResponse, error) { captiveCoreVersion := core.GetCoreVersion() protocolVersion, err := getProtocolVersion(ctx, ledgerEntryReader, ledgerReader) if err != nil { logger.WithError(err).Error("failed to fetch protocol version") } - return GetVersionInfoResponse{ + return protocol.GetVersionInfoResponse{ Version: config.Version, CommitHash: config.CommitHash, CommitHashDeprecated: config.CommitHash, diff --git a/cmd/stellar-rpc/internal/methods/json.go b/cmd/stellar-rpc/internal/methods/json.go index 310d1995..63a778d9 100644 --- a/cmd/stellar-rpc/internal/methods/json.go +++ b/cmd/stellar-rpc/internal/methods/json.go @@ -1,37 +1,12 @@ package methods import ( - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" ) -const ( - FormatBase64 = "base64" - FormatJSON = "json" -) - -var errInvalidFormat = fmt.Errorf( - "expected %s for optional 'xdrFormat'", - strings.Join([]string{FormatBase64, FormatJSON}, ", ")) - -func IsValidFormat(format string) error { - switch format { - case "": - case FormatJSON: - case FormatBase64: - default: - return errors.Wrapf(errInvalidFormat, "got '%s'", format) - } - return nil -} - func transactionToJSON(tx db.Transaction) ( []byte, []byte, diff --git a/cmd/stellar-rpc/internal/methods/send_transaction.go b/cmd/stellar-rpc/internal/methods/send_transaction.go index d23bc648..10bf833f 100644 --- a/cmd/stellar-rpc/internal/methods/send_transaction.go +++ b/cmd/stellar-rpc/internal/methods/send_transaction.go @@ -16,43 +16,9 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/daemon/interfaces" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) -// SendTransactionResponse represents the transaction submission response returned Stellar-RPC -type SendTransactionResponse struct { - // ErrorResultXDR is present only if Status is equal to proto.TXStatusError. - // ErrorResultXDR is a TransactionResult xdr string which contains details on why - // the transaction could not be accepted by stellar-core. - ErrorResultXDR string `json:"errorResultXdr,omitempty"` - ErrorResultJSON json.RawMessage `json:"errorResultJson,omitempty"` - - // DiagnosticEventsXDR is present only if Status is equal to proto.TXStatusError. - // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent - DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` - DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` - - // Status represents the status of the transaction submission returned by stellar-core. - // Status can be one of: proto.TXStatusPending, proto.TXStatusDuplicate, - // proto.TXStatusTryAgainLater, or proto.TXStatusError. - Status string `json:"status"` - // Hash is a hash of the transaction which can be used to look up whether - // the transaction was included in the ledger. - Hash string `json:"hash"` - // LatestLedger is the latest ledger known to Stellar-RPC at the time it handled - // the transaction submission request. - LatestLedger uint32 `json:"latestLedger"` - // LatestLedgerCloseTime is the unix timestamp of the close time of the latest ledger known to - // Stellar-RPC at the time it handled the transaction submission request. - LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime,string"` -} - -// SendTransactionRequest is the Stellar-RPC request to submit a transaction. -type SendTransactionRequest struct { - // Transaction is the base64 encoded transaction envelope. - Transaction string `json:"transaction"` - Format string `json:"xdrFormat,omitempty"` -} - // NewSendTransactionHandler returns a submit transaction json rpc handler func NewSendTransactionHandler( daemon interfaces.Daemon, @@ -61,9 +27,10 @@ func NewSendTransactionHandler( passphrase string, ) jrpc2.Handler { submitter := daemon.CoreClient() - return NewHandler(func(ctx context.Context, request SendTransactionRequest) (SendTransactionResponse, error) { - if err := IsValidFormat(request.Format); err != nil { - return SendTransactionResponse{}, &jrpc2.Error{ + return NewHandler(func(ctx context.Context, request protocol.SendTransactionRequest, + ) (protocol.SendTransactionResponse, error) { + if err := protocol.IsValidFormat(request.Format); err != nil { + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: err.Error(), } @@ -72,7 +39,7 @@ func NewSendTransactionHandler( var envelope xdr.TransactionEnvelope err := xdr.SafeUnmarshalBase64(request.Transaction, &envelope) if err != nil { - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: "invalid_xdr", } @@ -81,7 +48,7 @@ func NewSendTransactionHandler( var hash [32]byte hash, err = network.HashTransactionInEnvelope(envelope, passphrase) if err != nil { - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, Message: "invalid_hash", } @@ -101,7 +68,7 @@ func NewSendTransactionHandler( logger.WithError(err). WithField("tx", request.Transaction). Error("could not submit transaction") - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not submit transaction to stellar-core", } @@ -111,7 +78,7 @@ func NewSendTransactionHandler( if resp.IsException() { logger.WithField("exception", resp.Exception). WithField("tx", request.Transaction).Error("received exception from stellar core") - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "received exception from stellar-core", } @@ -119,7 +86,7 @@ func NewSendTransactionHandler( switch resp.Status { case proto.TXStatusError: - errorResp := SendTransactionResponse{ + errorResp := protocol.SendTransactionResponse{ Status: resp.Status, Hash: txHash, LatestLedger: latestLedgerInfo.Sequence, @@ -127,14 +94,14 @@ func NewSendTransactionHandler( } switch request.Format { - case FormatJSON: + case protocol.FormatJSON: errResult := xdr.TransactionResult{} err = xdr.SafeUnmarshalBase64(resp.Error, &errResult) if err != nil { logger.WithField("tx", request.Transaction). WithError(err).Error("Cannot decode error result") - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: errors.Wrap(err, "couldn't decode error").Error(), } @@ -145,7 +112,7 @@ func NewSendTransactionHandler( logger.WithField("tx", request.Transaction). WithError(err).Error("Cannot JSONify error result") - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: errors.Wrap(err, "couldn't serialize error").Error(), } @@ -157,7 +124,7 @@ func NewSendTransactionHandler( logger.WithField("tx", request.Transaction). WithError(err).Error("Cannot decode events") - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: errors.Wrap(err, "couldn't decode events").Error(), } @@ -170,7 +137,7 @@ func NewSendTransactionHandler( logger.WithField("tx", request.Transaction). WithError(err).Errorf("Cannot decode event %d: %+v", i+1, event) - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: errors.Wrapf(err, "couldn't decode event #%d", i+1).Error(), } @@ -181,7 +148,7 @@ func NewSendTransactionHandler( events, err := proto.DiagnosticEventsToSlice(resp.DiagnosticEvents) if err != nil { logger.WithField("tx", request.Transaction).Error("Cannot decode diagnostic events:", err) - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not decode diagnostic events", } @@ -194,7 +161,7 @@ func NewSendTransactionHandler( return errorResp, nil case proto.TXStatusPending, proto.TXStatusDuplicate, proto.TXStatusTryAgainLater: - return SendTransactionResponse{ + return protocol.SendTransactionResponse{ Status: resp.Status, Hash: txHash, LatestLedger: latestLedgerInfo.Sequence, @@ -204,7 +171,7 @@ func NewSendTransactionHandler( default: logger.WithField("status", resp.Status). WithField("tx", request.Transaction).Error("Unrecognized stellar-core status response") - return SendTransactionResponse{}, &jrpc2.Error{ + return protocol.SendTransactionResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "invalid status from stellar-core", } diff --git a/cmd/stellar-rpc/internal/methods/simulate_transaction.go b/cmd/stellar-rpc/internal/methods/simulate_transaction.go index dc56fff8..50b29bca 100644 --- a/cmd/stellar-rpc/internal/methods/simulate_transaction.go +++ b/cmd/stellar-rpc/internal/methods/simulate_transaction.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "strings" "github.com/creachadair/jrpc2" @@ -17,86 +16,21 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/preflight" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) -type SimulateTransactionRequest struct { - Transaction string `json:"transaction"` - ResourceConfig *preflight.ResourceConfig `json:"resourceConfig,omitempty"` - Format string `json:"xdrFormat,omitempty"` -} - -// SimulateHostFunctionResult contains the simulation result of each HostFunction within the single -// InvokeHostFunctionOp allowed in a Transaction -type SimulateHostFunctionResult struct { - AuthXDR *[]string `json:"auth,omitempty"` - AuthJSON []json.RawMessage `json:"authJson,omitempty"` - - ReturnValueXDR *string `json:"xdr,omitempty"` - ReturnValueJSON json.RawMessage `json:"returnValueJson,omitempty"` -} - -type RestorePreamble struct { - // TransactionDataXDR is an xdr.SorobanTransactionData in base64 - TransactionDataXDR string `json:"transactionData,omitempty"` - TransactionDataJSON json.RawMessage `json:"transactionDataJson,omitempty"` - - MinResourceFee int64 `json:"minResourceFee,string"` -} -type LedgerEntryChangeType int - -const ( - LedgerEntryChangeTypeCreated LedgerEntryChangeType = iota + 1 - LedgerEntryChangeTypeUpdated - LedgerEntryChangeTypeDeleted -) - -var ( - LedgerEntryChangeTypeName = map[LedgerEntryChangeType]string{ - LedgerEntryChangeTypeCreated: "created", - LedgerEntryChangeTypeUpdated: "updated", - LedgerEntryChangeTypeDeleted: "deleted", - } - LedgerEntryChangeTypeValue = map[string]LedgerEntryChangeType{ - "created": LedgerEntryChangeTypeCreated, - "updated": LedgerEntryChangeTypeUpdated, - "deleted": LedgerEntryChangeTypeDeleted, - } -) - -func (l LedgerEntryChangeType) String() string { - return LedgerEntryChangeTypeName[l] -} - -func (l LedgerEntryChangeType) MarshalJSON() ([]byte, error) { - return json.Marshal(l.String()) -} - -func (l *LedgerEntryChangeType) Parse(s string) error { - s = strings.TrimSpace(strings.ToLower(s)) - value, ok := LedgerEntryChangeTypeValue[s] - if !ok { - return fmt.Errorf("%q is not a valid ledger entry change type", s) - } - *l = value - return nil -} - -func (l *LedgerEntryChangeType) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - return l.Parse(s) +type PreflightGetter interface { + GetPreflight(ctx context.Context, params preflight.GetterParameters) (preflight.Preflight, error) } -func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff, format string) error { - if err := IsValidFormat(format); err != nil { - return err +func LedgerEntryChangeFromXDRDiff(diff preflight.XDRDiff, format string) (protocol.LedgerEntryChange, error) { + if err := protocol.IsValidFormat(format); err != nil { + return protocol.LedgerEntryChange{}, err } var ( entryXDR []byte - changeType LedgerEntryChangeType + changeType protocol.LedgerEntryChangeType ) beforePresent := len(diff.Before) > 0 @@ -105,60 +39,65 @@ func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff, format string) e case beforePresent: entryXDR = diff.Before if afterPresent { - changeType = LedgerEntryChangeTypeUpdated + changeType = protocol.LedgerEntryChangeTypeUpdated } else { - changeType = LedgerEntryChangeTypeDeleted + changeType = protocol.LedgerEntryChangeTypeDeleted } case afterPresent: entryXDR = diff.After - changeType = LedgerEntryChangeTypeCreated + changeType = protocol.LedgerEntryChangeTypeCreated default: - return errors.New("missing before and after") + return protocol.LedgerEntryChange{}, errors.New("missing before and after") } - l.Type = changeType + var result protocol.LedgerEntryChange + + result.Type = changeType // We need to unmarshal the ledger entry for both b64 and json cases // because we need the inner ledger key. var entry xdr.LedgerEntry if err := xdr.SafeUnmarshal(entryXDR, &entry); err != nil { - return err + return protocol.LedgerEntryChange{}, err } key, err := entry.LedgerKey() if err != nil { - return err + return protocol.LedgerEntryChange{}, err } switch format { - case FormatJSON: - return l.jsonXdrDiff(diff, key) + case protocol.FormatJSON: + err = AddLedgerEntryChangeJSON(&result, diff, key) + if err != nil { + return protocol.LedgerEntryChange{}, err + } default: keyB64, err := xdr.MarshalBase64(key) if err != nil { - return err + return protocol.LedgerEntryChange{}, err } - l.KeyXDR = keyB64 + result.KeyXDR = keyB64 if beforePresent { before := base64.StdEncoding.EncodeToString(diff.Before) - l.BeforeXDR = &before + result.BeforeXDR = &before } if afterPresent { after := base64.StdEncoding.EncodeToString(diff.After) - l.AfterXDR = &after + result.AfterXDR = &after } } - return nil + return result, nil } -func (l *LedgerEntryChange) jsonXdrDiff(diff preflight.XDRDiff, key xdr.LedgerKey) error { +func AddLedgerEntryChangeJSON(l *protocol.LedgerEntryChange, diff preflight.XDRDiff, key xdr.LedgerKey) error { var err error beforePresent := len(diff.Before) > 0 afterPresent := len(diff.After) > 0 @@ -185,61 +124,24 @@ func (l *LedgerEntryChange) jsonXdrDiff(diff preflight.XDRDiff, key xdr.LedgerKe return nil } -// LedgerEntryChange designates a change in a ledger entry. Before and After cannot be omitted at the same time. -// If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a deletion. -type LedgerEntryChange struct { - Type LedgerEntryChangeType `json:"type"` - - KeyXDR string `json:"key,omitempty"` // LedgerEntryKey in base64 - KeyJSON json.RawMessage `json:"keyJson,omitempty"` - - BeforeXDR *string `json:"before"` // LedgerEntry XDR in base64 - BeforeJSON json.RawMessage `json:"beforeJson,omitempty"` - - AfterXDR *string `json:"after"` // LedgerEntry XDR in base64 - AfterJSON json.RawMessage `json:"afterJson,omitempty"` -} - -type SimulateTransactionResponse struct { - Error string `json:"error,omitempty"` - - TransactionDataXDR string `json:"transactionData,omitempty"` // SorobanTransactionData XDR in base64 - TransactionDataJSON json.RawMessage `json:"transactionDataJson,omitempty"` - - EventsXDR []string `json:"events,omitempty"` // DiagnosticEvent XDR in base64 - EventsJSON []json.RawMessage `json:"eventsJson,omitempty"` - - MinResourceFee int64 `json:"minResourceFee,string,omitempty"` - // an array of the individual host function call results - Results []SimulateHostFunctionResult `json:"results,omitempty"` - // If present, it indicates that a prior RestoreFootprint is required - RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` - // If present, it indicates how the state (ledger entries) will change as a result of the transaction execution. - StateChanges []LedgerEntryChange `json:"stateChanges,omitempty"` - LatestLedger uint32 `json:"latestLedger"` -} - -type PreflightGetter interface { - GetPreflight(ctx context.Context, params preflight.GetterParameters) (preflight.Preflight, error) -} - // NewSimulateTransactionHandler returns a json rpc handler to run preflight simulations func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader, daemon interfaces.Daemon, getter PreflightGetter) jrpc2.Handler { - return NewHandler(func(ctx context.Context, request SimulateTransactionRequest) SimulateTransactionResponse { - if err := IsValidFormat(request.Format); err != nil { - return SimulateTransactionResponse{Error: err.Error()} + return NewHandler(func(ctx context.Context, request protocol.SimulateTransactionRequest, + ) protocol.SimulateTransactionResponse { + if err := protocol.IsValidFormat(request.Format); err != nil { + return protocol.SimulateTransactionResponse{Error: err.Error()} } var txEnvelope xdr.TransactionEnvelope if err := xdr.SafeUnmarshalBase64(request.Transaction, &txEnvelope); err != nil { logger.WithError(err).WithField("request", request). Info("could not unmarshal simulate transaction envelope") - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: "Could not unmarshal transaction", } } if len(txEnvelope.Operations()) != 1 { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: "Transaction contains more than one operation", } } @@ -257,21 +159,21 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge case xdr.OperationTypeInvokeHostFunction: case xdr.OperationTypeExtendFootprintTtl, xdr.OperationTypeRestoreFootprint: if txEnvelope.Type != xdr.EnvelopeTypeEnvelopeTypeTx && txEnvelope.V1.Tx.Ext.V != 1 { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: "To perform a SimulateTransaction for ExtendFootprintTtl or RestoreFootprint operations," + " SorobanTransactionData must be provided", } } footprint = txEnvelope.V1.Tx.Ext.SorobanData.Resources.Footprint default: - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: "Transaction contains unsupported operation type: " + op.Body.Type.String(), } } readTx, err := ledgerEntryReader.NewTx(ctx, true) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: "Cannot create read transaction", } } @@ -280,18 +182,18 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge }() latestLedger, err := readTx.GetLatestLedgerSequence() if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), } } bucketListSize, protocolVersion, err := getBucketListSizeAndProtocolVersion(ctx, ledgerReader, latestLedger) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), } } - resourceConfig := preflight.DefaultResourceConfig() + resourceConfig := protocol.DefaultResourceConfig() if request.ResourceConfig != nil { resourceConfig = *request.ResourceConfig } @@ -306,19 +208,19 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge } result, err := getter.GetPreflight(ctx, params) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } } - var results []SimulateHostFunctionResult + var results []protocol.SimulateHostFunctionResult if len(result.Result) != 0 { switch request.Format { - case FormatJSON: + case protocol.FormatJSON: rvJs, err := xdr2json.ConvertBytes(xdr.ScVal{}, result.Result) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } @@ -326,13 +228,13 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge auths, err := jsonifySlice(xdr.SorobanAuthorizationEntry{}, result.Auth) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } } - results = append(results, SimulateHostFunctionResult{ + results = append(results, protocol.SimulateHostFunctionResult{ ReturnValueJSON: rvJs, AuthJSON: auths, }) @@ -340,51 +242,52 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge default: rv := base64.StdEncoding.EncodeToString(result.Result) auth := base64EncodeSlice(result.Auth) - results = append(results, SimulateHostFunctionResult{ + results = append(results, protocol.SimulateHostFunctionResult{ ReturnValueXDR: &rv, AuthXDR: &auth, }) } } - var restorePreamble *RestorePreamble = nil + var restorePreamble *protocol.RestorePreamble if len(result.PreRestoreTransactionData) != 0 { switch request.Format { - case FormatJSON: + case protocol.FormatJSON: txDataJs, err := xdr2json.ConvertBytes( xdr.SorobanTransactionData{}, result.PreRestoreTransactionData) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } } - restorePreamble = &RestorePreamble{ + restorePreamble = &protocol.RestorePreamble{ TransactionDataJSON: txDataJs, MinResourceFee: result.PreRestoreMinFee, } default: - restorePreamble = &RestorePreamble{ + restorePreamble = &protocol.RestorePreamble{ TransactionDataXDR: base64.StdEncoding.EncodeToString(result.PreRestoreTransactionData), MinResourceFee: result.PreRestoreMinFee, } } } - stateChanges := make([]LedgerEntryChange, len(result.LedgerEntryDiff)) + stateChanges := make([]protocol.LedgerEntryChange, len(result.LedgerEntryDiff)) for i := range stateChanges { - if err := stateChanges[i].FromXDRDiff(result.LedgerEntryDiff[i], request.Format); err != nil { - return SimulateTransactionResponse{ + var err error + if stateChanges[i], err = LedgerEntryChangeFromXDRDiff(result.LedgerEntryDiff[i], request.Format); err != nil { + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } } } - simResp := SimulateTransactionResponse{ + simResp := protocol.SimulateTransactionResponse{ Error: result.Error, Results: results, MinResourceFee: result.MinFee, @@ -394,12 +297,12 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge } switch request.Format { - case FormatJSON: + case protocol.FormatJSON: simResp.TransactionDataJSON, err = xdr2json.ConvertBytes( xdr.SorobanTransactionData{}, result.TransactionData) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } @@ -407,7 +310,7 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge simResp.EventsJSON, err = jsonifySlice(xdr.DiagnosticEvent{}, result.Events) if err != nil { - return SimulateTransactionResponse{ + return protocol.SimulateTransactionResponse{ Error: err.Error(), LatestLedger: latestLedger, } diff --git a/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go b/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go index bb70ebda..d28f1ddd 100644 --- a/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/preflight" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/xdr2json" + "github.com/stellar/stellar-rpc/protocol" ) func TestLedgerEntryChange(t *testing.T) { @@ -44,7 +45,7 @@ func TestLedgerEntryChange(t *testing.T) { for _, test := range []struct { name string input preflight.XDRDiff - expectedOutput LedgerEntryChange + expectedOutput protocol.LedgerEntryChange }{ { name: "creation", @@ -52,8 +53,8 @@ func TestLedgerEntryChange(t *testing.T) { Before: nil, After: entryXDR, }, - expectedOutput: LedgerEntryChange{ - Type: LedgerEntryChangeTypeCreated, + expectedOutput: protocol.LedgerEntryChange{ + Type: protocol.LedgerEntryChangeTypeCreated, KeyXDR: keyB64, BeforeXDR: nil, AfterXDR: &entryB64, @@ -65,8 +66,8 @@ func TestLedgerEntryChange(t *testing.T) { Before: entryXDR, After: nil, }, - expectedOutput: LedgerEntryChange{ - Type: LedgerEntryChangeTypeDeleted, + expectedOutput: protocol.LedgerEntryChange{ + Type: protocol.LedgerEntryChangeTypeDeleted, KeyXDR: keyB64, BeforeXDR: &entryB64, AfterXDR: nil, @@ -78,28 +79,29 @@ func TestLedgerEntryChange(t *testing.T) { Before: entryXDR, After: entryXDR, }, - expectedOutput: LedgerEntryChange{ - Type: LedgerEntryChangeTypeUpdated, + expectedOutput: protocol.LedgerEntryChange{ + Type: protocol.LedgerEntryChangeTypeUpdated, KeyXDR: keyB64, BeforeXDR: &entryB64, AfterXDR: &entryB64, }, }, } { - var change LedgerEntryChange - require.NoError(t, change.FromXDRDiff(test.input, ""), test.name) + var change protocol.LedgerEntryChange + change, err := LedgerEntryChangeFromXDRDiff(test.input, "") + require.NoError(t, err, test.name) require.Equal(t, test.expectedOutput, change) // test json roundtrip changeJSON, err := json.Marshal(change) require.NoError(t, err, test.name) - var change2 LedgerEntryChange + var change2 protocol.LedgerEntryChange require.NoError(t, json.Unmarshal(changeJSON, &change2)) require.Equal(t, change, change2, test.name) // test JSON output - var changeJs LedgerEntryChange - require.NoError(t, changeJs.FromXDRDiff(test.input, FormatJSON), test.name) + changeJs, err := LedgerEntryChangeFromXDRDiff(test.input, protocol.FormatJSON) + require.NoError(t, err, test.name) require.Equal(t, keyJs, changeJs.KeyJSON) if changeJs.AfterJSON != nil { diff --git a/cmd/stellar-rpc/internal/preflight/preflight.go b/cmd/stellar-rpc/internal/preflight/preflight.go index 754a8184..d4e68dfd 100644 --- a/cmd/stellar-rpc/internal/preflight/preflight.go +++ b/cmd/stellar-rpc/internal/preflight/preflight.go @@ -28,6 +28,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/stellar-rpc/cmd/stellar-rpc/internal/db" + "github.com/stellar/stellar-rpc/protocol" ) type snapshotSourceHandle struct { @@ -36,7 +37,7 @@ type snapshotSourceHandle struct { } const ( - defaultInstructionLeeway uint64 = 0 + // Current base reserve is 0.5XLM (in stroops) defaultBaseReserve = 5_000_000 ) @@ -80,23 +81,13 @@ func FreeGoXDR(xdr C.xdr_t) { C.free(unsafe.Pointer(xdr.xdr)) } -type ResourceConfig struct { - InstructionLeeway uint64 `json:"instructionLeeway"` -} - -func DefaultResourceConfig() ResourceConfig { - return ResourceConfig{ - InstructionLeeway: defaultInstructionLeeway, - } -} - type GetterParameters struct { LedgerEntryReadTx db.LedgerEntryReadTx BucketListSize uint64 SourceAccount xdr.AccountId OperationBody xdr.OperationBody Footprint xdr.LedgerFootprint - ResourceConfig ResourceConfig + ResourceConfig protocol.ResourceConfig ProtocolVersion uint32 } @@ -108,7 +99,7 @@ type Parameters struct { NetworkPassphrase string LedgerEntryReadTx db.LedgerEntryReadTx BucketListSize uint64 - ResourceConfig ResourceConfig + ResourceConfig protocol.ResourceConfig EnableDebug bool ProtocolVersion uint32 } diff --git a/cmd/stellar-rpc/internal/db/cursor.go b/protocol/cursor.go similarity index 99% rename from cmd/stellar-rpc/internal/db/cursor.go rename to protocol/cursor.go index 6cdc1a8f..7e0791d2 100644 --- a/cmd/stellar-rpc/internal/db/cursor.go +++ b/protocol/cursor.go @@ -1,4 +1,4 @@ -package db +package protocol import ( "encoding/json" diff --git a/cmd/stellar-rpc/internal/db/cursor_test.go b/protocol/cursor_test.go similarity index 99% rename from cmd/stellar-rpc/internal/db/cursor_test.go rename to protocol/cursor_test.go index 9fcfcd74..fb2b2dfd 100644 --- a/cmd/stellar-rpc/internal/db/cursor_test.go +++ b/protocol/cursor_test.go @@ -1,4 +1,4 @@ -package db +package protocol import ( "encoding/json" diff --git a/protocol/format.go b/protocol/format.go new file mode 100644 index 00000000..5517dc9a --- /dev/null +++ b/protocol/format.go @@ -0,0 +1,26 @@ +package protocol + +import ( + "fmt" + "strings" +) + +const ( + FormatBase64 = "base64" + FormatJSON = "json" +) + +var errInvalidFormat = fmt.Errorf( + "expected %s for optional 'xdrFormat'", + strings.Join([]string{FormatBase64, FormatJSON}, ", ")) + +func IsValidFormat(format string) error { + switch format { + case "": + case FormatJSON: + case FormatBase64: + default: + return fmt.Errorf("got '%s': %w", format, errInvalidFormat) + } + return nil +} diff --git a/protocol/get_events.go b/protocol/get_events.go new file mode 100644 index 00000000..f2670847 --- /dev/null +++ b/protocol/get_events.go @@ -0,0 +1,337 @@ +package protocol + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +const ( + GetEventsMethodName = "getEvents" + MaxFiltersLimit = 5 + MaxTopicsLimit = 5 + MaxContractIDsLimit = 5 + MinTopicCount = 1 + MaxTopicCount = 4 +) + +type EventInfo struct { + EventType string `json:"type"` + Ledger int32 `json:"ledger"` + LedgerClosedAt string `json:"ledgerClosedAt"` + ContractID string `json:"contractId"` + ID string `json:"id"` + + // Deprecated: PagingToken field is deprecated, please use Cursor at top level for pagination + PagingToken string `json:"pagingToken"` + InSuccessfulContractCall bool `json:"inSuccessfulContractCall"` + TransactionHash string `json:"txHash"` + + // TopicXDR is a base64-encoded list of ScVals + TopicXDR []string `json:"topic,omitempty"` + TopicJSON []json.RawMessage `json:"topicJson,omitempty"` + + // ValueXDR is a base64-encoded ScVal + ValueXDR string `json:"value,omitempty"` + ValueJSON json.RawMessage `json:"valueJson,omitempty"` +} + +const ( + EventTypeSystem = "system" + EventTypeContract = "contract" + EventTypeDiagnostic = "diagnostic" +) + +func GetEventTypeFromEventTypeXDR() map[xdr.ContractEventType]string { + return map[xdr.ContractEventType]string{ + xdr.ContractEventTypeSystem: EventTypeSystem, + xdr.ContractEventTypeContract: EventTypeContract, + xdr.ContractEventTypeDiagnostic: EventTypeDiagnostic, + } +} + +func GetEventTypeXDRFromEventType() map[string]xdr.ContractEventType { + return map[string]xdr.ContractEventType{ + EventTypeSystem: xdr.ContractEventTypeSystem, + EventTypeContract: xdr.ContractEventTypeContract, + EventTypeDiagnostic: xdr.ContractEventTypeDiagnostic, + } +} + +func (e *EventFilter) Valid() error { + if err := e.EventType.valid(); err != nil { + return fmt.Errorf("filter type invalid: %w", err) + } + if len(e.ContractIDs) > MaxContractIDsLimit { + return errors.New("maximum 5 contract IDs per filter") + } + if len(e.Topics) > MaxTopicsLimit { + return errors.New("maximum 5 topics per filter") + } + for i, id := range e.ContractIDs { + _, err := strkey.Decode(strkey.VersionByteContract, id) + if err != nil { + return fmt.Errorf("contract ID %d invalid", i+1) + } + } + for i, topic := range e.Topics { + if err := topic.Valid(); err != nil { + return fmt.Errorf("topic %d invalid: %w", i+1, err) + } + } + return nil +} + +type EventTypeSet map[string]interface{} //nolint:recvcheck + +func (e EventTypeSet) valid() error { + for key := range e { + switch key { + case EventTypeSystem, EventTypeContract, EventTypeDiagnostic: + // ok + default: + return errors.New("if set, type must be either 'system', 'contract' or 'diagnostic'") + } + } + return nil +} + +func (e *EventTypeSet) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + *e = map[string]interface{}{} + return nil + } + var joined string + if err := json.Unmarshal(data, &joined); err != nil { + return err + } + *e = map[string]interface{}{} + if len(joined) == 0 { + return nil + } + for _, key := range strings.Split(joined, ",") { + (*e)[key] = nil + } + return nil +} + +func (e EventTypeSet) MarshalJSON() ([]byte, error) { + keys := make([]string, 0, len(e)) + for key := range e { + keys = append(keys, key) + } + return json.Marshal(strings.Join(keys, ",")) +} + +func (e EventTypeSet) Keys() []string { + keys := make([]string, 0, len(e)) + for key := range e { + keys = append(keys, key) + } + return keys +} + +func (e EventTypeSet) matches(event xdr.ContractEvent) bool { + if len(e) == 0 { + return true + } + _, ok := e[GetEventTypeFromEventTypeXDR()[event.Type]] + return ok +} + +type EventFilter struct { + EventType EventTypeSet `json:"type,omitempty"` + ContractIDs []string `json:"contractIds,omitempty"` + Topics []TopicFilter `json:"topics,omitempty"` +} + +type GetEventsRequest struct { + StartLedger uint32 `json:"startLedger,omitempty"` + EndLedger uint32 `json:"endLedger,omitempty"` + Filters []EventFilter `json:"filters"` + Pagination *PaginationOptions `json:"pagination,omitempty"` + Format string `json:"xdrFormat,omitempty"` +} + +func (g *GetEventsRequest) Valid(maxLimit uint) error { + if err := IsValidFormat(g.Format); err != nil { + return err + } + + // Validate the paging limit (if it exists) + if g.Pagination != nil && g.Pagination.Cursor != nil { + if g.StartLedger != 0 || g.EndLedger != 0 { + return errors.New("ledger ranges and cursor cannot both be set") + } + } else if g.StartLedger <= 0 { + return errors.New("startLedger must be positive") + } + + if g.Pagination != nil && g.Pagination.Limit > maxLimit { + return fmt.Errorf("limit must not exceed %d", maxLimit) + } + + // Validate filters + if len(g.Filters) > MaxFiltersLimit { + return errors.New("maximum 5 filters per request") + } + for i, filter := range g.Filters { + if err := filter.Valid(); err != nil { + return fmt.Errorf("filter %d invalid: %w", i+1, err) + } + } + + return nil +} + +func (g *GetEventsRequest) Matches(event xdr.DiagnosticEvent) bool { + if len(g.Filters) == 0 { + return true + } + for _, filter := range g.Filters { + if filter.Matches(event) { + return true + } + } + return false +} + +func (e *EventFilter) Matches(event xdr.DiagnosticEvent) bool { + return e.EventType.matches(event.Event) && e.matchesContractIDs(event.Event) && e.matchesTopics(event.Event) +} + +func (e *EventFilter) matchesContractIDs(event xdr.ContractEvent) bool { + if len(e.ContractIDs) == 0 { + return true + } + if event.ContractId == nil { + return false + } + needle := strkey.MustEncode(strkey.VersionByteContract, (*event.ContractId)[:]) + for _, id := range e.ContractIDs { + if id == needle { + return true + } + } + return false +} + +func (e *EventFilter) matchesTopics(event xdr.ContractEvent) bool { + if len(e.Topics) == 0 { + return true + } + v0, ok := event.Body.GetV0() + if !ok { + return false + } + for _, topicFilter := range e.Topics { + if topicFilter.Matches(v0.Topics) { + return true + } + } + return false +} + +type TopicFilter []SegmentFilter + +func (t TopicFilter) Valid() error { + if len(t) < MinTopicCount { + return errors.New("topic must have at least one segment") + } + if len(t) > MaxTopicCount { + return errors.New("topic cannot have more than 4 segments") + } + for i, segment := range t { + if err := segment.Valid(); err != nil { + return fmt.Errorf("segment %d invalid: %w", i+1, err) + } + } + return nil +} + +// An event matches a topic filter iff: +// - the event has EXACTLY as many topic segments as the filter AND +// - each segment either: matches exactly OR is a wildcard. +func (t TopicFilter) Matches(event []xdr.ScVal) bool { + if len(event) != len(t) { + return false + } + + for i, segmentFilter := range t { + if !segmentFilter.Matches(event[i]) { + return false + } + } + + return true +} + +type SegmentFilter struct { + Wildcard *string + ScVal *xdr.ScVal +} + +func (s *SegmentFilter) Matches(segment xdr.ScVal) bool { + switch { + case s.Wildcard != nil && *s.Wildcard == "*": + return true + case s.ScVal != nil: + if !s.ScVal.Equals(segment) { + return false + } + default: + panic("invalid segmentFilter") + } + + return true +} + +func (s *SegmentFilter) Valid() error { + if s.Wildcard != nil && s.ScVal != nil { + return errors.New("cannot set both wildcard and scval") + } + if s.Wildcard == nil && s.ScVal == nil { + return errors.New("must set either wildcard or scval") + } + if s.Wildcard != nil && *s.Wildcard != "*" { + return errors.New("wildcard must be '*'") + } + return nil +} + +func (s *SegmentFilter) UnmarshalJSON(p []byte) error { + s.Wildcard = nil + s.ScVal = nil + + var tmp string + if err := json.Unmarshal(p, &tmp); err != nil { + return err + } + if tmp == "*" { + s.Wildcard = &tmp + } else { + var out xdr.ScVal + if err := xdr.SafeUnmarshalBase64(tmp, &out); err != nil { + return err + } + s.ScVal = &out + } + return nil +} + +type PaginationOptions struct { + Cursor *Cursor `json:"cursor,omitempty"` + Limit uint `json:"limit,omitempty"` +} + +type GetEventsResponse struct { + Events []EventInfo `json:"events"` + LatestLedger uint32 `json:"latestLedger"` + // Cursor represents last populated event ID if total events reach the limit + // or end of the search window + Cursor string `json:"cursor"` +} diff --git a/protocol/get_events_test.go b/protocol/get_events_test.go new file mode 100644 index 00000000..ece8abb3 --- /dev/null +++ b/protocol/get_events_test.go @@ -0,0 +1,516 @@ +package protocol + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/xdr" +) + +func TestEventTypeSetMatches(t *testing.T) { + var defaultSet EventTypeSet + all := EventTypeSet{} + all[EventTypeContract] = nil + all[EventTypeDiagnostic] = nil + all[EventTypeSystem] = nil + + onlyContract := EventTypeSet{} + onlyContract[EventTypeContract] = nil + + contractEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeContract} + diagnosticEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeDiagnostic} + systemEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeSystem} + + for _, testCase := range []struct { + name string + set EventTypeSet + event xdr.ContractEvent + matches bool + }{ + { + "all matches Contract events", + all, + contractEvent, + true, + }, + { + "all matches System events", + all, + systemEvent, + true, + }, + { + "all matches Diagnostic events", + all, + systemEvent, + true, + }, + { + "defaultSet matches Contract events", + defaultSet, + contractEvent, + true, + }, + { + "defaultSet matches System events", + defaultSet, + systemEvent, + true, + }, + { + "defaultSet matches Diagnostic events", + defaultSet, + systemEvent, + true, + }, + { + "onlyContract set matches Contract events", + onlyContract, + contractEvent, + true, + }, + { + "onlyContract does not match System events", + onlyContract, + systemEvent, + false, + }, + { + "onlyContract does not match Diagnostic events", + defaultSet, + diagnosticEvent, + true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.matches, testCase.set.matches(testCase.event)) + }) + } +} + +func TestEventTypeSetValid(t *testing.T) { + for _, testCase := range []struct { + name string + keys []string + expectedError bool + }{ + { + "empty set", + []string{}, + false, + }, + { + "set with one valid element", + []string{EventTypeSystem}, + false, + }, + { + "set with two valid elements", + []string{EventTypeSystem, EventTypeContract}, + false, + }, + { + "set with three valid elements", + []string{EventTypeSystem, EventTypeContract, EventTypeDiagnostic}, + false, + }, + { + "set with one invalid element", + []string{"abc"}, + true, + }, + { + "set with multiple invalid elements", + []string{"abc", "def"}, + true, + }, + { + "set with valid elements mixed with invalid elements", + []string{EventTypeSystem, "abc"}, + true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + set := EventTypeSet{} + for _, key := range testCase.keys { + set[key] = nil + } + if testCase.expectedError { + assert.Error(t, set.valid()) + } else { + require.NoError(t, set.valid()) + } + }) + } +} + +func TestEventTypeSetMarshaling(t *testing.T) { + for _, testCase := range []struct { + name string + input string + expected []string + }{ + { + "empty set", + "", + []string{}, + }, + { + "set with one element", + "a", + []string{"a"}, + }, + { + "set with more than one element", + "a,b,c", + []string{"a", "b", "c"}, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + var set EventTypeSet + input, err := json.Marshal(testCase.input) + require.NoError(t, err) + err = set.UnmarshalJSON(input) + require.NoError(t, err) + assert.Equal(t, len(testCase.expected), len(set)) + for _, val := range testCase.expected { + _, ok := set[val] + assert.True(t, ok) + } + }) + } +} + +//nolint:funlen +func TestTopicFilterMatches(t *testing.T) { + transferSym := xdr.ScSymbol("transfer") + transfer := xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &transferSym, + } + sixtyfour := xdr.Uint64(64) + number := xdr.ScVal{ + Type: xdr.ScValTypeScvU64, + U64: &sixtyfour, + } + star := "*" + for _, tc := range []struct { + name string + filter TopicFilter + includes []xdr.ScVec + excludes []xdr.ScVec + }{ + { + name: "", + filter: nil, + includes: []xdr.ScVec{ + {}, + }, + excludes: []xdr.ScVec{ + {transfer}, + }, + }, + + // Exact matching + { + name: "ScSymbol(transfer)", + filter: []SegmentFilter{ + {ScVal: &transfer}, + }, + includes: []xdr.ScVec{ + {transfer}, + }, + excludes: []xdr.ScVec{ + {number}, + {transfer, transfer}, + }, + }, + + // Star + { + name: "*", + filter: []SegmentFilter{ + {Wildcard: &star}, + }, + includes: []xdr.ScVec{ + {transfer}, + }, + excludes: []xdr.ScVec{ + {transfer, transfer}, + }, + }, + { + name: "*/transfer", + filter: []SegmentFilter{ + {Wildcard: &star}, + {ScVal: &transfer}, + }, + includes: []xdr.ScVec{ + {number, transfer}, + {transfer, transfer}, + }, + excludes: []xdr.ScVec{ + {number}, + {number, number}, + {number, transfer, number}, + {transfer}, + {transfer, number}, + {transfer, transfer, transfer}, + }, + }, + { + name: "transfer/*", + filter: []SegmentFilter{ + {ScVal: &transfer}, + {Wildcard: &star}, + }, + includes: []xdr.ScVec{ + {transfer, number}, + {transfer, transfer}, + }, + excludes: []xdr.ScVec{ + {number}, + {number, number}, + {number, transfer, number}, + {transfer}, + {number, transfer}, + {transfer, transfer, transfer}, + }, + }, + { + name: "transfer/*/*", + filter: []SegmentFilter{ + {ScVal: &transfer}, + {Wildcard: &star}, + {Wildcard: &star}, + }, + includes: []xdr.ScVec{ + {transfer, number, number}, + {transfer, transfer, transfer}, + }, + excludes: []xdr.ScVec{ + {number}, + {number, number}, + {number, transfer}, + {number, transfer, number, number}, + {transfer}, + {transfer, transfer, transfer, transfer}, + }, + }, + { + name: "transfer/*/number", + filter: []SegmentFilter{ + {ScVal: &transfer}, + {Wildcard: &star}, + {ScVal: &number}, + }, + includes: []xdr.ScVec{ + {transfer, number, number}, + {transfer, transfer, number}, + }, + excludes: []xdr.ScVec{ + {number}, + {number, number}, + {number, number, number}, + {number, transfer, number}, + {transfer}, + {number, transfer}, + {transfer, transfer, transfer}, + {transfer, number, transfer}, + }, + }, + } { + name := tc.name + if name == "" { + name = topicFilterToString(tc.filter) + } + t.Run(name, func(t *testing.T) { + for _, include := range tc.includes { + assert.True( + t, + tc.filter.Matches(include), + "Expected %v filter to include %v", + name, + include, + ) + } + for _, exclude := range tc.excludes { + assert.False( + t, + tc.filter.Matches(exclude), + "Expected %v filter to exclude %v", + name, + exclude, + ) + } + }) + } +} + +func TestTopicFilterJSON(t *testing.T) { + var got TopicFilter + + require.NoError(t, json.Unmarshal([]byte("[]"), &got)) + assert.Equal(t, TopicFilter{}, got) + + star := "*" + require.NoError(t, json.Unmarshal([]byte("[\"*\"]"), &got)) + assert.Equal(t, TopicFilter{{Wildcard: &star}}, got) + + sixtyfour := xdr.Uint64(64) + scval := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &sixtyfour} + scvalstr, err := xdr.MarshalBase64(scval) + require.NoError(t, err) + require.NoError(t, json.Unmarshal([]byte(fmt.Sprintf("[%q]", scvalstr)), &got)) + assert.Equal(t, TopicFilter{{ScVal: &scval}}, got) +} + +func topicFilterToString(t TopicFilter) string { + var s []string + for _, segment := range t { + switch { + case segment.Wildcard != nil: + s = append(s, *segment.Wildcard) + case segment.ScVal != nil: + out, err := xdr.MarshalBase64(*segment.ScVal) + if err != nil { + panic(err) + } + s = append(s, out) + default: + panic("Invalid topic filter") + } + } + if len(s) == 0 { + s = append(s, "") + } + return strings.Join(s, "/") +} + +//nolint:funlen +func TestGetEventsRequestValid(t *testing.T) { + // omit startLedger but include cursor + var request GetEventsRequest + require.NoError(t, json.Unmarshal( + []byte("{ \"filters\": [], \"pagination\": { \"cursor\": \"0000000021474840576-0000000000\"} }"), + &request, + )) + assert.Equal(t, uint32(0), request.StartLedger) + require.NoError(t, request.Valid(1000)) + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: &PaginationOptions{Cursor: &Cursor{}}, + }).Valid(1000), "ledger ranges and cursor cannot both be set") + + require.NoError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: nil, + }).Valid(1000)) + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{}, + Pagination: &PaginationOptions{Limit: 1001}, + }).Valid(1000), "limit must not exceed 1000") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 0, + Filters: []EventFilter{}, + Pagination: nil, + }).Valid(1000), "startLedger must be positive") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {}, {}, {}, {}, {}, {}, + }, + Pagination: nil, + }).Valid(1000), "maximum 5 filters per request") + + err := (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {EventType: map[string]interface{}{"foo": nil}}, + }, + Pagination: nil, + }).Valid(1000) + expectedErrStr := "filter 1 invalid: filter type invalid: if set, type must be either 'system', 'contract' or 'diagnostic'" //nolint:lll + require.EqualError(t, err, expectedErrStr) + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {ContractIDs: []string{ + "CCVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKUD2U", + "CC53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53WQD5", + "CDGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZLND", + "CDO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53YUK", + "CDXO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO4M7R", + "CD7777777777777777777777777777777777777777777777777767GY", + }}, + }, + Pagination: nil, + }).Valid(1000), "filter 1 invalid: maximum 5 contract IDs per filter") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {ContractIDs: []string{"a"}}, + }, + Pagination: nil, + }).Valid(1000), "filter 1 invalid: contract ID 1 invalid") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {ContractIDs: []string{"CCVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVINVALID"}}, + }, + Pagination: nil, + }).Valid(1000), "filter 1 invalid: contract ID 1 invalid") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + { + Topics: []TopicFilter{ + {}, {}, {}, {}, {}, {}, + }, + }, + }, + Pagination: nil, + }).Valid(1000), "filter 1 invalid: maximum 5 topics per filter") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {Topics: []TopicFilter{ + {}, + }}, + }, + Pagination: nil, + }).Valid(1000), "filter 1 invalid: topic 1 invalid: topic must have at least one segment") + + require.EqualError(t, (&GetEventsRequest{ + StartLedger: 1, + Filters: []EventFilter{ + {Topics: []TopicFilter{ + { + {}, + {}, + {}, + {}, + {}, + }, + }}, + }, + Pagination: nil, + }).Valid(1000), "filter 1 invalid: topic 1 invalid: topic cannot have more than 4 segments") +} diff --git a/protocol/get_fee_stats.go b/protocol/get_fee_stats.go new file mode 100644 index 00000000..d616392c --- /dev/null +++ b/protocol/get_fee_stats.go @@ -0,0 +1,28 @@ +package protocol + +const GetFeeStatsMethodName = "getFeeStats" + +type FeeDistribution struct { + Max uint64 `json:"max,string"` + Min uint64 `json:"min,string"` + Mode uint64 `json:"mode,string"` + P10 uint64 `json:"p10,string"` + P20 uint64 `json:"p20,string"` + P30 uint64 `json:"p30,string"` + P40 uint64 `json:"p40,string"` + P50 uint64 `json:"p50,string"` + P60 uint64 `json:"p60,string"` + P70 uint64 `json:"p70,string"` + P80 uint64 `json:"p80,string"` + P90 uint64 `json:"p90,string"` + P95 uint64 `json:"p95,string"` + P99 uint64 `json:"p99,string"` + TransactionCount uint32 `json:"transactionCount,string"` + LedgerCount uint32 `json:"ledgerCount"` +} + +type GetFeeStatsResponse struct { + SorobanInclusionFee FeeDistribution `json:"sorobanInclusionFee"` + InclusionFee FeeDistribution `json:"inclusionFee"` + LatestLedger uint32 `json:"latestLedger"` +} diff --git a/protocol/get_health.go b/protocol/get_health.go new file mode 100644 index 00000000..b8826502 --- /dev/null +++ b/protocol/get_health.go @@ -0,0 +1,10 @@ +package protocol + +const GetHealthMethodName = "getHealth" + +type GetHealthResponse struct { + Status string `json:"status"` + LatestLedger uint32 `json:"latestLedger"` + OldestLedger uint32 `json:"oldestLedger"` + LedgerRetentionWindow uint32 `json:"ledgerRetentionWindow"` +} diff --git a/protocol/get_latest_ledger.go b/protocol/get_latest_ledger.go new file mode 100644 index 00000000..af03c99e --- /dev/null +++ b/protocol/get_latest_ledger.go @@ -0,0 +1,12 @@ +package protocol + +const GetLatestLedgerMethodName = "getLatestLedger" + +type GetLatestLedgerResponse struct { + // Hash of the latest ledger as a hex-encoded string + Hash string `json:"id"` + // Stellar Core protocol version associated with the ledger. + ProtocolVersion uint32 `json:"protocolVersion"` + // Sequence number of the latest ledger. + Sequence uint32 `json:"sequence"` +} diff --git a/protocol/get_ledger_entries.go b/protocol/get_ledger_entries.go new file mode 100644 index 00000000..76258891 --- /dev/null +++ b/protocol/get_ledger_entries.go @@ -0,0 +1,30 @@ +package protocol + +import "encoding/json" + +const GetLedgerEntriesMethodName = "getLedgerEntries" + +type GetLedgerEntriesRequest struct { + Keys []string `json:"keys"` + Format string `json:"xdrFormat,omitempty"` +} + +type LedgerEntryResult struct { + // Original request key matching this LedgerEntryResult. + KeyXDR string `json:"key,omitempty"` + KeyJSON json.RawMessage `json:"keyJson,omitempty"` + // Ledger entry data encoded in base 64. + DataXDR string `json:"xdr,omitempty"` + DataJSON json.RawMessage `json:"dataJson,omitempty"` + // Last modified ledger for this entry. + LastModifiedLedger uint32 `json:"lastModifiedLedgerSeq"` + // The ledger sequence until the entry is live, available for entries that have associated ttl ledger entries. + LiveUntilLedgerSeq *uint32 `json:"liveUntilLedgerSeq,omitempty"` +} + +type GetLedgerEntriesResponse struct { + // All found ledger entries. + Entries []LedgerEntryResult `json:"entries"` + // Sequence number of the latest ledger at time of request. + LatestLedger uint32 `json:"latestLedger"` +} diff --git a/protocol/get_ledgers.go b/protocol/get_ledgers.go new file mode 100644 index 00000000..1545bec2 --- /dev/null +++ b/protocol/get_ledgers.go @@ -0,0 +1,76 @@ +package protocol + +import ( + "encoding/json" + "errors" + "fmt" +) + +const GetLedgersMethodName = "getLedgers" + +type LedgerPaginationOptions struct { + Cursor string `json:"cursor,omitempty"` + Limit uint `json:"limit,omitempty"` +} + +type LedgerSeqRange struct { + FirstLedger uint32 + LastLedger uint32 +} + +// isStartLedgerWithinBounds checks whether the request start ledger/cursor is within the max/min ledger +// for the current RPC instance. +func IsStartLedgerWithinBounds(startLedger uint32, ledgerRange LedgerSeqRange) bool { + return startLedger >= ledgerRange.FirstLedger && startLedger <= ledgerRange.LastLedger +} + +// GetLedgersRequest represents the request parameters for fetching ledgers. +type GetLedgersRequest struct { + StartLedger uint32 `json:"startLedger"` + Pagination *LedgerPaginationOptions `json:"pagination,omitempty"` + Format string `json:"xdrFormat,omitempty"` +} + +// validate checks the validity of the request parameters. +func (req *GetLedgersRequest) Validate(maxLimit uint, ledgerRange LedgerSeqRange) error { + switch { + case req.Pagination != nil: + switch { + case req.Pagination.Cursor != "" && req.StartLedger != 0: + return errors.New("startLedger and cursor cannot both be set") + case req.Pagination.Limit > maxLimit: + return fmt.Errorf("limit must not exceed %d", maxLimit) + } + case req.StartLedger != 0 && !IsStartLedgerWithinBounds(req.StartLedger, ledgerRange): + return fmt.Errorf( + "start ledger must be between the oldest ledger: %d and the latest ledger: %d for this rpc instance", + ledgerRange.FirstLedger, + ledgerRange.LastLedger, + ) + } + + return IsValidFormat(req.Format) +} + +// LedgerInfo represents a single ledger in the response. +type LedgerInfo struct { + Hash string `json:"hash"` + Sequence uint32 `json:"sequence"` + LedgerCloseTime int64 `json:"ledgerCloseTime,string"` + + LedgerHeader string `json:"headerXdr"` + LedgerHeaderJSON json.RawMessage `json:"headerJson,omitempty"` + + LedgerMetadata string `json:"metadataXdr"` + LedgerMetadataJSON json.RawMessage `json:"metadataJson,omitempty"` +} + +// GetLedgersResponse encapsulates the response structure for getLedgers queries. +type GetLedgersResponse struct { + Ledgers []LedgerInfo `json:"ledgers"` + LatestLedger uint32 `json:"latestLedger"` + LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime"` + OldestLedger uint32 `json:"oldestLedger"` + OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTime"` + Cursor string `json:"cursor"` +} diff --git a/protocol/get_network.go b/protocol/get_network.go new file mode 100644 index 00000000..aef1e2e9 --- /dev/null +++ b/protocol/get_network.go @@ -0,0 +1,11 @@ +package protocol + +const GetNetworkMethodName = "getNetwork" + +type GetNetworkRequest struct{} + +type GetNetworkResponse struct { + FriendbotURL string `json:"friendbotUrl,omitempty"` + Passphrase string `json:"passphrase"` + ProtocolVersion int `json:"protocolVersion"` +} diff --git a/protocol/get_transaction.go b/protocol/get_transaction.go new file mode 100644 index 00000000..69d1e910 --- /dev/null +++ b/protocol/get_transaction.go @@ -0,0 +1,40 @@ +package protocol + +const ( + GetTransactionMethodName = "getTransaction" + // TransactionStatusSuccess indicates the transaction was included in the ledger and + // it was executed without errors. + TransactionStatusSuccess = "SUCCESS" + // TransactionStatusNotFound indicates the transaction was not found in Stellar-RPC's + // transaction store. + TransactionStatusNotFound = "NOT_FOUND" + // TransactionStatusFailed indicates the transaction was included in the ledger and + // it was executed with an error. + TransactionStatusFailed = "FAILED" +) + +// GetTransactionResponse is the response for the Stellar-RPC getTransaction() endpoint +type GetTransactionResponse struct { + // LatestLedger is the latest ledger stored in Stellar-RPC. + LatestLedger uint32 `json:"latestLedger"` + // LatestLedgerCloseTime is the unix timestamp of when the latest ledger was closed. + LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime,string"` + // LatestLedger is the oldest ledger stored in Stellar-RPC. + OldestLedger uint32 `json:"oldestLedger"` + // LatestLedgerCloseTime is the unix timestamp of when the oldest ledger was closed. + OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTime,string"` + + // Many of the fields below are only present if Status is not + // TransactionNotFound. + TransactionDetails + // LedgerCloseTime is the unix timestamp of when the transaction was + // included in the ledger. It isn't part of `TransactionInfo` because of a + // bug in which `createdAt` in getTransactions is encoded as a number + // whereas in getTransaction (singular) it's encoded as a string. + LedgerCloseTime int64 `json:"createdAt,string"` +} + +type GetTransactionRequest struct { + Hash string `json:"hash"` + Format string `json:"xdrFormat,omitempty"` +} diff --git a/protocol/get_transactions.go b/protocol/get_transactions.go new file mode 100644 index 00000000..64a2a423 --- /dev/null +++ b/protocol/get_transactions.go @@ -0,0 +1,90 @@ +package protocol + +import ( + "encoding/json" + "errors" + "fmt" +) + +const GetTransactionsMethodName = "getTransactions" + +// TransactionsPaginationOptions defines the available options for paginating through transactions. +type TransactionsPaginationOptions struct { + Cursor string `json:"cursor,omitempty"` + Limit uint `json:"limit,omitempty"` +} + +// GetTransactionsRequest represents the request parameters for fetching transactions within a range of ledgers. +type GetTransactionsRequest struct { + StartLedger uint32 `json:"startLedger"` + Pagination *TransactionsPaginationOptions `json:"pagination,omitempty"` + Format string `json:"xdrFormat,omitempty"` +} + +// isValid checks the validity of the request parameters. +func (req GetTransactionsRequest) IsValid(maxLimit uint, ledgerRange LedgerSeqRange) error { + if req.Pagination != nil && req.Pagination.Cursor != "" { + if req.StartLedger != 0 { + return errors.New("startLedger and cursor cannot both be set") + } + } else if req.StartLedger < ledgerRange.FirstLedger || req.StartLedger > ledgerRange.LastLedger { + return fmt.Errorf( + "start ledger must be between the oldest ledger: %d and the latest ledger: %d for this rpc instance", + ledgerRange.FirstLedger, + ledgerRange.LastLedger, + ) + } + + if req.Pagination != nil && req.Pagination.Limit > maxLimit { + return fmt.Errorf("limit must not exceed %d", maxLimit) + } + + return IsValidFormat(req.Format) +} + +type TransactionDetails struct { + // Status is one of: TransactionSuccess, TransactionFailed, TransactionNotFound. + Status string `json:"status"` + // TransactionHash is the hex encoded hash of the transaction. Note that for + // fee-bump transaction this will be the hash of the fee-bump transaction + // instead of the inner transaction hash. + TransactionHash string `json:"txHash"` + // ApplicationOrder is the index of the transaction among all the + // transactions for that ledger. + ApplicationOrder int32 `json:"applicationOrder"` + // FeeBump indicates whether the transaction is a feebump transaction + FeeBump bool `json:"feeBump"` + // EnvelopeXDR is the TransactionEnvelope XDR value. + EnvelopeXDR string `json:"envelopeXdr,omitempty"` + EnvelopeJSON json.RawMessage `json:"envelopeJson,omitempty"` + // ResultXDR is the TransactionResult XDR value. + ResultXDR string `json:"resultXdr,omitempty"` + ResultJSON json.RawMessage `json:"resultJson,omitempty"` + // ResultMetaXDR is the TransactionMeta XDR value. + ResultMetaXDR string `json:"resultMetaXdr,omitempty"` + ResultMetaJSON json.RawMessage `json:"resultMetaJson,omitempty"` + // DiagnosticEventsXDR is present only if transaction was not successful. + // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent + DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` + // Ledger is the sequence of the ledger which included the transaction. + Ledger uint32 `json:"ledger"` +} + +type TransactionInfo struct { + TransactionDetails + + // LedgerCloseTime is the unix timestamp of when the transaction was + // included in the ledger. + LedgerCloseTime int64 `json:"createdAt"` +} + +// GetTransactionsResponse encapsulates the response structure for getTransactions queries. +type GetTransactionsResponse struct { + Transactions []TransactionInfo `json:"transactions"` + LatestLedger uint32 `json:"latestLedger"` + LatestLedgerCloseTime int64 `json:"latestLedgerCloseTimestamp"` + OldestLedger uint32 `json:"oldestLedger"` + OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTimestamp"` + Cursor string `json:"cursor"` +} diff --git a/protocol/get_version_info.go b/protocol/get_version_info.go new file mode 100644 index 00000000..dbd76074 --- /dev/null +++ b/protocol/get_version_info.go @@ -0,0 +1,19 @@ +package protocol + +const GetVersionInfoMethodName = "getVersionInfo" + +type GetVersionInfoResponse struct { + Version string `json:"version"` + CommitHash string `json:"commitHash"` + BuildTimestamp string `json:"buildTimestamp"` + CaptiveCoreVersion string `json:"captiveCoreVersion"` + ProtocolVersion uint32 `json:"protocolVersion"` + //nolint:tagliatelle + CommitHashDeprecated string `json:"commit_hash"` + //nolint:tagliatelle + BuildTimestampDeprecated string `json:"build_time_stamp"` + //nolint:tagliatelle + CaptiveCoreVersionDeprecated string `json:"captive_core_version"` + //nolint:tagliatelle + ProtocolVersionDeprecated uint32 `json:"protocol_version"` +} diff --git a/protocol/send_transaction.go b/protocol/send_transaction.go new file mode 100644 index 00000000..795477a0 --- /dev/null +++ b/protocol/send_transaction.go @@ -0,0 +1,40 @@ +package protocol + +import "encoding/json" + +const SendTransactionMethodName = "sendTransaction" + +// SendTransactionResponse represents the transaction submission response returned Stellar-RPC +type SendTransactionResponse struct { + // ErrorResultXDR is present only if Status is equal to proto.TXStatusError. + // ErrorResultXDR is a TransactionResult xdr string which contains details on why + // the transaction could not be accepted by stellar-core. + ErrorResultXDR string `json:"errorResultXdr,omitempty"` + ErrorResultJSON json.RawMessage `json:"errorResultJson,omitempty"` + + // DiagnosticEventsXDR is present only if Status is equal to proto.TXStatusError. + // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent + DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` + + // Status represents the status of the transaction submission returned by stellar-core. + // Status can be one of: proto.TXStatusPending, proto.TXStatusDuplicate, + // proto.TXStatusTryAgainLater, or proto.TXStatusError. + Status string `json:"status"` + // Hash is a hash of the transaction which can be used to look up whether + // the transaction was included in the ledger. + Hash string `json:"hash"` + // LatestLedger is the latest ledger known to Stellar-RPC at the time it handled + // the transaction submission request. + LatestLedger uint32 `json:"latestLedger"` + // LatestLedgerCloseTime is the unix timestamp of the close time of the latest ledger known to + // Stellar-RPC at the time it handled the transaction submission request. + LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime,string"` +} + +// SendTransactionRequest is the Stellar-RPC request to submit a transaction. +type SendTransactionRequest struct { + // Transaction is the base64 encoded transaction envelope. + Transaction string `json:"transaction"` + Format string `json:"xdrFormat,omitempty"` +} diff --git a/protocol/simulate_transaction.go b/protocol/simulate_transaction.go new file mode 100644 index 00000000..bb681f2e --- /dev/null +++ b/protocol/simulate_transaction.go @@ -0,0 +1,126 @@ +package protocol + +import ( + "encoding/json" + "fmt" + "strings" +) + +const ( + SimulateTransactionMethodName = "simulateTransaction" + DefaultInstructionLeeway uint64 = 0 +) + +type SimulateTransactionRequest struct { + Transaction string `json:"transaction"` + ResourceConfig *ResourceConfig `json:"resourceConfig,omitempty"` + Format string `json:"xdrFormat,omitempty"` +} + +type ResourceConfig struct { + InstructionLeeway uint64 `json:"instructionLeeway"` +} + +func DefaultResourceConfig() ResourceConfig { + return ResourceConfig{ + InstructionLeeway: DefaultInstructionLeeway, + } +} + +// SimulateHostFunctionResult contains the simulation result of each HostFunction within the single +// InvokeHostFunctionOp allowed in a Transaction +type SimulateHostFunctionResult struct { + AuthXDR *[]string `json:"auth,omitempty"` + AuthJSON []json.RawMessage `json:"authJson,omitempty"` + + ReturnValueXDR *string `json:"xdr,omitempty"` + ReturnValueJSON json.RawMessage `json:"returnValueJson,omitempty"` +} + +type RestorePreamble struct { + // TransactionDataXDR is an xdr.SorobanTransactionData in base64 + TransactionDataXDR string `json:"transactionData,omitempty"` + TransactionDataJSON json.RawMessage `json:"transactionDataJson,omitempty"` + + MinResourceFee int64 `json:"minResourceFee,string"` +} +type LedgerEntryChangeType int //nolint:recvcheck + +const ( + LedgerEntryChangeTypeCreated LedgerEntryChangeType = iota + 1 + LedgerEntryChangeTypeUpdated + LedgerEntryChangeTypeDeleted +) + +var ( + LedgerEntryChangeTypeName = map[LedgerEntryChangeType]string{ //nolint:gochecknoglobals + LedgerEntryChangeTypeCreated: "created", + LedgerEntryChangeTypeUpdated: "updated", + LedgerEntryChangeTypeDeleted: "deleted", + } + LedgerEntryChangeTypeValue = map[string]LedgerEntryChangeType{ //nolint:gochecknoglobals + "created": LedgerEntryChangeTypeCreated, + "updated": LedgerEntryChangeTypeUpdated, + "deleted": LedgerEntryChangeTypeDeleted, + } +) + +func (l LedgerEntryChangeType) String() string { + return LedgerEntryChangeTypeName[l] +} + +func (l LedgerEntryChangeType) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +func (l *LedgerEntryChangeType) Parse(s string) error { + s = strings.TrimSpace(strings.ToLower(s)) + value, ok := LedgerEntryChangeTypeValue[s] + if !ok { + return fmt.Errorf("%q is not a valid ledger entry change type", s) + } + *l = value + return nil +} + +func (l *LedgerEntryChangeType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + return l.Parse(s) +} + +// LedgerEntryChange designates a change in a ledger entry. Before and After cannot be omitted at the same time. +// If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a deletion. +type LedgerEntryChange struct { + Type LedgerEntryChangeType `json:"type"` + + KeyXDR string `json:"key,omitempty"` // LedgerEntryKey in base64 + KeyJSON json.RawMessage `json:"keyJson,omitempty"` + + BeforeXDR *string `json:"before"` // LedgerEntry XDR in base64 + BeforeJSON json.RawMessage `json:"beforeJson,omitempty"` + + AfterXDR *string `json:"after"` // LedgerEntry XDR in base64 + AfterJSON json.RawMessage `json:"afterJson,omitempty"` +} + +type SimulateTransactionResponse struct { + Error string `json:"error,omitempty"` + + TransactionDataXDR string `json:"transactionData,omitempty"` // SorobanTransactionData XDR in base64 + TransactionDataJSON json.RawMessage `json:"transactionDataJson,omitempty"` + + EventsXDR []string `json:"events,omitempty"` // DiagnosticEvent XDR in base64 + EventsJSON []json.RawMessage `json:"eventsJson,omitempty"` + + MinResourceFee int64 `json:"minResourceFee,string,omitempty"` + // an array of the individual host function call results + Results []SimulateHostFunctionResult `json:"results,omitempty"` + // If present, it indicates that a prior RestoreFootprint is required + RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` + // If present, it indicates how the state (ledger entries) will change as a result of the transaction execution. + StateChanges []LedgerEntryChange `json:"stateChanges,omitempty"` + LatestLedger uint32 `json:"latestLedger"` +}