diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 39685927..49800ce5 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -3,6 +3,7 @@ package daemon import ( "context" "errors" + "net" "net/http" "net/http/pprof" //nolint:gosec "os" @@ -50,7 +51,9 @@ type Daemon struct { jsonRPCHandler *internal.Handler logger *supportlog.Entry preflightWorkerPool *preflight.PreflightWorkerPool + listener net.Listener server *http.Server + adminListener net.Listener adminServer *http.Server closeOnce sync.Once closeError error @@ -62,6 +65,15 @@ func (d *Daemon) GetDB() *db.DB { return d.db } +func (d *Daemon) GetEndpointAddrs() (net.TCPAddr, *net.TCPAddr) { + var addr = d.listener.Addr().(*net.TCPAddr) + var adminAddr *net.TCPAddr + if d.adminListener != nil { + adminAddr = d.adminListener.Addr().(*net.TCPAddr) + } + return *addr, adminAddr +} + func (d *Daemon) close() { shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), defaultShutdownGracePeriod) defer shutdownRelease() @@ -251,8 +263,14 @@ func MustNew(cfg *config.Config) *Daemon { daemon.ingestService = ingestService daemon.jsonRPCHandler = &jsonRPCHandler + // Use a separate listener in order to obtain the actual TCP port + // when using dynamic ports during testing (e.g. endpoint="localhost:0") + daemon.listener, err = net.Listen("tcp", cfg.Endpoint) + if err != nil { + daemon.logger.WithError(err).WithField("endpoint", cfg.Endpoint).Fatal("cannot listen on endpoint") + } daemon.server = &http.Server{ - Addr: cfg.Endpoint, + Addr: ":http", Handler: httpHandler, ReadTimeout: defaultReadTimeout, } @@ -269,7 +287,11 @@ func MustNew(cfg *config.Config) *Daemon { adminMux.Handle("/debug/pprof/"+profile.Name(), pprof.Handler(profile.Name())) } adminMux.Handle("/metrics", promhttp.HandlerFor(metricsRegistry, promhttp.HandlerOpts{})) - daemon.adminServer = &http.Server{Addr: cfg.AdminEndpoint, Handler: adminMux} + daemon.adminListener, err = net.Listen("tcp", cfg.AdminEndpoint) + if err != nil { + daemon.logger.WithError(err).WithField("endpoint", cfg.Endpoint).Fatal("cannot listen on admin endpoint") + } + daemon.adminServer = &http.Server{Handler: adminMux} } daemon.registerMetrics() return daemon @@ -340,20 +362,22 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) (*feewindow.FeeWindow func (d *Daemon) Run() { d.logger.WithFields(supportlog.F{ - "addr": d.server.Addr, + "addr": d.listener.Addr().String(), }).Info("starting HTTP server") panicGroup := util.UnrecoverablePanicGroup.Log(d.logger) panicGroup.Go(func() { - if err := d.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - // Error starting or closing listener: + if err := d.server.Serve(d.listener); !errors.Is(err, http.ErrServerClosed) { d.logger.WithError(err).Fatal("soroban JSON RPC server encountered fatal error") } }) if d.adminServer != nil { + d.logger.WithFields(supportlog.F{ + "addr": d.adminListener.Addr().String(), + }).Info("starting Admin HTTP server") panicGroup.Go(func() { - if err := d.adminServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + if err := d.adminServer.Serve(d.adminListener); !errors.Is(err, http.ErrServerClosed) { d.logger.WithError(err).Error("soroban admin server encountered fatal error") } }) diff --git a/cmd/soroban-rpc/internal/integrationtest/archive_test.go b/cmd/soroban-rpc/internal/integrationtest/archive_test.go index 9da7805f..57179284 100644 --- a/cmd/soroban-rpc/internal/integrationtest/archive_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/archive_test.go @@ -4,38 +4,55 @@ import ( "net" "net/http" "net/http/httptest" - "net/http/httputil" - "net/url" - "strconv" "sync" "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" ) func TestArchiveUserAgent(t *testing.T) { - ports := infrastructure.NewTestPorts(t) - archiveHost := net.JoinHostPort("localhost", strconv.Itoa(int(ports.CoreArchivePort))) - 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) + historyArchive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agent := r.Header["User-Agent"][0] + t.Log("agent", agent) + userAgents.Store(agent, "") + if r.URL.Path == "/.well-known/stellar-history.json" || r.URL.Path == "/history/00/00/00/history-0000001f.json" { + w.Write([]byte(`{ + "version": 1, + "server": "stellar-core 21.0.1 (dfd3dbff1d9cad4dc31e022de6ac2db731b4b326)", + "currentLedger": 31, + "networkPassphrase": "Standalone Network ; February 2017", + "currentBuckets": [] +}`)) + return + } + // emulate a problem with the archive + w.WriteHeader(http.StatusInternalServerError) })) - defer historyArchiveProxy.Close() + defer historyArchive.Close() + historyPort := historyArchive.Listener.Addr().(*net.TCPAddr).Port cfg := &infrastructure.TestConfig{ - TestPorts: &ports, - HistoryArchiveURL: historyArchiveProxy.URL, + OnlyRPC: &infrastructure.TestOnlyRPCConfig{ + CorePorts: infrastructure.TestCorePorts{ + CoreArchivePort: uint16(historyPort), + }, + DontWait: true, + }, } infrastructure.NewTest(t, cfg) - _, ok := userAgents.Load("soroban-rpc/0.0.0") - assert.True(t, ok, "rpc service should set user agent for history archives") - - _, ok = userAgents.Load("soroban-rpc/0.0.0/captivecore") - assert.True(t, ok, "rpc captive core should set user agent for history archives") + require.Eventually(t, + func() bool { + _, ok1 := userAgents.Load("soroban-rpc/0.0.0") + _, ok2 := userAgents.Load("soroban-rpc/0.0.0/captivecore") + return ok1 && ok2 + }, + 5*time.Second, + time.Second, + ) } diff --git a/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go index 90869bfa..6af5954f 100644 --- a/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/creachadair/jrpc2" "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" @@ -33,7 +32,7 @@ func buildSetOptionsTxParams(account txnbuild.SimpleAccount) txnbuild.Transactio // client - the JSON-RPC client used to send the transactions. // // Returns a slice of ledger numbers corresponding to where each transaction was recorded. -func sendTransactions(t *testing.T, client *jrpc2.Client) []uint32 { +func sendTransactions(t *testing.T, client *infrastructure.Client) []uint32 { kp := keypair.Root(infrastructure.StandaloneNetworkPassphrase) address := kp.Address() diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go index 1f536b43..d3e5b48b 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -7,6 +7,7 @@ import ( "time" "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/txnbuild" @@ -17,7 +18,41 @@ import ( "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) -func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.GetTransactionResponse { +// Client is a jrpc2 client which tolerates errors +type Client struct { + url string + cli *jrpc2.Client + opts *jrpc2.ClientOptions +} + +func NewClient(url string, opts *jrpc2.ClientOptions) *Client { + c := &Client{url: url, opts: opts} + c.refreshClient() + return c +} + +func (c *Client) refreshClient() { + if c.cli != nil { + c.cli.Close() + } + ch := jhttp.NewChannel(c.url, nil) + c.cli = jrpc2.NewClient(ch, c.opts) +} + +func (c *Client) CallResult(ctx context.Context, method string, params, result any) error { + err := c.cli.CallResult(ctx, method, params, result) + if err != nil { + // This is needed because of https://github.com/creachadair/jrpc2/issues/118 + c.refreshClient() + } + return err +} + +func (c *Client) Close() error { + return c.cli.Close() +} + +func getTransaction(t *testing.T, client *Client, hash string) methods.GetTransactionResponse { var result methods.GetTransactionResponse for i := 0; i < 60; i++ { request := methods.GetTransactionRequest{Hash: hash} @@ -35,7 +70,7 @@ func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.Get return result } -func SendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { +func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) assert.NoError(t, err) b64, err := tx.Base64() @@ -92,7 +127,7 @@ func SendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.F return response } -func SimulateTransactionFromTxParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { +func SimulateTransactionFromTxParams(t *testing.T, client *Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { savedAutoIncrement := params.IncrementSequenceNum params.IncrementSequenceNum = false tx, err := txnbuild.NewTransaction(params) @@ -153,7 +188,7 @@ func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.Transaction return params } -func PreflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { +func PreflightTransactionParams(t *testing.T, client *Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { response := SimulateTransactionFromTxParams(t, client, params) // The preamble should be zero except for the special restore case assert.Nil(t, response.RestorePreamble) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go index 7fa49d90..4daa1427 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go @@ -15,8 +15,7 @@ import ( var testSalt = sha256.Sum256([]byte("a1")) func GetHelloWorldContract() []byte { - testDirName := GetCurrentDirectory() - contractFile := path.Join(testDirName, helloWorldContractPath) + contractFile := path.Join(GetCurrentDirectory(), "../../../../../wasms/test_hello_world.wasm") ret, err := os.ReadFile(contractFile) if err != nil { str := fmt.Sprintf( diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl index 8b87c65c..670010c7 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl @@ -1,4 +1,8 @@ -PEER_PORT=${CORE_CAPTIVE_PORT} +# To fill in and use by RPC + +# This simply needs to be an unconflicting, unused port +# since captive core doesn't expect external connections +PEER_PORT=${CAPTIVE_CORE_PORT} ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true @@ -14,5 +18,7 @@ NAME="local_core" HOME_DOMAIN="core.local" # From "SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" -ADDRESS="localhost:${CORE_PORT}" + +# should be "core" when running RPC in a container or "localhost:port" when running RPC in the host +ADDRESS="${CORE_HOST_PORT}" QUALITY="MEDIUM" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh similarity index 64% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh index 75fd9d56..550ad1eb 100755 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh @@ -12,17 +12,20 @@ fi echo "using config:" cat stellar-core.cfg -# initialize new db -stellar-core new-db +# initialize new db (retry a few times to wait for the database to be available) +until stellar-core new-db; do + sleep 0.2 + echo "couldn't create new db, retrying" +done if [ "$1" = "standalone" ]; then # initialize for new history archive path, remove any pre-existing on same path from base image rm -rf ./history stellar-core new-hist vs - # serve history archives to horizon on port CORE_ARCHIVE_PORT + # serve history archives to horizon on port 1570 pushd ./history/vs/ - python3 -m http.server ${CORE_ARCHIVE_PORT} & + python3 -m http.server 1570 & popd fi diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml index 036eefaf..6a0992d4 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml @@ -1,15 +1,19 @@ +include: + - docker-compose.yml services: rpc: platform: linux/amd64 image: stellar/soroban-rpc:${RPC_IMAGE_TAG} depends_on: - core - ports: - - ${RPC_PORT}:${RPC_PORT} - - ${RPC_ADMIN_PORT}:${RPC_ADMIN_PORT} + ports: # we omit the host-side ports to allocate them dynamically + # HTTP + - "127.0.0.1::8000" + # Admin HTTP + - "127.0.0.1::8080" command: --config-path /soroban-rpc.config volumes: - - ${RPC_CONFIG_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg + - ${RPC_CONFIG_MOUNT_DIR}/captive-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 diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml index 212ded76..579cf67b 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml @@ -1,7 +1,6 @@ services: core-postgres: image: postgres:9.6.17-alpine - restart: on-failure environment: - POSTGRES_PASSWORD=mysecretpassword - POSTGRES_DB=stellar @@ -17,18 +16,19 @@ services: image: ${CORE_IMAGE:-stellar/unsafe-stellar-core:21.0.1-1897.dfd3dbff1.focal} depends_on: - core-postgres - restart: on-failure environment: - TRACY_NO_INVARIANT_CHECK=1 - ports: - - ${CORE_PORT}:${CORE_PORT} - - ${CORE_HTTP_PORT}:${CORE_HTTP_PORT} - # add extra port for history archive server - - ${CORE_ARCHIVE_PORT}:${CORE_ARCHIVE_PORT} + ports: # we omit the host-side ports to allocate them dynamically + # peer + - "127.0.0.1:0:11625" + # http + - "127.0.0.1:0:11626" + # history archive + - "127.0.0.1:0:1570" entrypoint: /usr/bin/env command: /start standalone volumes: - - ${CORE_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg - - ${CORE_MOUNT_DIR}/core-start.sh:/start + - ./stellar-core-integration-tests.cfg:/stellar-core.cfg + - ./core-start.sh:/start extra_hosts: - "host.docker.internal:host-gateway" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg similarity index 94% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg index c40599e3..5462dbfb 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg @@ -4,8 +4,8 @@ DEPRECATED_SQL_LEDGER_STATE=false NETWORK_PASSPHRASE="Standalone Network ; February 2017" -PEER_PORT=${CORE_PORT} -HTTP_PORT=${CORE_HTTP_PORT} +PEER_PORT=11625 +HTTP_PORT=11626 PUBLIC_HTTP_PORT=true NODE_SEED="SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index cf9b8d33..0c7191c6 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "fmt" + "net" "os" "os/exec" "os/signal" @@ -17,8 +18,6 @@ import ( "testing" "time" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/stellarcore" @@ -39,72 +38,51 @@ const ( MaxSupportedProtocolVersion = 21 FriendbotURL = "http://localhost:8000/friendbot" // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true - checkpointFrequency = 8 - helloWorldContractPath = "../../../../../wasms/test_hello_world.wasm" + checkpointFrequency = 8 + captiveCoreConfigFilename = "captive-core-integration-tests.cfg" + captiveCoreConfigTemplateFilename = captiveCoreConfigFilename + ".tmpl" + + inContainerCoreHostname = "core" + inContainerCorePort = 11625 + inContainerCoreHTTPPort = 11626 + inContainerCoreArchivePort = 1570 + // any unused port would do + inContainerCaptiveCorePort = 11725 + + inContainerRPCPort = 8000 + inContainerRPCAdminPort = 8080 ) +// Only run RPC, telling how to connect to Core +// and whether we should wait for it +type TestOnlyRPCConfig struct { + CorePorts TestCorePorts + DontWait bool +} + type TestConfig struct { ProtocolVersion uint32 // Run a previously released version of RPC (in a container) instead of the current version UseReleasedRPCVersion string - UseSQLitePath string - HistoryArchiveURL string - TestPorts *TestPorts - OnlyRPC bool - NoParallel bool -} - -type TestPorts struct { - RPCPort uint16 - RPCAdminPort uint16 - CorePort uint16 - CoreHTTPPort uint16 - CoreArchivePort uint16 - CoreCaptivePeerPort uint16 -} - -func NewTestPorts(t *testing.T) TestPorts { - return TestPorts{ - RPCPort: getFreeTCPPort(t), - RPCAdminPort: getFreeTCPPort(t), - CorePort: getFreeTCPPort(t), - CoreHTTPPort: getFreeTCPPort(t), - CoreArchivePort: getFreeTCPPort(t), - CoreCaptivePeerPort: getFreeTCPPort(t), - } - -} - -func (tp TestPorts) getMapping() map[string]uint16 { - return map[string]uint16{ - "RPC_PORT": tp.RPCPort, - "RPC_ADMIN_PORT": tp.RPCAdminPort, - "CORE_PORT": tp.CorePort, - "CORE_HTTP_PORT": tp.CoreHTTPPort, - "CORE_ARCHIVE_PORT": tp.CoreArchivePort, - "CORE_CAPTIVE_PORT": tp.CoreCaptivePeerPort, - } + // Use/Reuse a SQLite file instead of creating it from scratch + UseSQLitePath string + OnlyRPC *TestOnlyRPCConfig + // Do not mark the test as running in parallel + NoParallel bool } -func (tp TestPorts) getEnvs() []string { - mapping := tp.getMapping() - result := make([]string, 0, len(mapping)) - for k, v := range mapping { - result = append(result, fmt.Sprintf("%s=%d", k, v)) - } - return result +type TestCorePorts struct { + CorePort uint16 + CoreHTTPPort uint16 + CoreArchivePort uint16 + // This only needs to be an unconflicting port + captiveCorePort uint16 } -func (tp TestPorts) getFuncMapping() func(string) string { - mapping := tp.getMapping() - return func(name string) string { - port, ok := mapping[name] - if !ok { - // try to leave it as it was - return "$" + name - } - return strconv.Itoa(int(port)) - } +type TestPorts struct { + RPCPort uint16 + RPCAdminPort uint16 + TestCorePorts } type Test struct { @@ -114,16 +92,13 @@ type Test struct { protocolVersion uint32 - historyArchiveURL string - - expandedTemplatesDir string + rpcConfigFilesDir string rpcContainerVersion string - rpcContainerConfigMountDir string rpcContainerSQLiteMountDir string rpcContainerLogsCommand *exec.Cmd - rpcClient *jrpc2.Client + rpcClient *Client coreClient *stellarcore.Client daemon *daemon.Daemon @@ -146,39 +121,45 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { } parallel := true - sqlLitePath := "" - testPortsInitialized := false + sqlitePath := "" + shouldWaitForRPC := true if cfg != nil { - i.historyArchiveURL = cfg.HistoryArchiveURL i.rpcContainerVersion = cfg.UseReleasedRPCVersion i.protocolVersion = cfg.ProtocolVersion - sqlLitePath = cfg.UseSQLitePath - i.onlyRPC = cfg.OnlyRPC - parallel = !cfg.NoParallel - if cfg.TestPorts != nil { - i.testPorts = *cfg.TestPorts - testPortsInitialized = true + sqlitePath = cfg.UseSQLitePath + if cfg.OnlyRPC != nil { + i.onlyRPC = true + i.testPorts.TestCorePorts = cfg.OnlyRPC.CorePorts + shouldWaitForRPC = !cfg.OnlyRPC.DontWait } + parallel = !cfg.NoParallel } - - if !testPortsInitialized { - i.testPorts = NewTestPorts(t) + if sqlitePath == "" { + sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") } + if parallel { t.Parallel() } + // TODO: this function is pretty unreadable + if i.protocolVersion == 0 { // Default to the maximum supported protocol version i.protocolVersion = GetCoreMaxSupportedProtocol() } - i.expandedTemplatesDir = i.createExpandedTemplatesDir() - rpcCfg := i.getRPConfig(sqlLitePath) + i.rpcConfigFilesDir = i.t.TempDir() + if i.runRPCInContainer() { - i.rpcContainerConfigMountDir = i.createRPCContainerMountDir(rpcCfg) + // The container needs to use the sqlite mount point + i.rpcContainerSQLiteMountDir = filepath.Dir(sqlitePath) + i.generateCaptiveCoreCfgForContainer() + rpcCfg := i.getRPConfigForContainer(sqlitePath) + i.generateRPCConfigFile(rpcCfg) } + i.prepareShutdownHandlers() if i.runRPCInContainer() || !i.onlyRPC { // There are containerized workloads upCmd := []string{"up"} @@ -186,34 +167,48 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { upCmd = append(upCmd, "rpc") } upCmd = append(upCmd, "--detach", "--quiet-pull", "--no-color") - i.runComposeCommand(upCmd...) + i.runSuccessfulComposeCommand(upCmd...) if i.runRPCInContainer() { i.rpcContainerLogsCommand = i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") i.rpcContainerLogsCommand.Stdout = os.Stdout i.rpcContainerLogsCommand.Stderr = os.Stderr require.NoError(t, i.rpcContainerLogsCommand.Start()) } + i.fillContainerPorts() } - i.prepareShutdownHandlers() if !i.onlyRPC { i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(int(i.testPorts.CoreHTTPPort))} i.waitForCore() i.waitForCheckpoint() } if !i.runRPCInContainer() { + // We need to get a free port. Unfortunately this isn't completely clash-Free + // but there is no way to tell core to allocate the port dynamically + i.testPorts.captiveCorePort = getFreeTCPPort(i.t) + i.generateCaptiveCoreCfgForDaemon() + rpcCfg := i.getRPConfigForDaemon(sqlitePath) i.daemon = i.createDaemon(rpcCfg) + i.fillDaemonPorts() go i.daemon.Run() } - i.waitForRPC() + + i.rpcClient = NewClient(i.GetSorobanRPCURL(), nil) + if shouldWaitForRPC { + i.waitForRPC() + } return i } +func (i *Test) GetPorts() TestPorts { + return i.testPorts +} + func (i *Test) runRPCInContainer() bool { return i.rpcContainerVersion != "" } -func (i *Test) GetRPCLient() *jrpc2.Client { +func (i *Test) GetRPCLient() *Client { return i.rpcClient } func (i *Test) MasterKey() *keypair.Full { @@ -250,65 +245,67 @@ func (i *Test) waitForCheckpoint() { ) } -func (i *Test) getRPConfig(sqlitePath string) map[string]string { - if sqlitePath == "" { - sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") - } - - // 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", i.testPorts.CoreArchivePort) - if i.runRPCInContainer() { - // the archive needs to be accessed from the container - // where core is Core's hostname - archiveURL = fmt.Sprintf("http://core:%d", i.testPorts.CoreArchivePort) - } - if i.historyArchiveURL != "" { - // an archive URL was supplied explicitly - archiveURL = i.historyArchiveURL - } - - captiveCoreConfigPath := path.Join(i.expandedTemplatesDir, "captive-core-integration-tests.cfg") - bindHost := "localhost" - stellarCoreURL := fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort) - if i.runRPCInContainer() { +func (i *Test) getRPConfigForContainer(sqlitePath string) rpcConfig { + return rpcConfig{ + // Container's default path to captive core + coreBinaryPath: "/usr/bin/stellar-core", + archiveURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreArchivePort), // The file will be inside the container - captiveCoreConfigPath = "/stellar-core.cfg" + 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", i.testPorts.CoreHTTPPort) + // (otherwise it can't be accessible from the outside) + endPoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCPort), + adminEndpoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCAdminPort), + sqlitePath: "/db/" + filepath.Base(sqlitePath), + stellarCoreURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreHTTPPort), + captiveCoreStoragePath: "/tmp/captive-core", } +} - // in the container - captiveCoreStoragePath := "/tmp/captive-core" - if !i.runRPCInContainer() { - captiveCoreStoragePath = i.t.TempDir() +func (i *Test) getRPConfigForDaemon(sqlitePath string) rpcConfig { + coreBinaryPath := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + if coreBinaryPath == "" { + i.t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") } + return rpcConfig{ + coreBinaryPath: coreBinaryPath, + archiveURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort), + captiveCoreConfigPath: path.Join(i.rpcConfigFilesDir, captiveCoreConfigFilename), + stellarCoreURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort), + // Allocate port dynamically and then figure out what the port is + endPoint: "localhost:0", + adminEndpoint: "localhost:0", + captiveCoreStoragePath: i.t.TempDir(), + sqlitePath: sqlitePath, + } +} + +type rpcConfig struct { + endPoint string + adminEndpoint string + stellarCoreURL string + coreBinaryPath string + captiveCoreConfigPath string + captiveCoreStoragePath string + archiveURL string + sqlitePath string +} +func (vars rpcConfig) toMap() map[string]string { return map[string]string{ - "ENDPOINT": fmt.Sprintf("%s:%d", bindHost, i.testPorts.RPCPort), - "ADMIN_ENDPOINT": fmt.Sprintf("%s:%d", bindHost, i.testPorts.RPCAdminPort), - "STELLAR_CORE_URL": stellarCoreURL, + "ENDPOINT": vars.endPoint, + "ADMIN_ENDPOINT": vars.adminEndpoint, + "STELLAR_CORE_URL": vars.stellarCoreURL, "CORE_REQUEST_TIMEOUT": "2s", - "STELLAR_CORE_BINARY_PATH": coreBinaryPath, - "CAPTIVE_CORE_CONFIG_PATH": captiveCoreConfigPath, - "CAPTIVE_CORE_STORAGE_PATH": captiveCoreStoragePath, + "STELLAR_CORE_BINARY_PATH": vars.coreBinaryPath, + "CAPTIVE_CORE_CONFIG_PATH": vars.captiveCoreConfigPath, + "CAPTIVE_CORE_STORAGE_PATH": vars.captiveCoreStoragePath, "STELLAR_CAPTIVE_CORE_HTTP_PORT": "0", "FRIENDBOT_URL": FriendbotURL, "NETWORK_PASSPHRASE": StandaloneNetworkPassphrase, - "HISTORY_ARCHIVE_URLS": archiveURL, + "HISTORY_ARCHIVE_URLS": vars.archiveURL, "LOG_LEVEL": "debug", - "DB_PATH": sqlitePath, + "DB_PATH": vars.sqlitePath, "INGESTION_TIMEOUT": "10m", "EVENT_LEDGER_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), "TRANSACTION_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), @@ -320,17 +317,9 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { func (i *Test) waitForRPC() { i.t.Log("Waiting for RPC to be healthy...") - // This is needed because of https://github.com/creachadair/jrpc2/issues/118 - refreshClient := func() { - if i.rpcClient != nil { - i.rpcClient.Close() - } - ch := jhttp.NewChannel(i.GetSorobanRPCURL(), nil) - i.rpcClient = jrpc2.NewClient(ch, nil) - } + require.Eventually(i.t, func() bool { - refreshClient() result, err := i.GetRPCHealth() return err == nil && result.Status == "healthy" }, @@ -339,35 +328,7 @@ func (i *Test) waitForRPC() { ) } -func (i *Test) createExpandedTemplatesDir() string { - mountDir := i.t.TempDir() - configDir := filepath.Join(GetCurrentDirectory(), "docker") - entries, err := os.ReadDir(configDir) - require.NoError(i.t, err) - fmapping := i.testPorts.getFuncMapping() - for _, entry := range entries { - if !entry.Type().IsRegular() { - continue - } - if !strings.HasSuffix(entry.Name(), ".tmpl") { - continue - } - originalPath := filepath.Join(configDir, entry.Name()) - in, err := os.ReadFile(originalPath) - require.NoError(i.t, err) - out := os.Expand(string(in), fmapping) - targetPath := filepath.Join(mountDir, strings.TrimSuffix(entry.Name(), ".tmpl")) - info, err := entry.Info() - require.NoError(i.t, err) - err = os.WriteFile(targetPath, []byte(out), info.Mode()) - require.NoError(i.t, err) - } - return mountDir -} - -func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { - mountDir := i.t.TempDir() - +func (i *Test) generateCaptiveCoreCfgForContainer() { getOldVersionCaptiveCoreConfigVersion := func(dir string, filename string) ([]byte, error) { cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./%s/%s", i.rpcContainerVersion, dir, filename)) cmd.Dir = GetCurrentDirectory() @@ -375,40 +336,58 @@ func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { } // Get old version of captive-core-integration-tests.cfg.tmpl - out, err := getOldVersionCaptiveCoreConfigVersion("docker", "captive-core-integration-tests.cfg.tmpl") + out, err := getOldVersionCaptiveCoreConfigVersion("docker", captiveCoreConfigTemplateFilename) if err != nil { // Try the directory before the integration test refactoring // TODO: remove this hack after protocol 22 is released - out, err = getOldVersionCaptiveCoreConfigVersion("../../test", "captive-core-integration-tests.cfg") - outStr := strings.Replace(string(out), `ADDRESS="localhost"`, fmt.Sprintf(`ADDRESS="localhost:%d"`, i.testPorts.CorePort), -1) + out, err = getOldVersionCaptiveCoreConfigVersion("../../test", captiveCoreConfigFilename) + outStr := strings.Replace(string(out), `ADDRESS="localhost"`, `ADDRESS="${CORE_HOST_PORT}"`, -1) out = []byte(outStr) } require.NoError(i.t, err) + i.generateCaptiveCoreCfg(out, inContainerCaptiveCorePort, inContainerCoreHostname) +} +func (i *Test) generateCaptiveCoreCfg(tmplContents []byte, captiveCorePort uint16, coreHostPort string) { // Apply expansion - captiveCoreCfgContents := os.Expand(string(out), i.testPorts.getFuncMapping()) + mapping := func(in string) string { + switch in { + case "CAPTIVE_CORE_PORT": + // any non-conflicting port would do + return strconv.Itoa(int(captiveCorePort)) + case "CORE_HOST_PORT": + return coreHostPort + default: + // Try to leave it as it was + return "$" + in + } + } + + captiveCoreCfgContents := os.Expand(string(tmplContents), mapping) + err := os.WriteFile(filepath.Join(i.rpcConfigFilesDir, captiveCoreConfigFilename), []byte(captiveCoreCfgContents), 0666) + require.NoError(i.t, err) +} - // TODO: maybe it would be better to not place localhost in the file (and use a host replacement) - // replace ADDRESS="localhost by ADDRESS="core, so that the container can find core - captiveCoreCfgContents = strings.Replace(captiveCoreCfgContents, `ADDRESS="localhost`, `ADDRESS="core`, -1) - err = os.WriteFile(filepath.Join(mountDir, "stellar-core-integration-tests.cfg"), []byte(captiveCoreCfgContents), 0666) +func (i *Test) generateCaptiveCoreCfgForDaemon() { + out, err := os.ReadFile(filepath.Join(GetCurrentDirectory(), "docker", captiveCoreConfigTemplateFilename)) require.NoError(i.t, err) + i.generateCaptiveCoreCfg(out, i.testPorts.captiveCorePort, "localhost:"+strconv.Itoa(int(i.testPorts.CorePort))) +} - // Generate config file +func (i *Test) generateRPCConfigFile(rpcConfig rpcConfig) { cfgFileContents := "" - for k, v := range rpcConfig { + for k, v := range rpcConfig.toMap() { cfgFileContents += fmt.Sprintf("%s=%q\n", k, v) } - err = os.WriteFile(filepath.Join(mountDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) + err := os.WriteFile(filepath.Join(i.rpcConfigFilesDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) require.NoError(i.t, err) - - return mountDir } -func (i *Test) createDaemon(env map[string]string) *daemon.Daemon { +func (i *Test) createDaemon(c rpcConfig) *daemon.Daemon { var cfg config.Config + m := c.toMap() lookup := func(s string) (string, bool) { - ret, ok := env[s] + ret, ok := m[s] return ret, ok } require.NoError(i.t, cfg.SetValues(lookup)) @@ -425,24 +404,18 @@ func (i *Test) getComposeProjectName() string { } func (i *Test) getComposeCommand(args ...string) *exec.Cmd { - integrationYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.yml") - configFiles := []string{"-f", integrationYaml} + composeFile := "docker-compose.yml" if i.runRPCInContainer() { - rpcYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.rpc.yml") - configFiles = append(configFiles, "-f", rpcYaml) + composeFile = "docker-compose.rpc.yml" } + fullComposeFilePath := filepath.Join(GetCurrentDirectory(), "docker", composeFile) + cmdline := []string{"-f", fullComposeFilePath} // Use separate projects to run them in parallel projectName := i.getComposeProjectName() - cmdline := append([]string{"-p", projectName}, configFiles...) + cmdline = append([]string{"-p", projectName}, cmdline...) cmdline = append(cmdline, args...) cmd := exec.Command("docker-compose", cmdline...) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, - "CORE_MOUNT_DIR="+i.expandedTemplatesDir, - ) - cmd.Env = append(cmd.Env, i.testPorts.getEnvs()...) - if img := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_DOCKER_IMG"); img != "" { cmd.Env = append( cmd.Env, @@ -454,28 +427,35 @@ func (i *Test) getComposeCommand(args ...string) *exec.Cmd { cmd.Env = append( cmd.Env, "RPC_IMAGE_TAG="+i.rpcContainerVersion, - "RPC_CONFIG_MOUNT_DIR="+i.rpcContainerConfigMountDir, + "RPC_CONFIG_MOUNT_DIR="+i.rpcConfigFilesDir, "RPC_SQLITE_MOUNT_DIR="+i.rpcContainerSQLiteMountDir, "RPC_UID="+strconv.Itoa(os.Getuid()), "RPC_GID="+strconv.Itoa(os.Getgid()), ) } + if cmd.Env != nil { + cmd.Env = append(os.Environ(), cmd.Env...) + } return cmd } -func (i *Test) runComposeCommand(args ...string) { +func (i *Test) runComposeCommand(args ...string) ([]byte, error) { cmd := i.getComposeCommand(args...) - i.t.Log("Running", cmd.Args) - out, innerErr := cmd.Output() - if exitErr, ok := innerErr.(*exec.ExitError); ok { - i.t.Log("stdout\n:", string(out)) - i.t.Log("stderr:\n", string(exitErr.Stderr)) - } + return cmd.Output() +} - if innerErr != nil { - i.t.Fatalf("Compose command failed: %v", innerErr) +func (i *Test) runSuccessfulComposeCommand(args ...string) []byte { + out, err := i.runComposeCommand(args...) + if err != nil { + i.t.Log("Compose command failed, args:", args) } + if exitErr, ok := err.(*exec.ExitError); ok { + i.t.Log("stdout:\n", string(out)) + i.t.Log("stderr:\n", string(exitErr.Stderr)) + } + require.NoError(i.t, err) + return out } func (i *Test) prepareShutdownHandlers() { @@ -496,7 +476,7 @@ func (i *Test) prepareShutdownHandlers() { downCmd = append(downCmd, "rpc") } downCmd = append(downCmd, "-v") - i.runComposeCommand(downCmd...) + i.runSuccessfulComposeCommand(downCmd...) } if i.rpcContainerLogsCommand != nil { i.rpcContainerLogsCommand.Wait() @@ -576,7 +556,7 @@ func (i *Test) StopRPC() { i.daemon = nil } if i.runRPCInContainer() { - i.runComposeCommand("down", "rpc", "-v") + i.runSuccessfulComposeCommand("down", "rpc", "-v") } } @@ -647,6 +627,46 @@ func (i *Test) GetRPCHealth() (methods.HealthCheckResult, error) { return result, err } +func (i *Test) fillContainerPorts() { + getPublicPort := func(service string, privatePort int) uint16 { + var port uint16 + // We need to try several times because we detached from `docker-compose up` + // and the container may not be ready + require.Eventually(i.t, + func() bool { + out, err := i.runComposeCommand("port", service, strconv.Itoa(privatePort)) + if err != nil { + return false + } + _, strPort, err := net.SplitHostPort(strings.TrimSpace(string(out))) + require.NoError(i.t, err) + intPort, err := strconv.Atoi(strPort) + require.NoError(i.t, err) + port = uint16(intPort) + return true + }, + 2*time.Second, + 100*time.Millisecond, + ) + return port + } + i.testPorts.CorePort = getPublicPort("core", inContainerCorePort) + i.testPorts.CoreHTTPPort = getPublicPort("core", inContainerCoreHTTPPort) + i.testPorts.CoreArchivePort = getPublicPort("core", inContainerCoreArchivePort) + if i.runRPCInContainer() { + i.testPorts.RPCPort = getPublicPort("rpc", inContainerRPCPort) + i.testPorts.RPCAdminPort = getPublicPort("rpc", inContainerRPCAdminPort) + } +} + +func (i *Test) fillDaemonPorts() { + endpointAddr, adminEndpointAddr := i.daemon.GetEndpointAddrs() + i.testPorts.RPCPort = uint16(endpointAddr.Port) + if adminEndpointAddr != nil { + i.testPorts.RPCAdminPort = uint16(adminEndpointAddr.Port) + } +} + func GetCoreMaxSupportedProtocol() uint32 { str := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") if str == "" { diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go index f98b3c77..3cd791b3 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go @@ -1,9 +1,11 @@ package infrastructure import ( + "fmt" "net" "path/filepath" "runtime" + "time" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/require" @@ -37,3 +39,14 @@ func CreateTransactionParams(account txnbuild.Account, op txnbuild.Operation) tx }, } } + +func isLocalTCPPortOpen(port uint16) bool { + host := fmt.Sprintf("localhost:%d", port) + timeout := time.Second + conn, err := net.DialTimeout("tcp", host, timeout) + if err != nil { + return false + } + conn.Close() + return true +} diff --git a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go index b401b545..b6c360a3 100644 --- a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go @@ -38,26 +38,25 @@ func TestMigrate(t *testing.T) { } func testMigrateFromVersion(t *testing.T, version string) { - originalPorts := infrastructure.NewTestPorts(t) sqliteFile := filepath.Join(t.TempDir(), "soroban-rpc.db") test := infrastructure.NewTest(t, &infrastructure.TestConfig{ UseReleasedRPCVersion: version, UseSQLitePath: sqliteFile, - TestPorts: &originalPorts, }) // Submit an event-logging transaction in the version to migrate from submitTransactionResponse, _ := test.UploadHelloWorldContract() - // Replace RPC with the current version, but keeping the previous network and sql database (causing a data migration if needed) + // Replace RPC with the current version, but keeping the previous network and sql database (causing any data migrations) // We need to do some wiring to plug RPC into the prior network test.StopRPC() - freshPorts := infrastructure.NewTestPorts(t) - ports := originalPorts - ports.RPCPort = freshPorts.RPCPort + corePorts := test.GetPorts().TestCorePorts test = infrastructure.NewTest(t, &infrastructure.TestConfig{ - TestPorts: &ports, - OnlyRPC: true, + // We don't want to run Core again + OnlyRPC: &infrastructure.TestOnlyRPCConfig{ + CorePorts: corePorts, + DontWait: false, + }, UseSQLitePath: sqliteFile, // We don't want to mark the test as parallel twice since it causes a panic NoParallel: true, diff --git a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go index 4429bfca..380c9307 100644 --- a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/creachadair/jrpc2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -490,7 +489,7 @@ func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { return key } -func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.LedgerKey) { +func waitUntilLedgerEntryTTL(t *testing.T, client *infrastructure.Client, ledgerKey xdr.LedgerKey) { keyB64, err := xdr.MarshalBase64(ledgerKey) require.NoError(t, err) request := methods.GetLedgerEntriesRequest{