diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 13223de..714ce96 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -210,7 +210,7 @@ void AmmUiBackend::syncWalletState() setSequencerAddr(state.sequencerAddress); setSequencerReachable(state.sequencerReachable); - m_sequencer->configure(state.configPath); + m_sequencer->configure(state.configPath, state.sequencerAddress); const bool addressChanged = previousAddress != state.sequencerAddress; if ((walletCouldSubmit && !state.canSubmit()) || addressChanged) diff --git a/apps/amm/src/SequencerClient.cpp b/apps/amm/src/SequencerClient.cpp index 94c0e79..d199895 100644 --- a/apps/amm/src/SequencerClient.cpp +++ b/apps/amm/src/SequencerClient.cpp @@ -80,20 +80,24 @@ SequencerClient::SequencerClient(AmmClient* client, QObject* parent) { } -bool SequencerClient::configure(const QString& configPath) +bool SequencerClient::configure(const QString& configPath, + const QString& effectiveEndpoint) { - QUrl endpoint; + QUrl configuredEndpoint; QByteArray authorization; QFile file(configPath); - bool valid = file.open(QIODevice::ReadOnly); - if (valid) { + bool configValid = file.open(QIODevice::ReadOnly); + if (configValid) { const QJsonDocument document = QJsonDocument::fromJson(file.readAll()); const QJsonObject config = document.object(); - endpoint = QUrl(config.value(QStringLiteral("sequencer_addr")).toString()); - const QString scheme = endpoint.scheme().toLower(); - valid = document.isObject() && endpoint.isValid() && !endpoint.host().isEmpty() + configuredEndpoint = QUrl( + config.value(QStringLiteral("sequencer_addr")).toString()); + const QString scheme = configuredEndpoint.scheme().toLower(); + configValid = document.isObject() + && configuredEndpoint.isValid() + && !configuredEndpoint.host().isEmpty() && (scheme == QStringLiteral("http") || scheme == QStringLiteral("https")); - if (valid) { + if (configValid) { const QJsonObject basicAuth = config.value(QStringLiteral("basic_auth")).toObject(); const QString username = basicAuth.value(QStringLiteral("username")).toString(); const QString password = basicAuth.value(QStringLiteral("password")).toString(); @@ -103,10 +107,18 @@ bool SequencerClient::configure(const QString& configPath) } } } - if (!valid) { + + QUrl endpoint = effectiveEndpoint.isEmpty() + ? configuredEndpoint : QUrl(effectiveEndpoint); + const QString scheme = endpoint.scheme().toLower(); + const bool valid = endpoint.isValid() + && !endpoint.host().isEmpty() + && (scheme == QStringLiteral("http") || scheme == QStringLiteral("https")) + && (configValid || !effectiveEndpoint.isEmpty()); + if (!valid) endpoint = QUrl(); + if (!configValid || configuredEndpoint != endpoint) authorization.clear(); - } if (endpoint == m_endpoint && authorization == m_authorization) return valid; diff --git a/apps/amm/src/SequencerClient.h b/apps/amm/src/SequencerClient.h index cbcd789..342ad7b 100644 --- a/apps/amm/src/SequencerClient.h +++ b/apps/amm/src/SequencerClient.h @@ -25,7 +25,8 @@ public: explicit SequencerClient(AmmClient* client, QObject* parent = nullptr); - bool configure(const QString& configPath); + bool configure(const QString& configPath, + const QString& effectiveEndpoint = {}); QString endpoint() const { return m_endpoint.toString(); } bool isConfigured() const { return m_endpoint.isValid() && !m_endpoint.isEmpty(); } void applyAuthorization(QNetworkRequest& request) const; diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp index c404779..ec03d94 100644 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp @@ -53,6 +53,10 @@ namespace { } int requestCount() const { return m_requestCount; } + QByteArray lastRequest() const + { + return m_lastRequest; + } void failNextRequest() { ++m_failuresRemaining; } void holdResponses() { m_holdResponses = true; } void releaseNextResponse() @@ -108,6 +112,7 @@ namespace { return; ++m_requestCount; + m_lastRequest = request; const bool fail = m_failuresRemaining > 0; if (fail) --m_failuresRemaining; @@ -121,6 +126,7 @@ namespace { QTcpServer m_server; QHash m_requests; + QByteArray m_lastRequest; int m_requestCount = 0; int m_failuresRemaining = 0; bool m_holdResponses = false; @@ -480,6 +486,64 @@ int main(int argc, char** argv) "stale quote should have no wallet side effects")) return 1; + LocalRpcServer configuredEndpointServer; + LocalRpcServer adoptedEndpointServer; + if (!expect(configuredEndpointServer.listen() + && adoptedEndpointServer.listen(), + "endpoint-alignment sequencers should listen")) + return 1; + QTemporaryFile endpointConfig; + if (!expect(endpointConfig.open(), "endpoint-alignment config should open")) + return 1; + endpointConfig.write(QJsonDocument(QJsonObject { + { QStringLiteral("sequencer_addr"), configuredEndpointServer.endpoint() }, + { QStringLiteral("basic_auth"), QJsonObject { + { QStringLiteral("username"), QStringLiteral("user") }, + { QStringLiteral("password"), QStringLiteral("pass") }, + } }, + }).toJson(QJsonDocument::Compact)); + endpointConfig.flush(); + FakeAmmClient endpointClient; + SequencerClient alignedSequencer(&endpointClient); + if (!expect(alignedSequencer.configure( + endpointConfig.fileName(), adoptedEndpointServer.endpoint()), + "adopted wallet endpoint should configure")) + return 1; + if (!expect(alignedSequencer.endpoint() == adoptedEndpointServer.endpoint(), + "adopted wallet endpoint should override stale config")) + return 1; + QVector adoptedEndpointReads; + const QString endpointAccount(64, QLatin1Char('3')); + if (!expect(waitForAccounts( + alignedSequencer, { endpointAccount }, false, + &adoptedEndpointReads), + "adopted endpoint read should complete")) + return 1; + if (!expect(configuredEndpointServer.requestCount() == 0 + && adoptedEndpointServer.requestCount() == 1, + "direct reads should follow adopted wallet endpoint")) + return 1; + if (!expect(!adoptedEndpointServer.lastRequest().toLower().contains( + "authorization:"), + "stale config credentials must not reach adopted endpoint")) + return 1; + + if (!expect(alignedSequencer.configure( + endpointConfig.fileName(), configuredEndpointServer.endpoint()), + "matching wallet endpoint should configure")) + return 1; + QVector configuredEndpointReads; + if (!expect(waitForAccounts( + alignedSequencer, { endpointAccount }, false, + &configuredEndpointReads), + "matching endpoint read should complete")) + return 1; + if (!expect(configuredEndpointServer.requestCount() == 1 + && configuredEndpointServer.lastRequest().toLower().contains( + "authorization: basic dxnlcjpwyxnz"), + "matching endpoint should retain configured credentials")) + return 1; + LocalRpcServer server; if (!expect(server.listen(), "local sequencer should listen")) return 1;