diff --git a/cmd/soroban-rpc/internal/config/config.go b/cmd/soroban-rpc/internal/config/config.go index 1f89ab2b..15e69f6b 100644 --- a/cmd/soroban-rpc/internal/config/config.go +++ b/cmd/soroban-rpc/internal/config/config.go @@ -28,6 +28,7 @@ type Config struct { EventLedgerRetentionWindow uint32 FriendbotURL string HistoryArchiveURLs []string + HistoryArchiveUserAgent string IngestionTimeout time.Duration LogFormat LogFormat LogLevel logrus.Level @@ -64,6 +65,13 @@ type Config struct { flagset *pflag.FlagSet } +func (cfg *Config) ExtendedUserAgent(extension string) string { + if cfg.HistoryArchiveUserAgent == "" { + return extension + } + return cfg.HistoryArchiveUserAgent + "/" + extension +} + func (cfg *Config) SetValues(lookupEnv func(string) (string, bool)) error { // We start with the defaults if err := cfg.loadDefaults(); err != nil { diff --git a/cmd/soroban-rpc/internal/config/config_test.go b/cmd/soroban-rpc/internal/config/config_test.go index 67769a3c..df006b7e 100644 --- a/cmd/soroban-rpc/internal/config/config_test.go +++ b/cmd/soroban-rpc/internal/config/config_test.go @@ -51,6 +51,14 @@ func TestConfigLoadDefaults(t *testing.T) { assert.Equal(t, uint(runtime.NumCPU()), cfg.PreflightWorkerCount) } +func TestConfigExtendedUserAgent(t *testing.T) { + cfg := Config{ + HistoryArchiveUserAgent: "Test", + } + require.NoError(t, cfg.loadDefaults()) + assert.Equal(t, "Test/123", cfg.ExtendedUserAgent("123")) +} + func TestConfigLoadFlagsDefaultValuesOverrideExisting(t *testing.T) { // Set up a config with an existing non-default value cfg := Config{ diff --git a/cmd/soroban-rpc/internal/config/options_test.go b/cmd/soroban-rpc/internal/config/options_test.go index 958306ab..00b1654f 100644 --- a/cmd/soroban-rpc/internal/config/options_test.go +++ b/cmd/soroban-rpc/internal/config/options_test.go @@ -26,7 +26,7 @@ func TestAllConfigFieldsMustHaveASingleOption(t *testing.T) { // Allow us to explicitly exclude any fields on the Config struct, which are not going to have Options. // e.g. "ConfigPath" - excluded := map[string]bool{} + excluded := map[string]bool{"HistoryArchiveUserAgent": true} cfg := Config{} cfgValue := reflect.ValueOf(cfg) diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index d2ee9c3a..7658b096 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -19,6 +19,7 @@ import ( "github.com/stellar/go/ingest/ledgerbackend" supporthttp "github.com/stellar/go/support/http" supportlog "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal" @@ -121,7 +122,7 @@ func newCaptiveCore(cfg *config.Config, logger *supportlog.Entry) (*ledgerbacken CheckpointFrequency: cfg.CheckpointFrequency, Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, - UserAgent: "captivecore", + UserAgent: cfg.ExtendedUserAgent("captivecore"), UseDB: true, } return ledgerbackend.NewCaptive(captiveConfig) @@ -144,12 +145,18 @@ func MustNew(cfg *config.Config) *Daemon { if len(cfg.HistoryArchiveURLs) == 0 { logger.Fatal("no history archives url were provided") } - historyArchive, err := historyarchive.Connect( - cfg.HistoryArchiveURLs[0], + + historyArchive, err := historyarchive.NewArchivePool( + cfg.HistoryArchiveURLs, historyarchive.ArchiveOptions{ + NetworkPassphrase: cfg.NetworkPassphrase, CheckpointFrequency: cfg.CheckpointFrequency, + ConnectOptions: storage.ConnectOptions{ + Context: context.Background(), + UserAgent: cfg.HistoryArchiveUserAgent}, }, ) + if err != nil { logger.WithError(err).Fatal("could not connect to history archive") } diff --git a/cmd/soroban-rpc/internal/events/events.go b/cmd/soroban-rpc/internal/events/events.go index e854cd77..d4352983 100644 --- a/cmd/soroban-rpc/internal/events/events.go +++ b/cmd/soroban-rpc/internal/events/events.go @@ -19,7 +19,7 @@ type event struct { diagnosticEventXDR []byte txIndex uint32 eventIndex uint32 - txHash *xdr.Hash // intentionally stored as a pointer to save memory (amortized as soon as there are two events in a transaction) + txHash *xdr.Hash // intentionally stored as a pointer to save memory (amortized as soon as there are two events in a transaction) } func (e event) cursor(ledgerSeq uint32) Cursor { diff --git a/cmd/soroban-rpc/internal/ingest/ledgerentry.go b/cmd/soroban-rpc/internal/ingest/ledgerentry.go index 0b8fc48f..1e16efe3 100644 --- a/cmd/soroban-rpc/internal/ingest/ledgerentry.go +++ b/cmd/soroban-rpc/internal/ingest/ledgerentry.go @@ -38,10 +38,10 @@ func (s *Service) ingestLedgerEntryChanges(ctx context.Context, reader ingest.Ch results := changeStatsProcessor.GetResults() for stat, value := range results.Map() { stat = strings.Replace(stat, "stats_", "change_", 1) - s.ledgerStatsMetric. + s.metrics.ledgerStatsMetric. With(prometheus.Labels{"type": stat}).Add(float64(value.(int64))) } - s.ingestionDurationMetric. + s.metrics.ingestionDurationMetric. With(prometheus.Labels{"type": "ledger_entries"}).Observe(time.Since(startTime).Seconds()) return ctx.Err() } @@ -66,10 +66,10 @@ func (s *Service) ingestTempLedgerEntryEvictions( } for evictionType, count := range counts { - s.ledgerStatsMetric. + s.metrics.ledgerStatsMetric. With(prometheus.Labels{"type": evictionType}).Add(float64(count)) } - s.ingestionDurationMetric. + s.metrics.ingestionDurationMetric. With(prometheus.Labels{"type": "evicted_temp_ledger_entries"}).Observe(time.Since(startTime).Seconds()) return ctx.Err() } diff --git a/cmd/soroban-rpc/internal/ingest/service.go b/cmd/soroban-rpc/internal/ingest/service.go index 6240f7cb..2cc082cb 100644 --- a/cmd/soroban-rpc/internal/ingest/service.go +++ b/cmd/soroban-rpc/internal/ingest/service.go @@ -73,19 +73,24 @@ func newService(cfg Config) *Service { []string{"type"}, ) - cfg.Daemon.MetricsRegistry().MustRegister(ingestionDurationMetric, latestLedgerMetric, ledgerStatsMetric) + cfg.Daemon.MetricsRegistry().MustRegister( + ingestionDurationMetric, + latestLedgerMetric, + ledgerStatsMetric) service := &Service{ - logger: cfg.Logger, - db: cfg.DB, - eventStore: cfg.EventStore, - transactionStore: cfg.TransactionStore, - ledgerBackend: cfg.LedgerBackend, - networkPassPhrase: cfg.NetworkPassPhrase, - timeout: cfg.Timeout, - ingestionDurationMetric: ingestionDurationMetric, - latestLedgerMetric: latestLedgerMetric, - ledgerStatsMetric: ledgerStatsMetric, + logger: cfg.Logger, + db: cfg.DB, + eventStore: cfg.EventStore, + transactionStore: cfg.TransactionStore, + ledgerBackend: cfg.LedgerBackend, + networkPassPhrase: cfg.NetworkPassPhrase, + timeout: cfg.Timeout, + metrics: Metrics{ + ingestionDurationMetric: ingestionDurationMetric, + latestLedgerMetric: latestLedgerMetric, + ledgerStatsMetric: ledgerStatsMetric, + }, } return service @@ -119,21 +124,25 @@ func startService(service *Service, cfg Config) { }) } -type Service struct { - logger *log.Entry - db db.ReadWriter - eventStore *events.MemoryStore - transactionStore *transactions.MemoryStore - ledgerBackend backends.LedgerBackend - timeout time.Duration - networkPassPhrase string - done context.CancelFunc - wg sync.WaitGroup +type Metrics struct { ingestionDurationMetric *prometheus.SummaryVec latestLedgerMetric prometheus.Gauge ledgerStatsMetric *prometheus.CounterVec } +type Service struct { + logger *log.Entry + db db.ReadWriter + eventStore *events.MemoryStore + transactionStore *transactions.MemoryStore + ledgerBackend backends.LedgerBackend + timeout time.Duration + networkPassPhrase string + done context.CancelFunc + wg sync.WaitGroup + metrics Metrics +} + func (s *Service) Close() error { s.done() s.wg.Wait() @@ -286,9 +295,9 @@ func (s *Service) ingest(ctx context.Context, sequence uint32) error { } s.logger.Debugf("Ingested ledger %d", sequence) - s.ingestionDurationMetric. + s.metrics.ingestionDurationMetric. With(prometheus.Labels{"type": "total"}).Observe(time.Since(startTime).Seconds()) - s.latestLedgerMetric.Set(float64(sequence)) + s.metrics.latestLedgerMetric.Set(float64(sequence)) return nil } @@ -297,7 +306,7 @@ func (s *Service) ingestLedgerCloseMeta(tx db.WriteTx, ledgerCloseMeta xdr.Ledge if err := tx.LedgerWriter().InsertLedger(ledgerCloseMeta); err != nil { return err } - s.ingestionDurationMetric. + s.metrics.ingestionDurationMetric. With(prometheus.Labels{"type": "ledger_close_meta"}).Observe(time.Since(startTime).Seconds()) if err := s.eventStore.IngestEvents(ledgerCloseMeta); err != nil { diff --git a/cmd/soroban-rpc/internal/ingest/service_test.go b/cmd/soroban-rpc/internal/ingest/service_test.go index c2e4def0..a517562d 100644 --- a/cmd/soroban-rpc/internal/ingest/service_test.go +++ b/cmd/soroban-rpc/internal/ingest/service_test.go @@ -65,6 +65,7 @@ func TestRetryRunningIngestion(t *testing.T) { func TestIngestion(t *testing.T) { mockDB := &MockDB{} mockLedgerBackend := &ledgerbackend.MockDatabaseBackend{} + daemon := interfaces.MakeNoOpDeamon() config := Config{ Logger: supportlog.New(), diff --git a/cmd/soroban-rpc/internal/methods/get_transaction.go b/cmd/soroban-rpc/internal/methods/get_transaction.go index 7a2fe657..de58be64 100644 --- a/cmd/soroban-rpc/internal/methods/get_transaction.go +++ b/cmd/soroban-rpc/internal/methods/get_transaction.go @@ -56,6 +56,10 @@ type GetTransactionResponse struct { Ledger uint32 `json:"ledger,omitempty"` // LedgerCloseTime is the unix timestamp of when the transaction was included in the ledger. LedgerCloseTime int64 `json:"createdAt,string,omitempty"` + + // DiagnosticEventsXDR is present only if Status is equal to TransactionFailed. + // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent + DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` } type GetTransactionRequest struct { @@ -104,6 +108,8 @@ func GetTransaction(getter transactionGetter, request GetTransactionRequest) (Ge response.ResultXdr = base64.StdEncoding.EncodeToString(tx.Result) response.EnvelopeXdr = base64.StdEncoding.EncodeToString(tx.Envelope) response.ResultMetaXdr = base64.StdEncoding.EncodeToString(tx.Meta) + response.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) + if tx.Successful { response.Status = TransactionStatusSuccess } else { diff --git a/cmd/soroban-rpc/internal/methods/get_transaction_test.go b/cmd/soroban-rpc/internal/methods/get_transaction_test.go index 85847f00..263dd92a 100644 --- a/cmd/soroban-rpc/internal/methods/get_transaction_test.go +++ b/cmd/soroban-rpc/internal/methods/get_transaction_test.go @@ -98,6 +98,44 @@ func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { } } +func txMetaWithEvents(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { + + meta := txMeta(acctSeq, successful) + + contractIDBytes, _ := hex.DecodeString("df06d62447fd25da07c0135eed7557e5a5497ee7d15b7fe345bd47e191d8f577") + var contractID xdr.Hash + copy(contractID[:], contractIDBytes) + counter := xdr.ScSymbol("COUNTER") + + meta.V1.TxProcessing[0].TxApplyProcessing.V3 = &xdr.TransactionMetaV3{ + SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: []xdr.ContractEvent{{ + ContractId: &contractID, + Type: xdr.ContractEventTypeContract, + Body: xdr.ContractEventBody{ + V: 0, + V0: &xdr.ContractEventV0{ + Topics: []xdr.ScVal{{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counter, + }}, + Data: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counter, + }, + }, + }, + }}, + ReturnValue: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counter, + }, + }, + } + + return meta +} + func txEnvelope(acctSeq uint32) xdr.TransactionEnvelope { envelope, err := xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ Tx: xdr.Transaction{ @@ -154,6 +192,7 @@ func TestGetTransaction(t *testing.T) { ResultMetaXdr: expectedTxMeta, Ledger: 101, LedgerCloseTime: 2625, + DiagnosticEventsXDR: []string{}, }, tx) // ingest another (failed) transaction @@ -177,6 +216,7 @@ func TestGetTransaction(t *testing.T) { ResultMetaXdr: expectedTxMeta, Ledger: 101, LedgerCloseTime: 2625, + DiagnosticEventsXDR: []string{}, }, tx) // the new transaction should also be there @@ -206,5 +246,45 @@ func TestGetTransaction(t *testing.T) { ResultMetaXdr: expectedTxMeta, Ledger: 102, LedgerCloseTime: 2650, + DiagnosticEventsXDR: []string{}, + }, tx) + + // Test Txn with events + meta = txMetaWithEvents(3, true) + err = store.IngestTransactions(meta) + require.NoError(t, err) + + xdrHash = txHash(3) + hash = hex.EncodeToString(xdrHash[:]) + + expectedTxResult, err = xdr.MarshalBase64(meta.V1.TxProcessing[0].Result.Result) + require.NoError(t, err) + expectedEnvelope, err = xdr.MarshalBase64(txEnvelope(3)) + require.NoError(t, err) + expectedTxMeta, err = xdr.MarshalBase64(meta.V1.TxProcessing[0].TxApplyProcessing) + require.NoError(t, err) + + diagnosticEvents, err := meta.V1.TxProcessing[0].TxApplyProcessing.GetDiagnosticEvents() + require.NoError(t, err) + expectedEventsMeta, err := xdr.MarshalBase64(diagnosticEvents[0]) + + tx, err = GetTransaction(store, GetTransactionRequest{hash}) + require.NoError(t, err) + require.NoError(t, err) + require.Equal(t, GetTransactionResponse{ + Status: TransactionStatusSuccess, + LatestLedger: 103, + LatestLedgerCloseTime: 2675, + OldestLedger: 101, + OldestLedgerCloseTime: 2625, + ApplicationOrder: 1, + FeeBump: false, + EnvelopeXdr: expectedEnvelope, + ResultXdr: expectedTxResult, + ResultMetaXdr: expectedTxMeta, + Ledger: 103, + LedgerCloseTime: 2675, + DiagnosticEventsXDR: []string{expectedEventsMeta}, }, tx) + } diff --git a/cmd/soroban-rpc/internal/test/archive_test.go b/cmd/soroban-rpc/internal/test/archive_test.go new file mode 100644 index 00000000..127e6e61 --- /dev/null +++ b/cmd/soroban-rpc/internal/test/archive_test.go @@ -0,0 +1,25 @@ +package test + +import ( + "net/http" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArchiveUserAgent(t *testing.T) { + userAgents := sync.Map{} + cfg := &TestConfig{ + historyArchiveProxyCallback: func(r *http.Request) { + userAgents.Store(r.Header["User-Agent"][0], "") + }, + } + NewTest(t, cfg) + + _, ok := userAgents.Load("testing") + assert.True(t, ok, "rpc service should set user agent for history archives") + + _, ok = userAgents.Load("testing/captivecore") + assert.True(t, ok, "rpc captive core should set user agent for history archives") +} diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index db138c86..d1a5b901 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -296,7 +296,7 @@ func getCLIDefaultAccount(t *testing.T) string { } func NewCLITest(t *testing.T) *Test { - test := NewTest(t) + test := NewTest(t, nil) fundAccount(t, test, getCLIDefaultAccount(t), "1000000") return test } diff --git a/cmd/soroban-rpc/internal/test/cors_test.go b/cmd/soroban-rpc/internal/test/cors_test.go index 2e0cdb3e..ede91fd8 100644 --- a/cmd/soroban-rpc/internal/test/cors_test.go +++ b/cmd/soroban-rpc/internal/test/cors_test.go @@ -13,7 +13,7 @@ import ( // Specifically, when we include an Origin header in the request, a soroban-rpc should response // with a corresponding Access-Control-Allow-Origin. func TestCORS(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) request, err := http.NewRequest("POST", test.sorobanRPCURL(), bytes.NewBufferString("{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"getHealth\"}")) require.NoError(t, err) diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go b/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go index 46b0b25d..889b4e04 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go +++ b/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go @@ -18,7 +18,7 @@ import ( ) func TestGetLedgerEntriesNotFound(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -56,7 +56,7 @@ func TestGetLedgerEntriesNotFound(t *testing.T) { } func TestGetLedgerEntriesInvalidParams(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -74,7 +74,7 @@ func TestGetLedgerEntriesInvalidParams(t *testing.T) { } func TestGetLedgerEntriesSucceeds(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go b/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go index dd4879d5..b453f4d8 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go +++ b/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go @@ -18,7 +18,7 @@ import ( ) func TestGetLedgerEntryNotFound(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -51,7 +51,7 @@ func TestGetLedgerEntryNotFound(t *testing.T) { } func TestGetLedgerEntryInvalidParams(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -67,7 +67,7 @@ func TestGetLedgerEntryInvalidParams(t *testing.T) { } func TestGetLedgerEntrySucceeds(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) diff --git a/cmd/soroban-rpc/internal/test/get_network_test.go b/cmd/soroban-rpc/internal/test/get_network_test.go index 39805d86..9c8ef266 100644 --- a/cmd/soroban-rpc/internal/test/get_network_test.go +++ b/cmd/soroban-rpc/internal/test/get_network_test.go @@ -12,7 +12,7 @@ import ( ) func TestGetNetworkSucceeds(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) diff --git a/cmd/soroban-rpc/internal/test/health_test.go b/cmd/soroban-rpc/internal/test/health_test.go index 4afbf7f8..517b374d 100644 --- a/cmd/soroban-rpc/internal/test/health_test.go +++ b/cmd/soroban-rpc/internal/test/health_test.go @@ -11,7 +11,7 @@ import ( ) func TestHealth(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/test/integration.go index ea918d13..d3755a9c 100644 --- a/cmd/soroban-rpc/internal/test/integration.go +++ b/cmd/soroban-rpc/internal/test/integration.go @@ -3,6 +3,10 @@ package test import ( "context" "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" "os" "os/exec" "os/signal" @@ -30,9 +34,11 @@ const ( StandaloneNetworkPassphrase = "Standalone Network ; February 2017" stellarCoreProtocolVersion = 20 stellarCorePort = 11626 + stellarCoreArchiveHost = "localhost:1570" goModFile = "go.mod" goMonorepoGithubPath = "github.com/stellar/go" - friendbotURL = "http://localhost:8000/friendbot" + + friendbotURL = "http://localhost:8000/friendbot" // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true checkpointFrequency = 8 sorobanRPCPort = 8000 @@ -40,6 +46,10 @@ const ( helloWorldContractPath = "../../../../target/wasm32-unknown-unknown/test-wasms/test_hello_world.wasm" ) +type TestConfig struct { + historyArchiveProxyCallback func(*http.Request) +} + type Test struct { t *testing.T @@ -47,6 +57,9 @@ type Test struct { daemon *daemon.Daemon + historyArchiveProxy *httptest.Server + historyArchiveProxyCallback func(*http.Request) + coreClient *stellarcore.Client masterAccount txnbuild.Account @@ -54,7 +67,7 @@ type Test struct { shutdownCalls []func() } -func NewTest(t *testing.T) *Test { +func NewTest(t *testing.T, cfg *TestConfig) *Test { if os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_ENABLED") == "" { t.Skip("skipping integration test: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED not set") } @@ -71,6 +84,19 @@ func NewTest(t *testing.T) *Test { AccountID: i.MasterKey().Address(), Sequence: 0, } + if cfg != nil { + i.historyArchiveProxyCallback = cfg.historyArchiveProxyCallback + } + + proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: stellarCoreArchiveHost}) + + i.historyArchiveProxy = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if i.historyArchiveProxyCallback != nil { + i.historyArchiveProxyCallback(r) + } + proxy.ServeHTTP(w, r) + })) + i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") i.prepareShutdownHandlers() i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} @@ -138,7 +164,7 @@ func (i *Test) launchDaemon(coreBinaryPath string) { config.CaptiveCoreHTTPPort = 0 config.FriendbotURL = friendbotURL config.NetworkPassphrase = StandaloneNetworkPassphrase - config.HistoryArchiveURLs = []string{"http://localhost:1570"} + config.HistoryArchiveURLs = []string{i.historyArchiveProxy.URL} config.LogLevel = logrus.DebugLevel config.SQLiteDBPath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") config.IngestionTimeout = 10 * time.Minute @@ -146,6 +172,7 @@ func (i *Test) launchDaemon(coreBinaryPath string) { config.CheckpointFrequency = checkpointFrequency config.MaxHealthyLedgerLatency = time.Second * 10 config.PreflightEnableDebug = true + config.HistoryArchiveUserAgent = "testing" i.daemon = daemon.MustNew(&config) go i.daemon.Run() @@ -203,6 +230,9 @@ func (i *Test) prepareShutdownHandlers() { if i.daemon != nil { i.daemon.Close() } + if i.historyArchiveProxy != nil { + i.historyArchiveProxy.Close() + } i.runComposeCommand("down", "-v") }, ) diff --git a/cmd/soroban-rpc/internal/test/metrics_test.go b/cmd/soroban-rpc/internal/test/metrics_test.go index 5608a2f9..b807cb77 100644 --- a/cmd/soroban-rpc/internal/test/metrics_test.go +++ b/cmd/soroban-rpc/internal/test/metrics_test.go @@ -14,7 +14,7 @@ import ( ) func TestMetrics(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) metrics := getMetrics(test) buildMetric := fmt.Sprintf( "soroban_rpc_build_info{branch=\"%s\",build_timestamp=\"%s\",commit=\"%s\",goversion=\"%s\",version=\"%s\"} 1", diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index ec785050..12387530 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -189,7 +189,7 @@ func preflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbu } func TestSimulateTransactionSucceeds(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -307,7 +307,7 @@ func TestSimulateTransactionSucceeds(t *testing.T) { } func TestSimulateTransactionWithAuth(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -365,7 +365,7 @@ func TestSimulateTransactionWithAuth(t *testing.T) { } func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -550,7 +550,7 @@ func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { } func TestSimulateTransactionError(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -590,7 +590,7 @@ func TestSimulateTransactionError(t *testing.T) { } func TestSimulateTransactionMultipleOperations(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -625,7 +625,7 @@ func TestSimulateTransactionMultipleOperations(t *testing.T) { } func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -656,7 +656,7 @@ func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { } func TestSimulateTransactionUnmarshalError(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -673,7 +673,7 @@ func TestSimulateTransactionUnmarshalError(t *testing.T) { } func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -906,7 +906,7 @@ func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.L } func TestSimulateInvokePrng_u64_in_range(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -1017,7 +1017,7 @@ func TestSimulateInvokePrng_u64_in_range(t *testing.T) { } func TestSimulateSystemEvent(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -1125,7 +1125,7 @@ func TestSimulateSystemEvent(t *testing.T) { err = xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) require.NoError(t, err) assert.InDelta(t, 6856, uint32(transactionData.Resources.ReadBytes), 200) - + // the resulting fee is derived from compute factors and a default padding is applied to instructions by preflight // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo diff --git a/cmd/soroban-rpc/internal/test/transaction_test.go b/cmd/soroban-rpc/internal/test/transaction_test.go index 1cd0d198..9027bc58 100644 --- a/cmd/soroban-rpc/internal/test/transaction_test.go +++ b/cmd/soroban-rpc/internal/test/transaction_test.go @@ -21,7 +21,7 @@ import ( ) func TestSendTransactionSucceedsWithoutResults(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -46,7 +46,7 @@ func TestSendTransactionSucceedsWithoutResults(t *testing.T) { } func TestSendTransactionSucceedsWithResults(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -110,7 +110,7 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { } func TestSendTransactionBadSequence(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -152,7 +152,7 @@ func TestSendTransactionBadSequence(t *testing.T) { } func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -204,7 +204,7 @@ func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { } func TestSendTransactionFailedInLedger(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) @@ -266,7 +266,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { } func TestSendTransactionFailedInvalidXDR(t *testing.T) { - test := NewTest(t) + test := NewTest(t, nil) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) diff --git a/cmd/soroban-rpc/internal/transactions/transactions.go b/cmd/soroban-rpc/internal/transactions/transactions.go index 5ed719ad..f2feef49 100644 --- a/cmd/soroban-rpc/internal/transactions/transactions.go +++ b/cmd/soroban-rpc/internal/transactions/transactions.go @@ -146,9 +146,10 @@ type LedgerInfo struct { } type Transaction struct { - Result []byte // XDR encoded xdr.TransactionResult - Meta []byte // XDR encoded xdr.TransactionMeta - Envelope []byte // XDR encoded xdr.TransactionEnvelope + Result []byte // XDR encoded xdr.TransactionResult + Meta []byte // XDR encoded xdr.TransactionMeta + Envelope []byte // XDR encoded xdr.TransactionEnvelope + Events [][]byte // XDR encoded xdr.DiagnosticEvent FeeBump bool ApplicationOrder int32 Successful bool @@ -198,10 +199,33 @@ func (m *MemoryStore) GetTransaction(hash xdr.Hash) (Transaction, bool, StoreRan if !ok { return Transaction{}, false, storeRange } + + var txMeta xdr.TransactionMeta + err := txMeta.UnmarshalBinary(internalTx.meta) + if err != nil { + return Transaction{}, false, storeRange + } + + txEvents, err := txMeta.GetDiagnosticEvents() + if err != nil { + return Transaction{}, false, storeRange + } + + events := make([][]byte, 0, len(txEvents)) + + for _, e := range txEvents { + diagnosticEventXDR, err := e.MarshalBinary() + if err != nil { + return Transaction{}, false, storeRange + } + events = append(events, diagnosticEventXDR) + } + tx := Transaction{ Result: internalTx.result, Meta: internalTx.meta, Envelope: internalTx.envelope, + Events: events, FeeBump: internalTx.feeBump, Successful: internalTx.successful, ApplicationOrder: internalTx.applicationOrder, diff --git a/cmd/soroban-rpc/internal/transactions/transactions_test.go b/cmd/soroban-rpc/internal/transactions/transactions_test.go index d32a62c6..27b500c4 100644 --- a/cmd/soroban-rpc/internal/transactions/transactions_test.go +++ b/cmd/soroban-rpc/internal/transactions/transactions_test.go @@ -20,6 +20,7 @@ func expectedTransaction(t *testing.T, ledger uint32, feeBump bool) Transaction FeeBump: feeBump, ApplicationOrder: 1, Ledger: expectedLedgerInfo(ledger), + Events: [][]byte{}, } var err error tx.Result, err = transactionResult(ledger, feeBump).MarshalBinary() @@ -204,6 +205,42 @@ func txMeta(ledgerSequence uint32, feeBump bool) xdr.LedgerCloseMeta { } } +func txMetaWithEvents(ledgerSequence uint32, feeBump bool) xdr.LedgerCloseMeta { + tx := txMeta(ledgerSequence, feeBump) + contractIDBytes, _ := hex.DecodeString("df06d62447fd25da07c0135eed7557e5a5497ee7d15b7fe345bd47e191d8f577") + var contractID xdr.Hash + copy(contractID[:], contractIDBytes) + counter := xdr.ScSymbol("COUNTER") + + tx.V1.TxProcessing[0].TxApplyProcessing.V3 = &xdr.TransactionMetaV3{ + SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: []xdr.ContractEvent{{ + ContractId: &contractID, + Type: xdr.ContractEventTypeContract, + Body: xdr.ContractEventBody{ + V: 0, + V0: &xdr.ContractEventV0{ + Topics: []xdr.ScVal{{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counter, + }}, + Data: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counter, + }, + }, + }, + }}, + ReturnValue: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counter, + }, + }, + } + + return tx +} + func txEnvelope(ledgerSequence uint32, feeBump bool) xdr.TransactionEnvelope { var envelope xdr.TransactionEnvelope var err error @@ -311,6 +348,27 @@ func TestIngestTransactions(t *testing.T) { require.Len(t, store.transactions, 3) } +func TestGetTransactionsWithEventData(t *testing.T) { + store := NewMemoryStore(interfaces.MakeNoOpDeamon(), "passphrase", 100) + + // Insert ledger 1 + metaWithEvents := txMetaWithEvents(1, false) + require.NoError(t, store.IngestTransactions(metaWithEvents)) + require.Len(t, store.transactions, 1) + + // check events data + tx, ok, _ := store.GetTransaction(txHash(1, false)) + require.True(t, ok) + require.NotNil(t, tx.Events) + require.Len(t, tx.Events, 1) + + events, err := metaWithEvents.V1.TxProcessing[0].TxApplyProcessing.GetDiagnosticEvents() + require.NoError(t, err) + eventBytes, err := events[0].MarshalBinary() + require.NoError(t, err) + require.Equal(t, eventBytes, tx.Events[0]) +} + func stableHeapInUse() int64 { var ( m = runtime.MemStats{} diff --git a/cmd/soroban-rpc/main.go b/cmd/soroban-rpc/main.go index 130ea78d..e7e8713b 100644 --- a/cmd/soroban-rpc/main.go +++ b/cmd/soroban-rpc/main.go @@ -26,6 +26,7 @@ func main() { fmt.Fprintln(os.Stderr, err) os.Exit(1) } + cfg.HistoryArchiveUserAgent = fmt.Sprintf("soroban-rpc/%s", config.Version) daemon.MustNew(&cfg).Run() }, } diff --git a/go.mod b/go.mod index d7c19cf4..52a105e1 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stellar/go v0.0.0-20240202231803-b0df9f046eb4 + github.com/stellar/go v0.0.0-20240207003209-73de95c8eb55 github.com/stretchr/testify v1.8.4 golang.org/x/mod v0.13.0 gotest.tools/v3 v3.5.0 diff --git a/go.sum b/go.sum index 56e9fc7a..32930a08 100644 --- a/go.sum +++ b/go.sum @@ -366,8 +366,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stellar/go v0.0.0-20240202231803-b0df9f046eb4 h1:1DQT7eta18GSv+z6wF7AMUf7NqQ0qOrr2uJPGMRakRg= -github.com/stellar/go v0.0.0-20240202231803-b0df9f046eb4/go.mod h1:Ka4piwZT4Q9799f+BZeaKkAiYo4UpIWXyu0oSUbCVfM= +github.com/stellar/go v0.0.0-20240207003209-73de95c8eb55 h1:YBpAp7uPf/lzGxKPOGh1D05bX7uDVybA39BYoPXpRu4= +github.com/stellar/go v0.0.0-20240207003209-73de95c8eb55/go.mod h1:Ka4piwZT4Q9799f+BZeaKkAiYo4UpIWXyu0oSUbCVfM= github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=