diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 59f7d4d1..2a3dbae1 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -29,7 +29,7 @@ jobs: make build-libpreflight - name: Run golangci-lint - uses: golangci/golangci-lint-action@537aa1903e5d359d0b27dbc19ddd22c5087f3fbc # version v3.2.0 + uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # version v6.0.1 with: version: v1.52.2 # this is the golangci-lint version args: --issues-exit-code=0 # exit without errors for now - won't fail the build diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index c38ccde9..39685927 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -187,49 +187,7 @@ func MustNew(cfg *config.Config) *Daemon { }, metricsRegistry), } - eventStore := events.NewMemoryStore( - daemon, - cfg.NetworkPassphrase, - cfg.EventLedgerRetentionWindow, - ) - feewindows := feewindow.NewFeeWindows(cfg.ClassicFeeStatsLedgerRetentionWindow, cfg.SorobanFeeStatsLedgerRetentionWindow, cfg.NetworkPassphrase) - - // initialize the stores using what was on the DB - readTxMetaCtx, cancelReadTxMeta := context.WithTimeout(context.Background(), cfg.IngestionTimeout) - defer cancelReadTxMeta() - // NOTE: We could optimize this to avoid unnecessary ingestion calls - // (the range of txmetas can be larger than the individual store retention windows) - // but it's probably not worth the pain. - var initialSeq uint32 - var currentSeq uint32 - err = db.NewLedgerReader(dbConn).StreamAllLedgers(readTxMetaCtx, func(txmeta xdr.LedgerCloseMeta) error { - currentSeq = txmeta.LedgerSequence() - if initialSeq == 0 { - initialSeq = currentSeq - logger.WithFields(supportlog.F{ - "seq": currentSeq, - }).Info("initializing in-memory store") - } else if (currentSeq-initialSeq)%inMemoryInitializationLedgerLogPeriod == 0 { - logger.WithFields(supportlog.F{ - "seq": currentSeq, - }).Debug("still initializing in-memory store") - } - if err := eventStore.IngestEvents(txmeta); err != nil { - logger.WithError(err).Fatal("could not initialize event memory store") - } - if err := feewindows.IngestFees(txmeta); err != nil { - logger.WithError(err).Fatal("could not initialize fee stats") - } - return nil - }) - if err != nil { - logger.WithError(err).Fatal("could not obtain txmeta cache from the database") - } - if currentSeq != 0 { - logger.WithFields(supportlog.F{ - "seq": currentSeq, - }).Info("finished initializing in-memory store") - } + feewindows, eventStore := daemon.mustInitializeStorage(cfg) onIngestionRetry := func(err error, dur time.Duration) { logger.WithError(err).Error("could not run ingestion. Retrying") @@ -317,6 +275,69 @@ func MustNew(cfg *config.Config) *Daemon { return daemon } +// mustInitializeStorage initializes the storage using what was on the DB +func (d *Daemon) mustInitializeStorage(cfg *config.Config) (*feewindow.FeeWindows, *events.MemoryStore) { + eventStore := events.NewMemoryStore( + d, + cfg.NetworkPassphrase, + cfg.EventLedgerRetentionWindow, + ) + feewindows := feewindow.NewFeeWindows(cfg.ClassicFeeStatsLedgerRetentionWindow, cfg.SorobanFeeStatsLedgerRetentionWindow, cfg.NetworkPassphrase) + + readTxMetaCtx, cancelReadTxMeta := context.WithTimeout(context.Background(), cfg.IngestionTimeout) + defer cancelReadTxMeta() + var initialSeq uint32 + var currentSeq uint32 + dataMigrations, err := db.BuildMigrations(readTxMetaCtx, d.logger, d.db, cfg) + if err != nil { + d.logger.WithError(err).Fatal("could not build migrations") + } + // NOTE: We could optimize this to avoid unnecessary ingestion calls + // (the range of txmetas can be larger than the individual store retention windows) + // but it's probably not worth the pain. + err = db.NewLedgerReader(d.db).StreamAllLedgers(readTxMetaCtx, func(txmeta xdr.LedgerCloseMeta) error { + currentSeq = txmeta.LedgerSequence() + if initialSeq == 0 { + initialSeq = currentSeq + d.logger.WithFields(supportlog.F{ + "seq": currentSeq, + }).Info("initializing in-memory store") + } else if (currentSeq-initialSeq)%inMemoryInitializationLedgerLogPeriod == 0 { + d.logger.WithFields(supportlog.F{ + "seq": currentSeq, + }).Debug("still initializing in-memory store") + } + if err := eventStore.IngestEvents(txmeta); err != nil { + d.logger.WithError(err).Fatal("could not initialize event memory store") + } + if err := feewindows.IngestFees(txmeta); err != nil { + d.logger.WithError(err).Fatal("could not initialize fee stats") + } + // TODO: clean up once we remove the in-memory storage. + // (we should only stream over the required range) + if r := dataMigrations.ApplicableRange(); r.IsLedgerIncluded(currentSeq) { + if err := dataMigrations.Apply(readTxMetaCtx, txmeta); err != nil { + d.logger.WithError(err).Fatal("could not run migrations") + } + } + return nil + }) + if err != nil { + d.logger.WithError(err).Fatal("could not obtain txmeta cache from the database") + } + if err := dataMigrations.Commit(readTxMetaCtx); err != nil { + d.logger.WithError(err).Fatal("could not commit data migrations") + } + + if currentSeq != 0 { + d.logger.WithFields(supportlog.F{ + "seq": currentSeq, + }).Info("finished initializing in-memory store") + } + + return feewindows, eventStore +} + func (d *Daemon) Run() { d.logger.WithFields(supportlog.F{ "addr": d.server.Addr, diff --git a/cmd/soroban-rpc/internal/db/db.go b/cmd/soroban-rpc/internal/db/db.go index 227e2115..a3d54a6f 100644 --- a/cmd/soroban-rpc/internal/db/db.go +++ b/cmd/soroban-rpc/internal/db/db.go @@ -17,11 +17,12 @@ import ( "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" ) -//go:embed migrations/*.sql -var migrations embed.FS +//go:embed sqlmigrations/*.sql +var sqlMigrations embed.FS var ErrEmptyDB = errors.New("DB is empty") @@ -52,7 +53,7 @@ type dbCache struct { type DB struct { db.SessionInterface - cache dbCache + cache *dbCache } func openSQLiteDB(dbFilePath string) (*db.Session, error) { @@ -65,9 +66,9 @@ func openSQLiteDB(dbFilePath string) (*db.Session, error) { return nil, errors.Wrap(err, "open failed") } - if err = runMigrations(session.DB.DB, "sqlite3"); err != nil { + if err = runSQLMigrations(session.DB.DB, "sqlite3"); err != nil { _ = session.Close() - return nil, errors.Wrap(err, "could not run migrations") + return nil, errors.Wrap(err, "could not run SQL migrations") } return session, nil } @@ -79,7 +80,7 @@ func OpenSQLiteDBWithPrometheusMetrics(dbFilePath string, namespace string, sub } result := DB{ SessionInterface: db.RegisterMetrics(session, namespace, sub, registry), - cache: dbCache{ + cache: &dbCache{ ledgerEntries: newTransactionalCache(), }, } @@ -93,28 +94,50 @@ func OpenSQLiteDB(dbFilePath string) (*DB, error) { } result := DB{ SessionInterface: session, - cache: dbCache{ + cache: &dbCache{ ledgerEntries: newTransactionalCache(), }, } return &result, nil } -func getLatestLedgerSequence(ctx context.Context, q db.SessionInterface, cache *dbCache) (uint32, error) { - sql := sq.Select("value").From(metaTableName).Where(sq.Eq{"key": latestLedgerSequenceMetaKey}) +func getMetaBool(ctx context.Context, q db.SessionInterface, key string) (bool, error) { + valueStr, err := getMetaValue(ctx, q, key) + if err != nil { + return false, err + } + return strconv.ParseBool(valueStr) +} + +func setMetaBool(ctx context.Context, q db.SessionInterface, key string, value bool) error { + query := sq.Replace(metaTableName). + Values(key, strconv.FormatBool(value)) + _, err := q.Exec(ctx, query) + return err +} + +func getMetaValue(ctx context.Context, q db.SessionInterface, key string) (string, error) { + sql := sq.Select("value").From(metaTableName).Where(sq.Eq{"key": key}) var results []string if err := q.Select(ctx, &results, sql); err != nil { - return 0, err + return "", err } switch len(results) { case 0: - return 0, ErrEmptyDB + return "", ErrEmptyDB case 1: // expected length on an initialized DB default: - return 0, fmt.Errorf("multiple entries (%d) for key %q in table %q", len(results), latestLedgerSequenceMetaKey, metaTableName) + return "", fmt.Errorf("multiple entries (%d) for key %q in table %q", len(results), latestLedgerSequenceMetaKey, metaTableName) + } + return results[0], nil +} + +func getLatestLedgerSequence(ctx context.Context, q db.SessionInterface, cache *dbCache) (uint32, error) { + latestLedgerStr, err := getMetaValue(ctx, q, latestLedgerSequenceMetaKey) + if err != nil { + return 0, err } - latestLedgerStr := results[0] latestLedger, err := strconv.ParseUint(latestLedgerStr, 10, 32) if err != nil { return 0, err @@ -125,7 +148,7 @@ func getLatestLedgerSequence(ctx context.Context, q db.SessionInterface, cache * // Otherwise, the write-through cache won't get updated until the first ingestion commit cache.Lock() if cache.latestLedgerSeq == 0 { - // Only update the cache if value is missing (0), otherwise + // Only update the cache if the value is missing (0), otherwise // we may end up overwriting the entry with an older version cache.latestLedgerSeq = result } @@ -192,7 +215,7 @@ func NewReadWriter( } func (rw *readWriter) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return getLatestLedgerSequence(ctx, rw.db, &rw.db.cache) + return getLatestLedgerSequence(ctx, rw.db, rw.db.cache) } func (rw *readWriter) NewTx(ctx context.Context) (WriteTx, error) { @@ -204,8 +227,9 @@ func (rw *readWriter) NewTx(ctx context.Context) (WriteTx, error) { db := rw.db writer := writeTx{ - globalCache: &db.cache, + globalCache: db.cache, postCommit: func() error { + // TODO: this is sqlite-only, it shouldn't be here _, err := db.ExecRaw(ctx, "PRAGMA wal_checkpoint(TRUNCATE)") return err }, @@ -308,12 +332,12 @@ func (w writeTx) Rollback() error { } } -func runMigrations(db *sql.DB, dialect string) error { +func runSQLMigrations(db *sql.DB, dialect string) error { m := &migrate.AssetMigrationSource{ - Asset: migrations.ReadFile, + Asset: sqlMigrations.ReadFile, AssetDir: func() func(string) ([]string, error) { return func(path string) ([]string, error) { - dirEntry, err := migrations.ReadDir(path) + dirEntry, err := sqlMigrations.ReadDir(path) if err != nil { return nil, err } @@ -325,7 +349,7 @@ func runMigrations(db *sql.DB, dialect string) error { return entries, nil } }(), - Dir: "migrations", + Dir: "sqlmigrations", } _, err := migrate.ExecMax(db, dialect, m, migrate.Up, 0) return err diff --git a/cmd/soroban-rpc/internal/db/ledger_test.go b/cmd/soroban-rpc/internal/db/ledger_test.go index f6ebd70b..25369fac 100644 --- a/cmd/soroban-rpc/internal/db/ledger_test.go +++ b/cmd/soroban-rpc/internal/db/ledger_test.go @@ -6,10 +6,12 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/go/network" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" ) @@ -106,16 +108,9 @@ func NewTestDB(tb testing.TB) *DB { tmp := tb.TempDir() dbPath := path.Join(tmp, "db.sqlite") db, err := OpenSQLiteDB(dbPath) - if err != nil { - assert.NoError(tb, db.Close()) - } + require.NoError(tb, err) tb.Cleanup(func() { assert.NoError(tb, db.Close()) }) - return &DB{ - SessionInterface: db, - cache: dbCache{ - ledgerEntries: newTransactionalCache(), - }, - } + return db } diff --git a/cmd/soroban-rpc/internal/db/ledgerentry.go b/cmd/soroban-rpc/internal/db/ledgerentry.go index 8286e955..aaffed06 100644 --- a/cmd/soroban-rpc/internal/db/ledgerentry.go +++ b/cmd/soroban-rpc/internal/db/ledgerentry.go @@ -341,7 +341,7 @@ func NewLedgerEntryReader(db *DB) LedgerEntryReader { } func (r ledgerEntryReader) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return getLatestLedgerSequence(ctx, r.db, &r.db.cache) + return getLatestLedgerSequence(ctx, r.db, r.db.cache) } // NewCachedTx() caches all accessed ledger entries and select statements. If many ledger entries are accessed, it will grow without bounds. @@ -360,7 +360,7 @@ func (r ledgerEntryReader) NewCachedTx(ctx context.Context) (LedgerEntryReadTx, } cacheReadTx := r.db.cache.ledgerEntries.newReadTx() return &ledgerEntryReadTx{ - globalCache: &r.db.cache, + globalCache: r.db.cache, stmtCache: sq.NewStmtCache(txSession.GetTx()), latestLedgerSeqCache: r.db.cache.latestLedgerSeq, ledgerEntryCacheReadTx: &cacheReadTx, @@ -377,7 +377,7 @@ func (r ledgerEntryReader) NewTx(ctx context.Context) (LedgerEntryReadTx, error) r.db.cache.RLock() defer r.db.cache.RUnlock() return &ledgerEntryReadTx{ - globalCache: &r.db.cache, + globalCache: r.db.cache, latestLedgerSeqCache: r.db.cache.latestLedgerSeq, tx: txSession, buffer: xdr.NewEncodingBuffer(), diff --git a/cmd/soroban-rpc/internal/db/migration.go b/cmd/soroban-rpc/internal/db/migration.go new file mode 100644 index 00000000..2ed97946 --- /dev/null +++ b/cmd/soroban-rpc/internal/db/migration.go @@ -0,0 +1,194 @@ +package db + +import ( + "context" + "errors" + "fmt" + + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" +) + +type LedgerSeqRange struct { + firstLedgerSeq uint32 + lastLedgerSeq uint32 +} + +func (mlr *LedgerSeqRange) IsLedgerIncluded(ledgerSeq uint32) bool { + if mlr == nil { + return false + } + return ledgerSeq >= mlr.firstLedgerSeq && ledgerSeq <= mlr.lastLedgerSeq +} + +func (mlr *LedgerSeqRange) Merge(other *LedgerSeqRange) *LedgerSeqRange { + if mlr == nil { + return other + } + if other == nil { + return mlr + } + // TODO: using min/max can result in a much larger range than needed, + // as an optimization, we should probably use a sequence of ranges instead. + return &LedgerSeqRange{ + firstLedgerSeq: min(mlr.firstLedgerSeq, other.firstLedgerSeq), + lastLedgerSeq: max(mlr.lastLedgerSeq, other.lastLedgerSeq), + } +} + +type MigrationApplier interface { + // ApplicableRange returns the closed ledger sequence interval, + // where Apply() should be called. A null result indicates the empty range + ApplicableRange() *LedgerSeqRange + // Apply applies the migration on a ledger. It should never be applied + // in ledgers outside the ApplicableRange() + Apply(ctx context.Context, meta xdr.LedgerCloseMeta) error +} + +type migrationApplierFactory interface { + New(db *DB, latestLedger uint32) (MigrationApplier, error) +} + +type migrationApplierFactoryF func(db *DB, latestLedger uint32) (MigrationApplier, error) + +func (m migrationApplierFactoryF) New(db *DB, latestLedger uint32) (MigrationApplier, error) { + return m(db, latestLedger) +} + +type Migration interface { + MigrationApplier + Commit(ctx context.Context) error + Rollback(ctx context.Context) error +} + +type multiMigration []Migration + +func (mm multiMigration) ApplicableRange() *LedgerSeqRange { + var result *LedgerSeqRange + for _, m := range mm { + result = m.ApplicableRange().Merge(result) + } + return result +} + +func (mm multiMigration) Apply(ctx context.Context, meta xdr.LedgerCloseMeta) error { + var err error + for _, m := range mm { + ledgerSeq := meta.LedgerSequence() + if !m.ApplicableRange().IsLedgerIncluded(ledgerSeq) { + // The range of a sub-migration can be smaller than the global range. + continue + } + if localErr := m.Apply(ctx, meta); localErr != nil { + err = errors.Join(err, localErr) + } + } + return err +} + +func (mm multiMigration) Commit(ctx context.Context) error { + var err error + for _, m := range mm { + if localErr := m.Commit(ctx); localErr != nil { + err = errors.Join(err, localErr) + } + } + return err +} + +func (mm multiMigration) Rollback(ctx context.Context) error { + var err error + for _, m := range mm { + if localErr := m.Rollback(ctx); localErr != nil { + err = errors.Join(err, localErr) + } + } + return err +} + +// guardedMigration is a db data migration whose application is guarded by a boolean in the meta table +// (after the migration is applied the boolean is set to true, so that the migration is not applied again) +type guardedMigration struct { + guardMetaKey string + db *DB + migration MigrationApplier + alreadyMigrated bool +} + +func newGuardedDataMigration(ctx context.Context, uniqueMigrationName string, factory migrationApplierFactory, db *DB) (Migration, error) { + migrationDB := &DB{ + cache: db.cache, + SessionInterface: db.SessionInterface.Clone(), + } + if err := migrationDB.Begin(ctx); err != nil { + return nil, err + } + metaKey := "Migration" + uniqueMigrationName + "Done" + previouslyMigrated, err := getMetaBool(ctx, migrationDB, metaKey) + if err != nil && !errors.Is(err, ErrEmptyDB) { + migrationDB.Rollback() + return nil, err + } + latestLedger, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(ctx) + if err != nil && err != ErrEmptyDB { + migrationDB.Rollback() + return nil, fmt.Errorf("failed to get latest ledger sequence: %w", err) + } + applier, err := factory.New(migrationDB, latestLedger) + if err != nil { + migrationDB.Rollback() + return nil, err + } + guardedMigration := &guardedMigration{ + guardMetaKey: metaKey, + db: migrationDB, + migration: applier, + alreadyMigrated: previouslyMigrated, + } + return guardedMigration, nil +} + +func (g *guardedMigration) Apply(ctx context.Context, meta xdr.LedgerCloseMeta) error { + if g.alreadyMigrated { + // This shouldn't happen since we would be out of the applicable range + // but, just in case. + return nil + } + return g.migration.Apply(ctx, meta) +} + +func (g *guardedMigration) ApplicableRange() *LedgerSeqRange { + if g.alreadyMigrated { + return nil + } + return g.migration.ApplicableRange() +} + +func (g *guardedMigration) Commit(ctx context.Context) error { + if g.alreadyMigrated { + return nil + } + err := setMetaBool(ctx, g.db, g.guardMetaKey, true) + if err != nil { + g.Rollback(ctx) + return errors.Join(err, g.Rollback(ctx)) + } + return g.db.Commit() +} + +func (g *guardedMigration) Rollback(ctx context.Context) error { + return g.db.Rollback() +} + +func BuildMigrations(ctx context.Context, logger *log.Entry, db *DB, cfg *config.Config) (Migration, error) { + migrationName := "TransactionsTable" + factory := newTransactionTableMigration(ctx, logger.WithField("migration", migrationName), cfg.TransactionLedgerRetentionWindow, cfg.NetworkPassphrase) + m, err := newGuardedDataMigration(ctx, migrationName, factory, db) + if err != nil { + return nil, fmt.Errorf("creating guarded transaction migration: %w", err) + } + // Add other migrations here + return multiMigration{m}, nil +} diff --git a/cmd/soroban-rpc/internal/db/migrations/01_init.sql b/cmd/soroban-rpc/internal/db/sqlmigrations/01_init.sql similarity index 100% rename from cmd/soroban-rpc/internal/db/migrations/01_init.sql rename to cmd/soroban-rpc/internal/db/sqlmigrations/01_init.sql diff --git a/cmd/soroban-rpc/internal/db/migrations/02_transactions.sql b/cmd/soroban-rpc/internal/db/sqlmigrations/02_transactions.sql similarity index 100% rename from cmd/soroban-rpc/internal/db/migrations/02_transactions.sql rename to cmd/soroban-rpc/internal/db/sqlmigrations/02_transactions.sql diff --git a/cmd/soroban-rpc/internal/db/transaction.go b/cmd/soroban-rpc/internal/db/transaction.go index c703c39e..099b4a15 100644 --- a/cmd/soroban-rpc/internal/db/transaction.go +++ b/cmd/soroban-rpc/internal/db/transaction.go @@ -39,7 +39,7 @@ type TransactionWriter interface { RegisterMetrics(ingest, count prometheus.Observer) } -// TransactionReader provides all of the public ways to read from the DB. +// TransactionReader provides all the public ways to read from the DB. type TransactionReader interface { GetTransaction(ctx context.Context, hash xdr.Hash) (Transaction, ledgerbucketwindow.LedgerRange, error) GetLedgerRange(ctx context.Context) (ledgerbucketwindow.LedgerRange, error) @@ -107,8 +107,7 @@ func (txn *transactionHandler) InsertTransactions(lcm xdr.LedgerCloseMeta) error } _, err = query.RunWith(txn.stmtCache).Exec() - L.WithError(err). - WithField("duration", time.Since(start)). + L.WithField("duration", time.Since(start)). Infof("Ingested %d transaction lookups", len(transactions)) return err @@ -302,3 +301,50 @@ func ParseTransaction(lcm xdr.LedgerCloseMeta, ingestTx ingest.LedgerTransaction return tx, nil } + +type transactionTableMigration struct { + firstLedger uint32 + lastLedger uint32 + writer TransactionWriter +} + +func (t *transactionTableMigration) ApplicableRange() *LedgerSeqRange { + return &LedgerSeqRange{ + firstLedgerSeq: t.firstLedger, + lastLedgerSeq: t.lastLedger, + } +} + +func (t *transactionTableMigration) Apply(ctx context.Context, meta xdr.LedgerCloseMeta) error { + return t.writer.InsertTransactions(meta) +} + +func newTransactionTableMigration(ctx context.Context, logger *log.Entry, retentionWindow uint32, passphrase string) migrationApplierFactory { + return migrationApplierFactoryF(func(db *DB, latestLedger uint32) (MigrationApplier, error) { + firstLedgerToMigrate := uint32(2) + writer := &transactionHandler{ + log: logger, + db: db, + stmtCache: sq.NewStmtCache(db.GetTx()), + passphrase: passphrase, + } + if latestLedger > retentionWindow { + firstLedgerToMigrate = latestLedger - retentionWindow + } + // Truncate the table, since it may contain data, causing insert conflicts later on. + // (the migration was shipped after the actual transactions table change) + // FIXME: this can be simply replaced by an upper limit in the ledgers to migrate + // but ... it can't be done until https://github.com/stellar/soroban-rpc/issues/208 + // is addressed + _, err := db.Exec(ctx, sq.Delete(transactionTableName)) + if err != nil { + return nil, fmt.Errorf("couldn't delete table %q: %w", transactionTableName, err) + } + migration := transactionTableMigration{ + firstLedger: firstLedgerToMigrate, + lastLedger: latestLedger, + writer: writer, + } + return &migration, nil + }) +} diff --git a/cmd/soroban-rpc/internal/db/transaction_test.go b/cmd/soroban-rpc/internal/db/transaction_test.go index 068f793d..7257c1e1 100644 --- a/cmd/soroban-rpc/internal/db/transaction_test.go +++ b/cmd/soroban-rpc/internal/db/transaction_test.go @@ -10,6 +10,7 @@ import ( "github.com/stellar/go/network" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" "github.com/stretchr/testify/assert" diff --git a/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go b/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go index 5be64cea..5bc3a40f 100644 --- a/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go +++ b/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go @@ -207,6 +207,7 @@ func TestJRPCRequestDurationLimiter_Limiting(t *testing.T) { ch := jhttp.NewChannel("http://"+addr+"/", nil) client := jrpc2.NewClient(ch, nil) + defer client.Close() var res interface{} req := struct { @@ -251,6 +252,7 @@ func TestJRPCRequestDurationLimiter_NoLimiting(t *testing.T) { ch := jhttp.NewChannel("http://"+addr+"/", nil) client := jrpc2.NewClient(ch, nil) + defer client.Close() var res interface{} req := struct { @@ -292,6 +294,7 @@ func TestJRPCRequestDurationLimiter_NoLimiting_Warn(t *testing.T) { ch := jhttp.NewChannel("http://"+addr+"/", nil) client := jrpc2.NewClient(ch, nil) + defer client.Close() var res interface{} req := struct { diff --git a/cmd/soroban-rpc/internal/test/archive_test.go b/cmd/soroban-rpc/internal/test/archive_test.go index 127e6e61..eaa4578e 100644 --- a/cmd/soroban-rpc/internal/test/archive_test.go +++ b/cmd/soroban-rpc/internal/test/archive_test.go @@ -1,7 +1,12 @@ package test import ( + "net" "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "strconv" "sync" "testing" @@ -9,17 +14,24 @@ import ( ) func TestArchiveUserAgent(t *testing.T) { + archiveHost := net.JoinHostPort("localhost", strconv.Itoa(StellarCoreArchivePort)) + proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: archiveHost}) userAgents := sync.Map{} + historyArchiveProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userAgents.Store(r.Header["User-Agent"][0], "") + proxy.ServeHTTP(w, r) + })) + defer historyArchiveProxy.Close() + cfg := &TestConfig{ - historyArchiveProxyCallback: func(r *http.Request) { - userAgents.Store(r.Header["User-Agent"][0], "") - }, + HistoryArchiveURL: historyArchiveProxy.URL, } + NewTest(t, cfg) - _, ok := userAgents.Load("testing") + _, ok := userAgents.Load("soroban-rpc/0.0.0") assert.True(t, ok, "rpc service should set user agent for history archives") - _, ok = userAgents.Load("testing/captivecore") + _, ok = userAgents.Load("soroban-rpc/0.0.0/captivecore") assert.True(t, ok, "rpc captive core should set user agent for history archives") } diff --git a/cmd/soroban-rpc/internal/test/docker-compose.rpc.yml b/cmd/soroban-rpc/internal/test/docker-compose.rpc.yml new file mode 100644 index 00000000..3443aff4 --- /dev/null +++ b/cmd/soroban-rpc/internal/test/docker-compose.rpc.yml @@ -0,0 +1,17 @@ +services: + rpc: + platform: linux/amd64 + image: stellar/soroban-rpc:${RPC_IMAGE_TAG} + depends_on: + - core + ports: + - "8000:8000" + - "8080:8080" + command: --config-path /soroban-rpc.config + volumes: + - ${RPC_CONFIG_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg + - ${RPC_CONFIG_MOUNT_DIR}/soroban-rpc.config:/soroban-rpc.config + - ${RPC_SQLITE_MOUNT_DIR}:/db/ + # Needed so that the sql database files created in the container + # have the same uid and gid as in the host + user: "${RPC_UID}:${RPC_GID}" diff --git a/cmd/soroban-rpc/internal/test/docker-compose.yml b/cmd/soroban-rpc/internal/test/docker-compose.yml index 4b246f7b..cf3e7b0d 100644 --- a/cmd/soroban-rpc/internal/test/docker-compose.yml +++ b/cmd/soroban-rpc/internal/test/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: core-postgres: image: postgres:9.6.17-alpine diff --git a/cmd/soroban-rpc/internal/test/get_fee_stats_test.go b/cmd/soroban-rpc/internal/test/get_fee_stats_test.go index b1de24e7..15969aea 100644 --- a/cmd/soroban-rpc/internal/test/get_fee_stats_test.go +++ b/cmd/soroban-rpc/internal/test/get_fee_stats_test.go @@ -4,8 +4,6 @@ import ( "context" "testing" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" @@ -18,9 +16,7 @@ import ( func TestGetFeeStats(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() account := txnbuild.NewSimpleAccount(address, 0) 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 50c0af9e..835f183f 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go +++ b/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,8 +19,7 @@ import ( func TestGetLedgerEntriesNotFound(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) @@ -58,8 +56,7 @@ func TestGetLedgerEntriesNotFound(t *testing.T) { func TestGetLedgerEntriesInvalidParams(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() var keys []string keys = append(keys, "<>@@#$") @@ -76,8 +73,7 @@ func TestGetLedgerEntriesInvalidParams(t *testing.T) { func TestGetLedgerEntriesSucceeds(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() 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 df606bfc..007dd0f2 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go +++ b/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,8 +19,7 @@ import ( func TestGetLedgerEntryNotFound(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) @@ -53,8 +51,7 @@ func TestGetLedgerEntryNotFound(t *testing.T) { func TestGetLedgerEntryInvalidParams(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() request := methods.GetLedgerEntryRequest{ Key: "<>@@#$", @@ -69,8 +66,7 @@ func TestGetLedgerEntryInvalidParams(t *testing.T) { func TestGetLedgerEntrySucceeds(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() kp := keypair.Root(StandaloneNetworkPassphrase) account := txnbuild.NewSimpleAccount(kp.Address(), 0) diff --git a/cmd/soroban-rpc/internal/test/get_network_test.go b/cmd/soroban-rpc/internal/test/get_network_test.go index dad90771..777a48e4 100644 --- a/cmd/soroban-rpc/internal/test/get_network_test.go +++ b/cmd/soroban-rpc/internal/test/get_network_test.go @@ -4,8 +4,6 @@ import ( "context" "testing" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stretchr/testify/assert" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" @@ -14,8 +12,7 @@ import ( func TestGetNetworkSucceeds(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() request := methods.GetNetworkRequest{} diff --git a/cmd/soroban-rpc/internal/test/get_transactions_test.go b/cmd/soroban-rpc/internal/test/get_transactions_test.go index f3da92dd..48371cea 100644 --- a/cmd/soroban-rpc/internal/test/get_transactions_test.go +++ b/cmd/soroban-rpc/internal/test/get_transactions_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" @@ -57,8 +56,7 @@ func sendTransactions(t *testing.T, client *jrpc2.Client) []uint32 { func TestGetTransactions(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() ledgers := sendTransactions(t, client) diff --git a/cmd/soroban-rpc/internal/test/get_version_info_test.go b/cmd/soroban-rpc/internal/test/get_version_info_test.go index 9f406b8d..25a43f62 100644 --- a/cmd/soroban-rpc/internal/test/get_version_info_test.go +++ b/cmd/soroban-rpc/internal/test/get_version_info_test.go @@ -3,12 +3,11 @@ package test import ( "context" "fmt" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" "os/exec" "testing" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" + "github.com/stretchr/testify/assert" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" @@ -28,8 +27,7 @@ func TestGetVersionInfoSucceeds(t *testing.T) { config.BuildTimestamp = buildTimeStamp }) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() var result methods.GetVersionInfoResponse err := client.CallResult(context.Background(), "getVersionInfo", nil, &result) diff --git a/cmd/soroban-rpc/internal/test/health_test.go b/cmd/soroban-rpc/internal/test/health_test.go index 0840959c..adae006c 100644 --- a/cmd/soroban-rpc/internal/test/health_test.go +++ b/cmd/soroban-rpc/internal/test/health_test.go @@ -4,8 +4,6 @@ import ( "context" "testing" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stretchr/testify/assert" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" @@ -15,8 +13,7 @@ import ( func TestHealth(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() var result methods.HealthCheckResult if err := client.CallResult(context.Background(), "getHealth", nil, &result); err != nil { diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/test/integration.go index 03e86237..531a162f 100644 --- a/cmd/soroban-rpc/internal/test/integration.go +++ b/cmd/soroban-rpc/internal/test/integration.go @@ -3,41 +3,39 @@ package test import ( "context" "fmt" - "net/http" - "net/http/httptest" - "net/http/httputil" - "net/url" "os" "os/exec" "os/signal" "path" "path/filepath" + "runtime" "strconv" + "strings" "sync" "syscall" "testing" "time" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" ) const ( StandaloneNetworkPassphrase = "Standalone Network ; February 2017" - maxSupportedProtocolVersion = 21 + MaxSupportedProtocolVersion = 21 + StellarCoreArchivePort = 1570 stellarCorePort = 11626 - stellarCoreArchiveHost = "localhost:1570" - goModFile = "go.mod" - - 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 @@ -46,50 +44,52 @@ const ( ) type TestConfig struct { - historyArchiveProxyCallback func(*http.Request) - ProtocolVersion uint32 + ProtocolVersion uint32 + // Run a previously released version of RPC (in a container) instead of the current version + UseReleasedRPCVersion string + UseSQLitePath string + HistoryArchiveURL string } type Test struct { t *testing.T - composePath string // docker compose yml file - protocolVersion uint32 - daemon *daemon.Daemon + historyArchiveURL string - historyArchiveProxy *httptest.Server - historyArchiveProxyCallback func(*http.Request) + rpcContainerVersion string + rpcContainerConfigMountDir string + rpcContainerSQLiteMountDir string + rpcContainerLogsCommand *exec.Cmd + rpcClient *jrpc2.Client coreClient *stellarcore.Client + daemon *daemon.Daemon + masterAccount txnbuild.Account shutdownOnce sync.Once - shutdownCalls []func() + shutdown func() } 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") } - coreBinaryPath := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - if coreBinaryPath == "" { - t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - } - - i := &Test{ - t: t, - composePath: findDockerComposePath(), - } + i := &Test{t: t} i.masterAccount = &txnbuild.SimpleAccount{ AccountID: i.MasterKey().Address(), Sequence: 0, } + + sqlLitePath := "" if cfg != nil { - i.historyArchiveProxyCallback = cfg.historyArchiveProxyCallback + i.historyArchiveURL = cfg.HistoryArchiveURL + i.rpcContainerVersion = cfg.UseReleasedRPCVersion i.protocolVersion = cfg.ProtocolVersion + sqlLitePath = cfg.UseSQLitePath } if i.protocolVersion == 0 { @@ -97,25 +97,37 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.protocolVersion = GetCoreMaxSupportedProtocol() } - 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) - })) - + rpcCfg := i.getRPConfig(sqlLitePath) + if i.runRPCInContainer() { + i.rpcContainerConfigMountDir = i.createRPCContainerMountDir(rpcCfg) + } i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") + if i.runRPCInContainer() { + cmd := i.getComposeCommand("logs", "-f", "rpc") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Start()) + } i.prepareShutdownHandlers() i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} i.waitForCore() i.waitForCheckpoint() - i.launchDaemon(coreBinaryPath) + if !i.runRPCInContainer() { + i.daemon = i.createDaemon(rpcCfg) + go i.daemon.Run() + } + i.waitForRPC() return i } +func (i *Test) runRPCInContainer() bool { + return i.rpcContainerVersion != "" +} + +func (i *Test) GetRPCLient() *jrpc2.Client { + return i.rpcClient +} func (i *Test) MasterKey() *keypair.Full { return keypair.Root(StandaloneNetworkPassphrase) } @@ -153,83 +165,175 @@ func (i *Test) waitForCheckpoint() { i.t.Fatal("Core could not reach checkpoint ledger after 30s") } -func (i *Test) launchDaemon(coreBinaryPath string) { - var config config.Config - cmd := &cobra.Command{} - if err := config.AddFlags(cmd); err != nil { - i.t.FailNow() - } - if err := config.SetValues(func(string) (string, bool) { return "", false }); err != nil { - i.t.FailNow() - } - - config.Endpoint = fmt.Sprintf("localhost:%d", sorobanRPCPort) - config.AdminEndpoint = fmt.Sprintf("localhost:%d", adminPort) - config.StellarCoreURL = "http://localhost:" + strconv.Itoa(stellarCorePort) - config.CoreRequestTimeout = time.Second * 2 - config.StellarCoreBinaryPath = coreBinaryPath - config.CaptiveCoreConfigPath = path.Join(i.composePath, "captive-core-integration-tests.cfg") - config.CaptiveCoreStoragePath = i.t.TempDir() - config.CaptiveCoreHTTPPort = 0 - config.FriendbotURL = friendbotURL - config.NetworkPassphrase = StandaloneNetworkPassphrase - 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 - config.EventLedgerRetentionWindow = ledgerbucketwindow.OneDayOfLedgers - config.TransactionLedgerRetentionWindow = ledgerbucketwindow.OneDayOfLedgers - config.CheckpointFrequency = checkpointFrequency - config.MaxHealthyLedgerLatency = time.Second * 10 - config.PreflightEnableDebug = true - config.HistoryArchiveUserAgent = "testing" - - i.daemon = daemon.MustNew(&config) - go i.daemon.Run() - - // wait for the storage to catch up for 1 minute - info, err := i.coreClient.Info(context.Background()) - if err != nil { - i.t.Fatalf("cannot obtain latest ledger from core: %v", err) +func (i *Test) getRPConfig(sqlitePath string) map[string]string { + if sqlitePath == "" { + sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") } - targetLedgerSequence := uint32(info.Info.Ledger.Num) - reader := db.NewLedgerEntryReader(i.daemon.GetDB()) - success := false - for t := 30; t >= 0; t -= 1 { - sequence, err := reader.GetLatestLedgerSequence(context.Background()) - if err != nil { - if err != db.ErrEmptyDB { - i.t.Fatalf("cannot access ledger entry storage: %v", err) - } - } else { - if sequence >= targetLedgerSequence { - success = true - break + // Container's default path to captive core + coreBinaryPath := "/usr/bin/stellar-core" + if !i.runRPCInContainer() { + coreBinaryPath = os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + if coreBinaryPath == "" { + i.t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + } + } + + archiveURL := fmt.Sprintf("http://localhost:%d", StellarCoreArchivePort) + if i.runRPCInContainer() { + // the archive needs to be accessed from the container + // where core is Core's hostname + archiveURL = fmt.Sprintf("http://core:%d", StellarCoreArchivePort) + } + if i.historyArchiveURL != "" { + // an archive URL was supplied explicitly + archiveURL = i.historyArchiveURL + } + + captiveCoreConfigPath := path.Join(GetCurrentDirectory(), "captive-core-integration-tests.cfg") + bindHost := "localhost" + stellarCoreURL := fmt.Sprintf("http://localhost:%d", stellarCorePort) + if i.runRPCInContainer() { + // The file will be inside the container + captiveCoreConfigPath = "/stellar-core.cfg" + // The container needs to listen on all interfaces, not just localhost + bindHost = "0.0.0.0" + // The container needs to use the sqlite mount point + i.rpcContainerSQLiteMountDir = filepath.Dir(sqlitePath) + sqlitePath = "/db/" + filepath.Base(sqlitePath) + stellarCoreURL = fmt.Sprintf("http://core:%d", stellarCorePort) + } + + // in the container + captiveCoreStoragePath := "/tmp/captive-core" + if !i.runRPCInContainer() { + captiveCoreStoragePath = i.t.TempDir() + } + + return map[string]string{ + "ENDPOINT": fmt.Sprintf("%s:%d", bindHost, sorobanRPCPort), + "ADMIN_ENDPOINT": fmt.Sprintf("%s:%d", bindHost, adminPort), + "STELLAR_CORE_URL": stellarCoreURL, + "CORE_REQUEST_TIMEOUT": "2s", + "STELLAR_CORE_BINARY_PATH": coreBinaryPath, + "CAPTIVE_CORE_CONFIG_PATH": captiveCoreConfigPath, + "CAPTIVE_CORE_STORAGE_PATH": captiveCoreStoragePath, + "STELLAR_CAPTIVE_CORE_HTTP_PORT": "0", + "FRIENDBOT_URL": friendbotURL, + "NETWORK_PASSPHRASE": StandaloneNetworkPassphrase, + "HISTORY_ARCHIVE_URLS": archiveURL, + "LOG_LEVEL": "debug", + "DB_PATH": sqlitePath, + "INGESTION_TIMEOUT": "10m", + "EVENT_LEDGER_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), + "TRANSACTION_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), + "CHECKPOINT_FREQUENCY": strconv.Itoa(checkpointFrequency), + "MAX_HEALTHY_LEDGER_LATENCY": "10s", + "PREFLIGHT_ENABLE_DEBUG": "true", + } +} + +func (i *Test) waitForRPC() { + i.t.Log("Waiting for RPC to be healthy...") + + // This is needed because if https://github.com/creachadair/jrpc2/issues/118 + refreshClient := func() { + if i.rpcClient != nil { + i.rpcClient.Close() + } + ch := jhttp.NewChannel(i.sorobanRPCURL(), nil) + i.rpcClient = jrpc2.NewClient(ch, nil) + } + + var result methods.HealthCheckResult + for t := 30; t >= 0; t-- { + refreshClient() + err := i.rpcClient.CallResult(context.Background(), "getHealth", nil, &result) + if err == nil { + if result.Status == "healthy" { + i.t.Log("RPC is healthy") + return } } + i.t.Log("RPC still unhealthy", err, result.Status) time.Sleep(time.Second) } - if !success { - i.t.Fatal("LedgerEntryStorage failed to sync in 1 minute") + + i.t.Fatal("RPC failed to get healthy in 30 seconds") +} + +func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { + mountDir := i.t.TempDir() + // Get old version of captive-core-integration-tests.cfg + cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./captive-core-integration-tests.cfg", i.rpcContainerVersion)) + cmd.Dir = GetCurrentDirectory() + out, err := cmd.Output() + require.NoError(i.t, err) + + // replace ADDRESS="localhost" by ADDRESS="core", so that the container can find core + captiveCoreCfgContents := strings.Replace(string(out), `ADDRESS="localhost"`, `ADDRESS="core"`, -1) + err = os.WriteFile(filepath.Join(mountDir, "stellar-core-integration-tests.cfg"), []byte(captiveCoreCfgContents), 0666) + require.NoError(i.t, err) + + // Generate config file + cfgFileContents := "" + for k, v := range rpcConfig { + cfgFileContents += fmt.Sprintf("%s=%q\n", k, v) } + err = os.WriteFile(filepath.Join(mountDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) + require.NoError(i.t, err) + + return mountDir } -// Runs a docker-compose command applied to the above configs -func (i *Test) runComposeCommand(args ...string) { - integrationYaml := filepath.Join(i.composePath, "docker-compose.yml") +func (i *Test) createDaemon(env map[string]string) *daemon.Daemon { + var cfg config.Config + lookup := func(s string) (string, bool) { + ret, ok := env[s] + return ret, ok + } + require.NoError(i.t, cfg.SetValues(lookup)) + require.NoError(i.t, cfg.Validate()) + cfg.HistoryArchiveUserAgent = fmt.Sprintf("soroban-rpc/%s", config.Version) + return daemon.MustNew(&cfg) +} - cmdline := append([]string{"-f", integrationYaml}, args...) +func (i *Test) getComposeCommand(args ...string) *exec.Cmd { + integrationYaml := filepath.Join(GetCurrentDirectory(), "docker-compose.yml") + configFiles := []string{"-f", integrationYaml} + if i.runRPCInContainer() { + rpcYaml := filepath.Join(GetCurrentDirectory(), "docker-compose.rpc.yml") + configFiles = append(configFiles, "-f", rpcYaml) + } + cmdline := append(configFiles, args...) cmd := exec.Command("docker-compose", cmdline...) if img := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_DOCKER_IMG"); img != "" { - cmd.Env = os.Environ() cmd.Env = append( - cmd.Environ(), - fmt.Sprintf("CORE_IMAGE=%s", img), + cmd.Env, + "CORE_IMAGE="+img, ) } - i.t.Log("Running", cmd.Env, cmd.Args) + if i.runRPCInContainer() { + cmd.Env = append( + cmd.Env, + "RPC_IMAGE_TAG="+i.rpcContainerVersion, + "RPC_CONFIG_MOUNT_DIR="+i.rpcContainerConfigMountDir, + "RPC_SQLITE_MOUNT_DIR="+i.rpcContainerSQLiteMountDir, + "RPC_UID="+strconv.Itoa(os.Getuid()), + "RPC_GID="+strconv.Itoa(os.Getgid()), + ) + } + if len(cmd.Env) > 0 { + cmd.Env = append(cmd.Env, os.Environ()...) + } + return cmd +} + +// Runs a docker-compose command applied to the above configs +func (i *Test) runComposeCommand(args ...string) { + cmd := i.getComposeCommand(args...) + i.t.Log("Running", cmd.Args) out, innerErr := cmd.Output() if exitErr, ok := innerErr.(*exec.ExitError); ok { fmt.Printf("stdout:\n%s\n", string(out)) @@ -242,28 +346,32 @@ func (i *Test) runComposeCommand(args ...string) { } func (i *Test) prepareShutdownHandlers() { - i.shutdownCalls = append(i.shutdownCalls, - func() { - if i.daemon != nil { - i.daemon.Close() - } - if i.historyArchiveProxy != nil { - i.historyArchiveProxy.Close() - } - i.runComposeCommand("down", "-v") - }, - ) + done := make(chan struct{}) + i.shutdown = func() { + close(done) + i.StopRPC() + if i.rpcClient != nil { + i.rpcClient.Close() + } + i.runComposeCommand("down", "-v") + if i.rpcContainerLogsCommand != nil { + i.rpcContainerLogsCommand.Wait() + } + } - // Register cleanup handlers (on panic and ctrl+c) so the containers are + // Register shutdown handlers (on panic and ctrl+c) so the containers are // stopped even if ingestion or testing fails. i.t.Cleanup(i.Shutdown) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { - <-c - i.Shutdown() - os.Exit(int(syscall.SIGTERM)) + select { + case <-c: + i.Shutdown() + os.Exit(int(syscall.SIGTERM)) + case <-done: + } }() } @@ -273,10 +381,7 @@ func (i *Test) prepareShutdownHandlers() { // called before. func (i *Test) Shutdown() { i.shutdownOnce.Do(func() { - // run them in the opposite order in which they where added - for callI := len(i.shutdownCalls) - 1; callI >= 0; callI-- { - i.shutdownCalls[callI]() - } + i.shutdown() }) } @@ -288,7 +393,7 @@ func (i *Test) waitForCore() { _, err := i.coreClient.Info(ctx) cancel() if err != nil { - i.t.Logf("could not obtain info response: %v", err) + i.t.Logf("Core is not up: %v", err) time.Sleep(time.Second) continue } @@ -341,75 +446,30 @@ func (i *Test) UpgradeProtocol(version uint32) { i.t.Fatalf("could not upgrade protocol in 10s") } -// Cluttering code with if err != nil is absolute nonsense. -func panicIf(err error) { - if err != nil { - panic(err) - } -} - -// findProjectRoot iterates upward on the directory until go.mod file is found. -func findProjectRoot(current string) string { - // Lets you check if a particular directory contains a file. - directoryContainsFilename := func(dir string, filename string) bool { - files, innerErr := os.ReadDir(dir) - panicIf(innerErr) - - for _, file := range files { - if file.Name() == filename { - return true - } - } - return false +func (i *Test) StopRPC() { + if i.daemon != nil { + i.daemon.Close() + i.daemon = nil } - var err error - - // In either case, we try to walk up the tree until we find "go.mod", - // which we hope is the root directory of the project. - for !directoryContainsFilename(current, goModFile) { - current, err = filepath.Abs(filepath.Join(current, "..")) - - // FIXME: This only works on *nix-like systems. - if err != nil || filepath.Base(current)[0] == filepath.Separator { - fmt.Println("Failed to establish project root directory.") - panic(err) - } + if i.runRPCInContainer() { + i.runComposeCommand("down", "rpc", "-v") } - return current } -// findDockerComposePath performs a best-effort attempt to find the project's -// Docker Compose files. -func findDockerComposePath() string { - current, err := os.Getwd() - panicIf(err) - - // - // We have a primary and backup attempt for finding the necessary docker - // files: via $GOPATH and via local directory traversal. - // - - if gopath := os.Getenv("GOPATH"); gopath != "" { - monorepo := filepath.Join(gopath, "src", "github.com", "stellar", "soroban-rpc") - if _, err = os.Stat(monorepo); !os.IsNotExist(err) { - current = monorepo - } - } - - current = findProjectRoot(current) - - // Directly jump down to the folder that should contain the configs - return filepath.Join(current, "cmd", "soroban-rpc", "internal", "test") +//go:noinline +func GetCurrentDirectory() string { + _, currentFilename, _, _ := runtime.Caller(1) + return filepath.Dir(currentFilename) } func GetCoreMaxSupportedProtocol() uint32 { str := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") if str == "" { - return maxSupportedProtocolVersion + return MaxSupportedProtocolVersion } version, err := strconv.ParseUint(str, 10, 32) if err != nil { - return maxSupportedProtocolVersion + return MaxSupportedProtocolVersion } return uint32(version) diff --git a/cmd/soroban-rpc/internal/test/integration_test.go b/cmd/soroban-rpc/internal/test/integration_test.go deleted file mode 100644 index 684a61ad..00000000 --- a/cmd/soroban-rpc/internal/test/integration_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package test - -import ( - "fmt" - "testing" -) - -func TestFindDockerComposePath(t *testing.T) { - dockerPath := findDockerComposePath() - - if len(dockerPath) == 0 { - t.Fail() - } - fmt.Printf("docker compose path is %s\n", dockerPath) -} diff --git a/cmd/soroban-rpc/internal/test/metrics_test.go b/cmd/soroban-rpc/internal/test/metrics_test.go index 9bf63a48..57211ad1 100644 --- a/cmd/soroban-rpc/internal/test/metrics_test.go +++ b/cmd/soroban-rpc/internal/test/metrics_test.go @@ -2,15 +2,16 @@ package test import ( "fmt" - io_prometheus_client "github.com/prometheus/client_model/go" - "github.com/stellar/go/support/errors" - "github.com/stretchr/testify/assert" "io" "net/http" "net/url" "runtime" "testing" + io_prometheus_client "github.com/prometheus/client_model/go" + "github.com/stellar/go/support/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" diff --git a/cmd/soroban-rpc/internal/test/migrate_test.go b/cmd/soroban-rpc/internal/test/migrate_test.go new file mode 100644 index 00000000..c8fcc7ec --- /dev/null +++ b/cmd/soroban-rpc/internal/test/migrate_test.go @@ -0,0 +1,116 @@ +package test + +import ( + "context" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" +) + +// Test that every Soroban RPC version (within the current protocol) can migrate cleanly to the current version +// We cannot test prior protocol versions since the Transaction XDR used for the test could be incompatible +// TODO: find a way to test migrations between protocols +func TestMigrate(t *testing.T) { + if GetCoreMaxSupportedProtocol() != MaxSupportedProtocolVersion { + t.Skip("Only test this for the latest protocol: ", MaxSupportedProtocolVersion) + } + for _, originVersion := range getCurrentProtocolReleasedVersions(t) { + if originVersion == "21.1.0" { + // This version of the RPC container fails to even start with its captive core companion file + // (it fails Invalid configuration: DEPRECATED_SQL_LEDGER_STATE not set.) + continue + } + if originVersion == "21.3.0" { + // This version of RPC wasn't published as a docker container + continue + } + t.Run(originVersion, func(t *testing.T) { + testMigrateFromVersion(t, originVersion) + }) + } +} + +func testMigrateFromVersion(t *testing.T, version string) { + sqliteFile := filepath.Join(t.TempDir(), "soroban-rpc.db") + it := NewTest(t, &TestConfig{ + UseReleasedRPCVersion: version, + UseSQLitePath: sqliteFile, + }) + + client := it.GetRPCLient() + + // Submit an event-logging transaction in the version to migrate from + kp := keypair.Root(StandaloneNetworkPassphrase) + address := kp.Address() + account := txnbuild.NewSimpleAccount(address, 0) + + contractBinary := getHelloWorldContract(t) + params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + createInstallContractCodeOperation(account.AccountID, contractBinary), + }, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + }) + tx, err := txnbuild.NewTransaction(params) + assert.NoError(t, err) + submitTransactionResponse := sendSuccessfulTransaction(t, client, kp, tx) + + // Run the current RPC version, but the previous network and sql database (causing a data migration if needed) + it.StopRPC() + it = NewTest(t, &TestConfig{UseSQLitePath: sqliteFile}) + + // make sure that the transaction submitted before and its events exist in current RPC + var transactionsResult methods.GetTransactionsResponse + getTransactions := methods.GetTransactionsRequest{ + StartLedger: submitTransactionResponse.Ledger, + Pagination: &methods.TransactionsPaginationOptions{ + Limit: 1, + }, + } + err = client.CallResult(context.Background(), "getTransactions", getTransactions, &transactionsResult) + require.NoError(t, err) + require.Equal(t, 1, len(transactionsResult.Transactions)) + require.Equal(t, submitTransactionResponse.Ledger, transactionsResult.Transactions[0].Ledger) + + var eventsResult methods.GetEventsResponse + getEventsRequest := methods.GetEventsRequest{ + StartLedger: submitTransactionResponse.Ledger, + Pagination: &methods.PaginationOptions{ + Limit: 1, + }, + } + err = client.CallResult(context.Background(), "getEvents", getEventsRequest, &eventsResult) + require.NoError(t, err) + require.Equal(t, len(eventsResult.Events), 1) + require.Equal(t, submitTransactionResponse.Ledger, uint32(eventsResult.Events[0].Ledger)) +} + +func getCurrentProtocolReleasedVersions(t *testing.T) []string { + protocolStr := strconv.Itoa(MaxSupportedProtocolVersion) + cmd := exec.Command("git", "tag") + cmd.Dir = GetCurrentDirectory() + out, err := cmd.Output() + require.NoError(t, err) + tags := strings.Split(string(out), "\n") + filteredTags := make([]string, 0, len(tags)) + for _, tag := range tags { + if strings.HasPrefix(tag, "v"+protocolStr) { + filteredTags = append(filteredTags, tag[1:]) + } + } + return filteredTags +} diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index 65c5767d..642c7937 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -124,14 +123,14 @@ func simulateTransactionFromTxParams(t *testing.T, client *jrpc2.Client, params savedAutoIncrement := params.IncrementSequenceNum params.IncrementSequenceNum = false tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) + require.NoError(t, err) params.IncrementSequenceNum = savedAutoIncrement txB64, err := tx.Base64() - assert.NoError(t, err) + require.NoError(t, err) request := methods.SimulateTransactionRequest{Transaction: txB64} var response methods.SimulateTransactionResponse err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - assert.NoError(t, err) + require.NoError(t, err) return response } @@ -191,9 +190,7 @@ func preflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbu func TestSimulateTransactionSucceeds(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() contractBinary := getHelloWorldContract(t) params := txnbuild.TransactionParams{ @@ -323,8 +320,7 @@ func TestSimulateTransactionSucceeds(t *testing.T) { func TestSimulateTransactionWithAuth(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() @@ -381,8 +377,7 @@ func TestSimulateTransactionWithAuth(t *testing.T) { func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() @@ -565,8 +560,7 @@ func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { func TestSimulateTransactionError(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() invokeHostOp := createInvokeHostOperation(sourceAccount, xdr.Hash{}, "noMethod") @@ -605,8 +599,7 @@ func TestSimulateTransactionError(t *testing.T) { func TestSimulateTransactionMultipleOperations(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() contractBinary := getHelloWorldContract(t) @@ -640,8 +633,7 @@ func TestSimulateTransactionMultipleOperations(t *testing.T) { func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() params := txnbuild.TransactionParams{ SourceAccount: &txnbuild.SimpleAccount{ @@ -671,8 +663,7 @@ func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { func TestSimulateTransactionUnmarshalError(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() request := methods.SimulateTransactionRequest{Transaction: "invalid"} var result methods.SimulateTransactionResponse @@ -688,8 +679,7 @@ func TestSimulateTransactionUnmarshalError(t *testing.T) { func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() @@ -921,8 +911,7 @@ func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.L func TestSimulateInvokePrng_u64_in_range(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() @@ -1032,8 +1021,7 @@ func TestSimulateInvokePrng_u64_in_range(t *testing.T) { func TestSimulateSystemEvent(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address() diff --git a/cmd/soroban-rpc/internal/test/transaction_test.go b/cmd/soroban-rpc/internal/test/transaction_test.go index f838ad6b..4a8461f6 100644 --- a/cmd/soroban-rpc/internal/test/transaction_test.go +++ b/cmd/soroban-rpc/internal/test/transaction_test.go @@ -8,7 +8,6 @@ import ( "time" "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,8 +22,7 @@ import ( func TestSendTransactionSucceedsWithoutResults(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() kp := keypair.Root(StandaloneNetworkPassphrase) address := kp.Address() @@ -48,8 +46,7 @@ func TestSendTransactionSucceedsWithoutResults(t *testing.T) { func TestSendTransactionSucceedsWithResults(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() kp := keypair.Root(StandaloneNetworkPassphrase) address := kp.Address() @@ -112,8 +109,7 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { func TestSendTransactionBadSequence(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() kp := keypair.Root(StandaloneNetworkPassphrase) address := kp.Address() @@ -154,8 +150,7 @@ func TestSendTransactionBadSequence(t *testing.T) { func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() kp := keypair.Root(StandaloneNetworkPassphrase) address := kp.Address() @@ -206,8 +201,7 @@ func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { func TestSendTransactionFailedInLedger(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() kp := keypair.Root(StandaloneNetworkPassphrase) address := kp.Address() @@ -268,8 +262,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { func TestSendTransactionFailedInvalidXDR(t *testing.T) { test := NewTest(t, nil) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() request := methods.SendTransactionRequest{Transaction: "abcdef"} var response methods.SendTransactionResponse diff --git a/cmd/soroban-rpc/internal/test/upgrade_test.go b/cmd/soroban-rpc/internal/test/upgrade_test.go index 6889e614..6a59cefd 100644 --- a/cmd/soroban-rpc/internal/test/upgrade_test.go +++ b/cmd/soroban-rpc/internal/test/upgrade_test.go @@ -5,8 +5,6 @@ import ( "testing" "time" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" @@ -24,8 +22,7 @@ func TestUpgradeFrom20To21(t *testing.T) { ProtocolVersion: 20, }) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) + client := test.GetRPCLient() sourceAccount := keypair.Root(StandaloneNetworkPassphrase) address := sourceAccount.Address()