mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-20 22:10:00 +00:00
fix(amm): submit wallet mutations asynchronously
This commit is contained in:
parent
02a65aaf09
commit
cfaaba3bce
@ -191,6 +191,7 @@ void AmmUiBackend::syncWalletState()
|
||||
{
|
||||
const WalletUiState& state = m_walletController->state();
|
||||
const bool walletWasOpen = isWalletOpen();
|
||||
const bool walletCouldSubmit = walletCanSubmit();
|
||||
const bool wasReachable = sequencerReachable();
|
||||
const QString previousAddress = sequencerAddr();
|
||||
|
||||
@ -212,6 +213,8 @@ void AmmUiBackend::syncWalletState()
|
||||
m_sequencer->configure(state.configPath);
|
||||
|
||||
const bool addressChanged = previousAddress != state.sequencerAddress;
|
||||
if ((walletCouldSubmit && !state.canSubmit()) || addressChanged)
|
||||
m_newPosition->cancelSubmit();
|
||||
if (addressChanged) {
|
||||
m_identityRetryTimer->stop();
|
||||
m_pendingTransactions.clear();
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QPointer>
|
||||
#include <QScopedValueRollback>
|
||||
#include <libbase58.h>
|
||||
|
||||
@ -171,6 +172,7 @@ NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet,
|
||||
void NewPositionRuntime::clearWalletAccounts()
|
||||
{
|
||||
++m_walletGeneration;
|
||||
cancelSubmit();
|
||||
m_walletPublicAccountIds.clear();
|
||||
m_wallet->clearSnapshot();
|
||||
}
|
||||
@ -182,11 +184,44 @@ void NewPositionRuntime::setWalletAccounts(const QVector<WalletAccount>& account
|
||||
if (account.isPublic)
|
||||
publicAccountIds.append(account.address);
|
||||
}
|
||||
if (publicAccountIds != m_walletPublicAccountIds)
|
||||
if (publicAccountIds != m_walletPublicAccountIds) {
|
||||
++m_walletGeneration;
|
||||
cancelSubmit();
|
||||
}
|
||||
m_walletPublicAccountIds = std::move(publicAccountIds);
|
||||
}
|
||||
|
||||
void NewPositionRuntime::cancelSubmit()
|
||||
{
|
||||
if (!m_submitInFlight)
|
||||
return;
|
||||
++m_submitGeneration;
|
||||
m_submitInFlight = false;
|
||||
ResultCallback callback = std::move(m_submitCallback);
|
||||
m_submitCallback = {};
|
||||
if (callback)
|
||||
callback(publicError(QStringLiteral("wallet_unavailable")).toVariantMap());
|
||||
}
|
||||
|
||||
void NewPositionRuntime::finishSubmit(quint64 submitGeneration, QVariantMap result)
|
||||
{
|
||||
if (!m_submitInFlight || submitGeneration != m_submitGeneration)
|
||||
return;
|
||||
m_submitInFlight = false;
|
||||
ResultCallback callback = std::move(m_submitCallback);
|
||||
m_submitCallback = {};
|
||||
if (callback)
|
||||
callback(std::move(result));
|
||||
}
|
||||
|
||||
bool NewPositionRuntime::submitIsCurrent(quint64 submitGeneration,
|
||||
quint64 walletGeneration) const
|
||||
{
|
||||
return m_submitInFlight
|
||||
&& submitGeneration == m_submitGeneration
|
||||
&& walletGeneration == m_walletGeneration;
|
||||
}
|
||||
|
||||
QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const
|
||||
{
|
||||
if (!walletOpen)
|
||||
@ -280,14 +315,20 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request,
|
||||
}
|
||||
const QString configId = configResult.value.value(
|
||||
QStringLiteral("configId")).toString();
|
||||
QPointer<NewPositionRuntime> guard(this);
|
||||
m_sequencer->readAccounts({ configId }, refreshPublicData,
|
||||
[this, request, network, walletOpen, refreshPublicData,
|
||||
[guard, request, network, walletOpen, refreshPublicData,
|
||||
callback = std::move(callback)](QVector<WalletAccountRead> configReads) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
const QJsonObject config = accountReadJson(configReads.value(0));
|
||||
m_sequencer->readAccounts(m_walletPublicAccountIds, refreshPublicData,
|
||||
[this, request, network, walletOpen, refreshPublicData, config,
|
||||
guard->m_sequencer->readAccounts(
|
||||
guard->m_walletPublicAccountIds, refreshPublicData,
|
||||
[guard, request, network, walletOpen, refreshPublicData, config,
|
||||
callback = std::move(callback)](
|
||||
QVector<WalletAccountRead> walletReads) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
const QJsonObject hints = QJsonObject::fromVariantMap(request);
|
||||
QJsonArray configured;
|
||||
for (const QString& id : network.tokenIds)
|
||||
@ -297,7 +338,7 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request,
|
||||
const QJsonArray resolved = variantStringArray(
|
||||
hints.value(QStringLiteral("resolvedTokenIds")).toVariant());
|
||||
const QJsonArray walletAccounts = accountReadsJson(walletReads);
|
||||
const AmmClientResult tokenResult = m_client->tokenIds(QJsonObject {
|
||||
const AmmClientResult tokenResult = guard->m_client->tokenIds(QJsonObject {
|
||||
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||
{ QStringLiteral("config"), config },
|
||||
{ QStringLiteral("walletAccounts"), walletAccounts },
|
||||
@ -322,12 +363,14 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request,
|
||||
.value(QStringLiteral("tokenIds")).toArray()) {
|
||||
definitionIds.append(id.toString());
|
||||
}
|
||||
m_sequencer->readAccounts(definitionIds, refreshPublicData,
|
||||
[this, network, walletOpen, config, walletAccounts,
|
||||
guard->m_sequencer->readAccounts(definitionIds, refreshPublicData,
|
||||
[guard, network, walletOpen, config, walletAccounts,
|
||||
configured, recent, resolved,
|
||||
callback = std::move(callback)](
|
||||
QVector<WalletAccountRead> definitions) mutable {
|
||||
const AmmClientResult result = m_client->context(QJsonObject {
|
||||
if (!guard)
|
||||
return;
|
||||
const AmmClientResult result = guard->m_client->context(QJsonObject {
|
||||
{ QStringLiteral("networkId"), network.id },
|
||||
{ QStringLiteral("networkFingerprint"), network.fingerprint },
|
||||
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||
@ -354,7 +397,7 @@ void NewPositionRuntime::buildQuoteInputAsync(
|
||||
const ActiveNetworkSnapshot& network,
|
||||
bool walletOpen,
|
||||
bool forceRefresh,
|
||||
std::function<void(QJsonObject, QJsonObject)> callback) const
|
||||
std::function<void(QJsonObject, QJsonObject)> callback)
|
||||
{
|
||||
if (network.status != QStringLiteral("ready")) {
|
||||
callback({}, publicError(network.status));
|
||||
@ -372,12 +415,15 @@ void NewPositionRuntime::buildQuoteInputAsync(
|
||||
}
|
||||
const QString configId = configResult.value.value(
|
||||
QStringLiteral("configId")).toString();
|
||||
QPointer<NewPositionRuntime> guard(this);
|
||||
m_sequencer->readAccounts({ configId }, forceRefresh,
|
||||
[this, request, network, walletOpen, forceRefresh,
|
||||
[guard, request, network, walletOpen, forceRefresh,
|
||||
callback = std::move(callback)](QVector<WalletAccountRead> configReads) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
const QJsonObject config = accountReadJson(configReads.value(0));
|
||||
const QJsonObject requestObject = QJsonObject::fromVariantMap(request);
|
||||
const AmmClientResult pairResult = m_client->pairIds(QJsonObject {
|
||||
const AmmClientResult pairResult = guard->m_client->pairIds(QJsonObject {
|
||||
{ QStringLiteral("ammProgramId"), network.ammProgramId },
|
||||
{ QStringLiteral("config"), config },
|
||||
{ QStringLiteral("tokenAId"),
|
||||
@ -407,15 +453,20 @@ void NewPositionRuntime::buildQuoteInputAsync(
|
||||
pair.value(QStringLiteral("currentTickId")).toString(),
|
||||
pair.value(QStringLiteral("clockId")).toString(),
|
||||
};
|
||||
m_sequencer->readAccounts(fixedIds, forceRefresh,
|
||||
[this, requestObject, network, walletOpen, config, pair,
|
||||
guard->m_sequencer->readAccounts(fixedIds, forceRefresh,
|
||||
[guard, requestObject, network, walletOpen, config, pair,
|
||||
callback = std::move(callback)](
|
||||
QVector<WalletAccountRead> fixedReads) mutable {
|
||||
m_sequencer->readAccounts(m_walletPublicAccountIds, false,
|
||||
[this, requestObject, network, walletOpen, config, pair,
|
||||
if (!guard)
|
||||
return;
|
||||
guard->m_sequencer->readAccounts(
|
||||
guard->m_walletPublicAccountIds, false,
|
||||
[guard, requestObject, network, walletOpen, config, pair,
|
||||
fixedReads = std::move(fixedReads),
|
||||
callback = std::move(callback)](
|
||||
QVector<WalletAccountRead> walletReads) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
QStringList selectedIds;
|
||||
for (const QString& key : {
|
||||
QStringLiteral("holdingAId"),
|
||||
@ -426,8 +477,7 @@ void NewPositionRuntime::buildQuoteInputAsync(
|
||||
if (!id.isEmpty() && !selectedIds.contains(id))
|
||||
selectedIds.append(id);
|
||||
}
|
||||
auto finish = [this, requestObject, network, walletOpen,
|
||||
config, pair,
|
||||
auto finish = [requestObject, network, walletOpen, config,
|
||||
fixedReads = std::move(fixedReads),
|
||||
walletReads = std::move(walletReads),
|
||||
callback = std::move(callback)](
|
||||
@ -464,7 +514,7 @@ void NewPositionRuntime::buildQuoteInputAsync(
|
||||
if (selectedIds.isEmpty())
|
||||
finish({});
|
||||
else
|
||||
m_sequencer->readAccounts(
|
||||
guard->m_sequencer->readAccounts(
|
||||
selectedIds, true, std::move(finish));
|
||||
});
|
||||
});
|
||||
@ -477,14 +527,17 @@ void NewPositionRuntime::quoteAsync(const QVariantMap& request,
|
||||
bool forceRefresh,
|
||||
ResultCallback callback)
|
||||
{
|
||||
QPointer<NewPositionRuntime> guard(this);
|
||||
buildQuoteInputAsync(request, network, walletOpen, forceRefresh,
|
||||
[this, callback = std::move(callback)](
|
||||
[guard, callback = std::move(callback)](
|
||||
QJsonObject input, QJsonObject error) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (!error.isEmpty()) {
|
||||
callback(error.toVariantMap());
|
||||
return;
|
||||
}
|
||||
const AmmClientResult result = m_client->quote(input);
|
||||
const AmmClientResult result = guard->m_client->quote(input);
|
||||
callback((result.ok ? result.value
|
||||
: publicError(QStringLiteral("backend_error")))
|
||||
.toVariantMap());
|
||||
@ -506,114 +559,154 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request,
|
||||
return;
|
||||
}
|
||||
m_submitInFlight = true;
|
||||
const quint64 submitGeneration = ++m_submitGeneration;
|
||||
const quint64 walletGeneration = m_walletGeneration;
|
||||
auto finish = [this, callback = std::move(callback)](QVariantMap result) mutable {
|
||||
m_submitInFlight = false;
|
||||
callback(std::move(result));
|
||||
};
|
||||
m_submitCallback = std::move(callback);
|
||||
QPointer<NewPositionRuntime> guard(this);
|
||||
buildQuoteInputAsync(request, network, true, true,
|
||||
[this, quoteHash, walletGeneration, finish = std::move(finish)](
|
||||
[guard, quoteHash, submitGeneration, walletGeneration](
|
||||
QJsonObject input, QJsonObject error) mutable {
|
||||
if (walletGeneration != m_walletGeneration) {
|
||||
finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap());
|
||||
if (!guard
|
||||
|| !guard->submitIsCurrent(submitGeneration, walletGeneration))
|
||||
return;
|
||||
}
|
||||
if (!error.isEmpty()) {
|
||||
finish(error.toVariantMap());
|
||||
guard->finishSubmit(submitGeneration, error.toVariantMap());
|
||||
return;
|
||||
}
|
||||
const AmmClientResult quoteResult = m_client->quote(input);
|
||||
const AmmClientResult quoteResult = guard->m_client->quote(input);
|
||||
if (!quoteResult.ok) {
|
||||
finish(publicError(QStringLiteral("backend_error")).toVariantMap());
|
||||
guard->finishSubmit(
|
||||
submitGeneration,
|
||||
publicError(QStringLiteral("backend_error")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
const QJsonObject quote = quoteResult.value;
|
||||
if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) {
|
||||
QJsonObject result = publicError(QStringLiteral("quote_changed"));
|
||||
result.insert(QStringLiteral("quote"), quote);
|
||||
finish(result.toVariantMap());
|
||||
guard->finishSubmit(submitGeneration, result.toVariantMap());
|
||||
return;
|
||||
}
|
||||
if (!quote.value(QStringLiteral("canSubmit")).toBool(false)) {
|
||||
QJsonObject result = publicError(QStringLiteral("quote_not_submittable"));
|
||||
result.insert(QStringLiteral("quote"), quote);
|
||||
finish(result.toVariantMap());
|
||||
guard->finishSubmit(submitGeneration, result.toVariantMap());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue freshLp;
|
||||
if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
|
||||
if (walletGeneration != m_walletGeneration) {
|
||||
finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
const WalletAccountCreation creation = m_wallet->createAccount(true);
|
||||
if (!creation.ok() || !creation.publicAccount.ok()) {
|
||||
finish(publicError(QStringLiteral("wallet_submission_failed")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
freshLp = accountReadJson(creation.publicAccount);
|
||||
if (!m_walletPublicAccountIds.contains(creation.accountId))
|
||||
m_walletPublicAccountIds.append(creation.accountId);
|
||||
guard->m_wallet->createAccountAsync(
|
||||
true,
|
||||
[guard, input = std::move(input), quoteHash,
|
||||
submitGeneration, walletGeneration](
|
||||
WalletAccountCreation creation) mutable {
|
||||
if (!guard
|
||||
|| !guard->submitIsCurrent(
|
||||
submitGeneration, walletGeneration))
|
||||
return;
|
||||
if (!creation.ok() || !creation.publicAccount.ok()) {
|
||||
const QString code = creation.failure
|
||||
== WalletFailure::WalletUnavailable
|
||||
? QStringLiteral("wallet_unavailable")
|
||||
: QStringLiteral("wallet_submission_failed");
|
||||
guard->finishSubmit(
|
||||
submitGeneration, publicError(code).toVariantMap());
|
||||
return;
|
||||
}
|
||||
if (!guard->m_walletPublicAccountIds.contains(creation.accountId))
|
||||
guard->m_walletPublicAccountIds.append(creation.accountId);
|
||||
guard->submitPlanAsync(
|
||||
std::move(input), quoteHash,
|
||||
accountReadJson(creation.publicAccount),
|
||||
submitGeneration, walletGeneration);
|
||||
});
|
||||
return;
|
||||
}
|
||||
guard->submitPlanAsync(
|
||||
std::move(input), quoteHash, {},
|
||||
submitGeneration, walletGeneration);
|
||||
});
|
||||
}
|
||||
|
||||
QJsonObject planInput = input;
|
||||
planInput.insert(QStringLiteral("quoteHash"), quoteHash);
|
||||
planInput.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch());
|
||||
if (!freshLp.isUndefined())
|
||||
planInput.insert(QStringLiteral("freshLp"), freshLp);
|
||||
const AmmClientResult planResult = m_client->plan(planInput);
|
||||
if (!planResult.ok) {
|
||||
finish(publicError(QStringLiteral("backend_error")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
const QJsonObject plan = planResult.value;
|
||||
if (plan.value(QStringLiteral("status")).toString()
|
||||
!= QStringLiteral("ready")) {
|
||||
const QString code = plan.value(QStringLiteral("code")).toString();
|
||||
finish(publicError(code.isEmpty()
|
||||
? QStringLiteral("wallet_submission_failed") : code).toVariantMap());
|
||||
return;
|
||||
}
|
||||
void NewPositionRuntime::submitPlanAsync(QJsonObject input,
|
||||
const QString& quoteHash,
|
||||
QJsonValue freshLp,
|
||||
quint64 submitGeneration,
|
||||
quint64 walletGeneration)
|
||||
{
|
||||
if (!submitIsCurrent(submitGeneration, walletGeneration))
|
||||
return;
|
||||
|
||||
const QStringList accountIds = jsonStringList(
|
||||
plan.value(QStringLiteral("accountIds")).toArray());
|
||||
const QVector<bool> signingRequirements = jsonBoolList(
|
||||
plan.value(QStringLiteral("signingRequirements")).toArray());
|
||||
const QVector<quint32> instruction = jsonUIntList(
|
||||
plan.value(QStringLiteral("instruction")).toArray());
|
||||
bool deadlineValid = false;
|
||||
const qulonglong deadline = plan.value(QStringLiteral("deadlineMs"))
|
||||
.toString().toULongLong(&deadlineValid);
|
||||
if (!deadlineValid
|
||||
|| static_cast<qulonglong>(QDateTime::currentMSecsSinceEpoch()) >= deadline) {
|
||||
finish(publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap());
|
||||
input.insert(QStringLiteral("quoteHash"), quoteHash);
|
||||
input.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch());
|
||||
if (!freshLp.isUndefined() && !freshLp.isNull())
|
||||
input.insert(QStringLiteral("freshLp"), std::move(freshLp));
|
||||
const AmmClientResult planResult = m_client->plan(input);
|
||||
if (!planResult.ok) {
|
||||
finishSubmit(
|
||||
submitGeneration,
|
||||
publicError(QStringLiteral("backend_error")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
const QJsonObject plan = planResult.value;
|
||||
if (plan.value(QStringLiteral("status")).toString()
|
||||
!= QStringLiteral("ready")) {
|
||||
const QString code = plan.value(QStringLiteral("code")).toString();
|
||||
finishSubmit(
|
||||
submitGeneration,
|
||||
publicError(code.isEmpty()
|
||||
? QStringLiteral("wallet_submission_failed") : code).toVariantMap());
|
||||
return;
|
||||
}
|
||||
|
||||
bool deadlineValid = false;
|
||||
const qulonglong deadline = plan.value(QStringLiteral("deadlineMs"))
|
||||
.toString().toULongLong(&deadlineValid);
|
||||
if (!deadlineValid
|
||||
|| static_cast<qulonglong>(QDateTime::currentMSecsSinceEpoch()) >= deadline) {
|
||||
finishSubmit(
|
||||
submitGeneration,
|
||||
publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
if (!submitIsCurrent(submitGeneration, walletGeneration))
|
||||
return;
|
||||
|
||||
WalletTransaction transaction {
|
||||
plan.value(QStringLiteral("programId")).toString(),
|
||||
jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()),
|
||||
jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()),
|
||||
jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()),
|
||||
};
|
||||
QPointer<NewPositionRuntime> guard(this);
|
||||
m_wallet->submitPublicTransactionAsync(
|
||||
transaction,
|
||||
[guard, submitGeneration, walletGeneration, deadlineMs =
|
||||
plan.value(QStringLiteral("deadlineMs")), affectedAccountIds =
|
||||
plan.value(QStringLiteral("affectedAccountIds"))](
|
||||
WalletSubmission submission) mutable {
|
||||
if (!guard
|
||||
|| !guard->submitIsCurrent(submitGeneration, walletGeneration))
|
||||
return;
|
||||
}
|
||||
if (walletGeneration != m_walletGeneration) {
|
||||
finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap());
|
||||
return;
|
||||
}
|
||||
const WalletSubmission submission = m_wallet->submitPublicTransaction({
|
||||
plan.value(QStringLiteral("programId")).toString(),
|
||||
accountIds,
|
||||
signingRequirements,
|
||||
instruction,
|
||||
});
|
||||
const QString transactionId = submission.accepted()
|
||||
? base58TransactionId(submission.nativeHash) : QString();
|
||||
if (transactionId.isEmpty()) {
|
||||
finish(publicError(QStringLiteral("wallet_submission_failed")).toVariantMap());
|
||||
const QString code = submission.failure
|
||||
== WalletFailure::WalletUnavailable
|
||||
? QStringLiteral("wallet_unavailable")
|
||||
: QStringLiteral("wallet_submission_failed");
|
||||
guard->finishSubmit(
|
||||
submitGeneration, publicError(code).toVariantMap());
|
||||
return;
|
||||
}
|
||||
finish(QJsonObject {
|
||||
guard->finishSubmit(submitGeneration, QJsonObject {
|
||||
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
|
||||
{ QStringLiteral("status"), QStringLiteral("submitted") },
|
||||
{ QStringLiteral("transactionId"), transactionId },
|
||||
{ QStringLiteral("nativeTransactionHash"), submission.nativeHash },
|
||||
{ QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) },
|
||||
{ QStringLiteral("deadlineMs"), deadlineMs },
|
||||
{ QStringLiteral("affectedAccountIds"),
|
||||
plan.value(QStringLiteral("affectedAccountIds")) },
|
||||
affectedAccountIds },
|
||||
}.toVariantMap());
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
#include <QVector>
|
||||
@ -15,7 +16,7 @@ class WalletProvider;
|
||||
class SequencerClient;
|
||||
struct WalletAccount;
|
||||
|
||||
class NewPositionRuntime {
|
||||
class NewPositionRuntime : public QObject {
|
||||
public:
|
||||
using ResultCallback = std::function<void(QVariantMap)>;
|
||||
|
||||
@ -41,6 +42,7 @@ public:
|
||||
const ActiveNetworkSnapshot& network,
|
||||
bool walletCanSubmit,
|
||||
ResultCallback callback);
|
||||
void cancelSubmit();
|
||||
|
||||
QVariantMap context(const QVariantMap& request,
|
||||
const ActiveNetworkSnapshot& network,
|
||||
@ -65,7 +67,15 @@ private:
|
||||
const ActiveNetworkSnapshot& network,
|
||||
bool walletOpen,
|
||||
bool forceRefresh,
|
||||
std::function<void(QJsonObject, QJsonObject)> callback) const;
|
||||
std::function<void(QJsonObject, QJsonObject)> callback);
|
||||
void submitPlanAsync(QJsonObject input,
|
||||
const QString& quoteHash,
|
||||
QJsonValue freshLp,
|
||||
quint64 submitGeneration,
|
||||
quint64 walletGeneration);
|
||||
void finishSubmit(quint64 submitGeneration, QVariantMap result);
|
||||
bool submitIsCurrent(quint64 submitGeneration,
|
||||
quint64 walletGeneration) const;
|
||||
|
||||
WalletProvider* m_wallet;
|
||||
AmmClient* m_client;
|
||||
@ -73,4 +83,6 @@ private:
|
||||
QStringList m_walletPublicAccountIds;
|
||||
bool m_submitInFlight = false;
|
||||
quint64 m_walletGeneration = 0;
|
||||
quint64 m_submitGeneration = 0;
|
||||
ResultCallback m_submitCallback;
|
||||
};
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
#include <QTemporaryFile>
|
||||
#include <QTimer>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
@ -143,6 +145,15 @@ namespace {
|
||||
return first && second;
|
||||
}
|
||||
|
||||
bool waitForCondition(const std::function<bool()>& condition)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
while (!condition() && timer.elapsed() < 3000)
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
|
||||
return condition();
|
||||
}
|
||||
|
||||
class FakeWallet final : public WalletProvider {
|
||||
public:
|
||||
WalletSession connect(const WalletPaths&) override
|
||||
@ -182,6 +193,17 @@ namespace {
|
||||
return creation;
|
||||
}
|
||||
|
||||
void createAccountAsync(bool isPublic, AccountCreationCallback callback) override
|
||||
{
|
||||
WalletAccountCreation creation = createAccount(isPublic);
|
||||
if (deferAccountCreation) {
|
||||
pendingAccountCreation = std::move(callback);
|
||||
pendingCreation = std::move(creation);
|
||||
} else {
|
||||
callback(std::move(creation));
|
||||
}
|
||||
}
|
||||
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override
|
||||
{
|
||||
WalletAccountRead read;
|
||||
@ -197,6 +219,32 @@ namespace {
|
||||
return { WalletFailure::None, transactionHash };
|
||||
}
|
||||
|
||||
void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) override
|
||||
{
|
||||
WalletSubmission submission = submitPublicTransaction(transaction);
|
||||
if (deferSubmission) {
|
||||
pendingSubmissionCallback = std::move(callback);
|
||||
pendingSubmission = std::move(submission);
|
||||
} else {
|
||||
callback(std::move(submission));
|
||||
}
|
||||
}
|
||||
|
||||
void finishAccountCreation()
|
||||
{
|
||||
AccountCreationCallback callback = std::move(pendingAccountCreation);
|
||||
if (callback)
|
||||
callback(std::move(pendingCreation));
|
||||
}
|
||||
|
||||
void finishSubmission()
|
||||
{
|
||||
SubmissionCallback callback = std::move(pendingSubmissionCallback);
|
||||
if (callback)
|
||||
callback(std::move(pendingSubmission));
|
||||
}
|
||||
|
||||
void disconnect() override {}
|
||||
|
||||
QString transactionHash = QStringLiteral(
|
||||
@ -205,6 +253,12 @@ namespace {
|
||||
int createdAccounts = 0;
|
||||
int submissions = 0;
|
||||
WalletTransaction submitted;
|
||||
bool deferAccountCreation = false;
|
||||
bool deferSubmission = false;
|
||||
AccountCreationCallback pendingAccountCreation;
|
||||
SubmissionCallback pendingSubmissionCallback;
|
||||
WalletAccountCreation pendingCreation;
|
||||
WalletSubmission pendingSubmission;
|
||||
};
|
||||
|
||||
class FakeAmmClient final : public AmmClient {
|
||||
@ -625,5 +679,113 @@ int main(int argc, char** argv)
|
||||
"wallet change should cancel submit before side effects"))
|
||||
return 1;
|
||||
|
||||
LocalRpcServer asyncSubmitServer;
|
||||
if (!expect(asyncSubmitServer.listen(), "async-submit sequencer should listen"))
|
||||
return 1;
|
||||
if (!expect(sequencerConfig.resize(0) && sequencerConfig.seek(0),
|
||||
"async-submit config should rewind"))
|
||||
return 1;
|
||||
sequencerConfig.write(QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("sequencer_addr"), asyncSubmitServer.endpoint() },
|
||||
}).toJson(QJsonDocument::Compact));
|
||||
sequencerConfig.flush();
|
||||
if (!expect(sequencer.configure(sequencerConfig.fileName()),
|
||||
"async-submit sequencer should configure"))
|
||||
return 1;
|
||||
|
||||
FakeWallet asyncWallet;
|
||||
asyncWallet.deferAccountCreation = true;
|
||||
asyncWallet.deferSubmission = true;
|
||||
FakeAmmClient asyncClient;
|
||||
NewPositionRuntime asyncRuntime(&asyncWallet, &asyncClient, &sequencer);
|
||||
int asyncCallbackCount = 0;
|
||||
QVariantMap asyncResult;
|
||||
asyncRuntime.submitAsync(
|
||||
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
|
||||
[&](QVariantMap result) {
|
||||
++asyncCallbackCount;
|
||||
asyncResult = std::move(result);
|
||||
});
|
||||
if (!expect(waitForCondition([&]() { return asyncWallet.createdAccounts == 1; }),
|
||||
"submit should asynchronously request a fresh account"))
|
||||
return 1;
|
||||
if (!expect(asyncCallbackCount == 0 && asyncWallet.submissions == 0,
|
||||
"account creation should not block or finish submission"))
|
||||
return 1;
|
||||
|
||||
QVariantMap concurrentResult;
|
||||
asyncRuntime.submitAsync(
|
||||
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
|
||||
[&](QVariantMap result) { concurrentResult = std::move(result); });
|
||||
if (!expect(concurrentResult.value(QStringLiteral("code")).toString()
|
||||
== QStringLiteral("submit_in_progress"),
|
||||
"concurrent submit should fail while async mutation is pending"))
|
||||
return 1;
|
||||
|
||||
asyncWallet.finishAccountCreation();
|
||||
if (!expect(waitForCondition([&]() { return asyncWallet.submissions == 1; }),
|
||||
"fresh account completion should dispatch transaction"))
|
||||
return 1;
|
||||
if (!expect(asyncCallbackCount == 0,
|
||||
"transaction submission should remain asynchronous"))
|
||||
return 1;
|
||||
asyncWallet.finishSubmission();
|
||||
if (!expect(asyncCallbackCount == 1
|
||||
&& asyncResult.value(QStringLiteral("status")).toString()
|
||||
== QStringLiteral("submitted"),
|
||||
"async wallet completion should finish exactly once"))
|
||||
return 1;
|
||||
|
||||
FakeWallet cancelledMutationWallet;
|
||||
cancelledMutationWallet.deferAccountCreation = true;
|
||||
FakeAmmClient cancelledMutationClient;
|
||||
NewPositionRuntime cancelledMutationRuntime(
|
||||
&cancelledMutationWallet, &cancelledMutationClient, &sequencer);
|
||||
int cancelledMutationCallbacks = 0;
|
||||
QVariantMap cancelledMutationResult;
|
||||
cancelledMutationRuntime.submitAsync(
|
||||
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
|
||||
[&](QVariantMap result) {
|
||||
++cancelledMutationCallbacks;
|
||||
cancelledMutationResult = std::move(result);
|
||||
});
|
||||
if (!expect(waitForCondition(
|
||||
[&]() { return cancelledMutationWallet.createdAccounts == 1; }),
|
||||
"cancellable submit should reach account creation"))
|
||||
return 1;
|
||||
cancelledMutationRuntime.clearWalletAccounts();
|
||||
if (!expect(cancelledMutationCallbacks == 1
|
||||
&& cancelledMutationResult.value(QStringLiteral("code")).toString()
|
||||
== QStringLiteral("wallet_unavailable"),
|
||||
"wallet change should immediately cancel pending mutation"))
|
||||
return 1;
|
||||
cancelledMutationWallet.finishAccountCreation();
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
|
||||
if (!expect(cancelledMutationCallbacks == 1
|
||||
&& cancelledMutationWallet.submissions == 0,
|
||||
"stale account completion should have no transaction side effect"))
|
||||
return 1;
|
||||
|
||||
FakeWallet destroyedRuntimeWallet;
|
||||
destroyedRuntimeWallet.deferSubmission = true;
|
||||
FakeAmmClient destroyedRuntimeClient;
|
||||
destroyedRuntimeClient.requiresFreshLp = false;
|
||||
int destroyedRuntimeCallbacks = 0;
|
||||
auto destroyedRuntime = std::make_unique<NewPositionRuntime>(
|
||||
&destroyedRuntimeWallet, &destroyedRuntimeClient, &sequencer);
|
||||
destroyedRuntime->submitAsync(
|
||||
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
|
||||
[&](QVariantMap) { ++destroyedRuntimeCallbacks; });
|
||||
if (!expect(waitForCondition(
|
||||
[&]() { return destroyedRuntimeWallet.submissions == 1; }),
|
||||
"lifetime test should reach async submission"))
|
||||
return 1;
|
||||
destroyedRuntime.reset();
|
||||
destroyedRuntimeWallet.finishSubmission();
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
|
||||
if (!expect(destroyedRuntimeCallbacks == 0,
|
||||
"destroyed runtime should ignore late wallet completion"))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
@ -109,6 +110,58 @@ WalletAccountRead parsePublicAccount(const QString& accountId, const QString& pa
|
||||
read.dataHex = data;
|
||||
return read;
|
||||
}
|
||||
|
||||
bool encodeTransaction(const WalletTransaction& transaction,
|
||||
QVariantList* signingRequirements,
|
||||
QVariantList* instruction)
|
||||
{
|
||||
if (!isHex(transaction.programId, 64)
|
||||
|| transaction.accountIds.size() != transaction.signingRequirements.size()) {
|
||||
return false;
|
||||
}
|
||||
for (const QString& accountId : transaction.accountIds) {
|
||||
if (!isHex(accountId, 64))
|
||||
return false;
|
||||
}
|
||||
|
||||
signingRequirements->reserve(transaction.signingRequirements.size());
|
||||
for (bool required : transaction.signingRequirements)
|
||||
signingRequirements->append(required);
|
||||
|
||||
instruction->reserve(transaction.instruction.size());
|
||||
for (quint32 word : transaction.instruction)
|
||||
instruction->append(word);
|
||||
return true;
|
||||
}
|
||||
|
||||
WalletSubmission parseSubmission(const QString& response)
|
||||
{
|
||||
WalletSubmission submission;
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response.toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
submission.failure = WalletFailure::SubmissionFailed;
|
||||
return submission;
|
||||
}
|
||||
|
||||
const QJsonObject result = document.object();
|
||||
const QJsonValue success = result.value(QStringLiteral("success"));
|
||||
const QJsonValue error = result.value(QStringLiteral("error"));
|
||||
const QString hash = result.value(QStringLiteral("tx_hash")).toString();
|
||||
const bool emptyError = error.isUndefined()
|
||||
|| error.isNull()
|
||||
|| (error.isString() && error.toString().isEmpty());
|
||||
if (!success.isBool()
|
||||
|| !success.toBool()
|
||||
|| !emptyError
|
||||
|| !isHex(hash, 64, false)) {
|
||||
submission.failure = WalletFailure::SubmissionFailed;
|
||||
return submission;
|
||||
}
|
||||
|
||||
submission.nativeHash = hash.toLower();
|
||||
return submission;
|
||||
}
|
||||
}
|
||||
|
||||
struct LogosWalletProvider::Impl {
|
||||
@ -138,12 +191,16 @@ LogosWalletProvider::LogosWalletProvider(LogosModules* logos)
|
||||
|
||||
LogosWalletProvider::~LogosWalletProvider()
|
||||
{
|
||||
++m_generation;
|
||||
++m_sessionGeneration;
|
||||
if (m_connected)
|
||||
save();
|
||||
}
|
||||
|
||||
WalletSession LogosWalletProvider::connect(const WalletPaths& paths)
|
||||
{
|
||||
++m_generation;
|
||||
++m_sessionGeneration;
|
||||
clearSnapshot();
|
||||
if (!m_impl->logos)
|
||||
return failedSession(WalletFailure::WalletUnavailable);
|
||||
@ -168,6 +225,7 @@ WalletSession LogosWalletProvider::connect(const WalletPaths& paths)
|
||||
|
||||
void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback callback)
|
||||
{
|
||||
++m_sessionGeneration;
|
||||
clearSnapshot();
|
||||
const quint64 generation = ++m_generation;
|
||||
if (!m_impl->logos) {
|
||||
@ -239,6 +297,8 @@ void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback
|
||||
WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths,
|
||||
const QString& password)
|
||||
{
|
||||
++m_generation;
|
||||
++m_sessionGeneration;
|
||||
clearSnapshot();
|
||||
if (!m_impl->logos)
|
||||
return failedCreation(WalletFailure::WalletUnavailable);
|
||||
@ -368,6 +428,167 @@ WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic)
|
||||
return creation;
|
||||
}
|
||||
|
||||
void LogosWalletProvider::createAccountAsync(bool isPublic,
|
||||
AccountCreationCallback callback)
|
||||
{
|
||||
QPointer<LogosWalletProvider> guard(this);
|
||||
if (!m_connected || !m_impl->logos) {
|
||||
QTimer::singleShot(0, [guard, callback = std::move(callback)]() mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
WalletAccountCreation creation;
|
||||
creation.failure = WalletFailure::WalletUnavailable;
|
||||
callback(std::move(creation));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const quint64 sessionGeneration = m_sessionGeneration;
|
||||
auto finish = [guard, sessionGeneration, isPublic,
|
||||
callback = std::move(callback)](
|
||||
WalletAccountCreation creation, QString fallbackBalance) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletAccountCreation failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
callback(std::move(failed));
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard->m_snapshotReady) {
|
||||
WalletAccount account;
|
||||
account.address = creation.accountId;
|
||||
account.isPublic = isPublic;
|
||||
if (isPublic && creation.publicAccount.ok()) {
|
||||
account.balance = littleEndianU128ToDecimal(
|
||||
creation.publicAccount.balanceHex);
|
||||
auto read = std::find_if(
|
||||
guard->m_snapshot.publicAccountReads.begin(),
|
||||
guard->m_snapshot.publicAccountReads.end(),
|
||||
[&creation](const WalletAccountRead& existing) {
|
||||
return existing.accountId == creation.accountId;
|
||||
});
|
||||
if (read == guard->m_snapshot.publicAccountReads.end())
|
||||
guard->m_snapshot.publicAccountReads.append(creation.publicAccount);
|
||||
else
|
||||
*read = creation.publicAccount;
|
||||
} else {
|
||||
account.balance = std::move(fallbackBalance);
|
||||
}
|
||||
auto existing = std::find_if(
|
||||
guard->m_snapshot.accounts.begin(),
|
||||
guard->m_snapshot.accounts.end(),
|
||||
[&creation](const WalletAccount& candidate) {
|
||||
return candidate.address == creation.accountId;
|
||||
});
|
||||
if (existing == guard->m_snapshot.accounts.end())
|
||||
guard->m_snapshot.accounts.append(account);
|
||||
else
|
||||
*existing = account;
|
||||
creation.snapshot = guard->m_snapshot;
|
||||
}
|
||||
callback(std::move(creation));
|
||||
};
|
||||
|
||||
auto created = [guard, sessionGeneration, isPublic,
|
||||
finish = std::move(finish)](QString accountId) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletAccountCreation failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
finish(std::move(failed), {});
|
||||
return;
|
||||
}
|
||||
|
||||
WalletAccountCreation creation;
|
||||
creation.accountId = std::move(accountId);
|
||||
if (!isHex(creation.accountId, 64)) {
|
||||
creation.failure = WalletFailure::CreateFailed;
|
||||
finish(std::move(creation), {});
|
||||
return;
|
||||
}
|
||||
|
||||
guard->m_impl->logos->logos_execution_zone.saveAsync(
|
||||
[guard, sessionGeneration, isPublic, creation = std::move(creation),
|
||||
finish = std::move(finish)](int result) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletAccountCreation failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
finish(std::move(failed), {});
|
||||
return;
|
||||
}
|
||||
if (result != WALLET_FFI_SUCCESS) {
|
||||
creation.failure = WalletFailure::SaveFailed;
|
||||
finish(std::move(creation), {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPublic) {
|
||||
guard->m_impl->logos->logos_execution_zone.get_balanceAsync(
|
||||
creation.accountId, false,
|
||||
[guard, sessionGeneration, creation = std::move(creation),
|
||||
finish = std::move(finish)](QString balance) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletAccountCreation failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
finish(std::move(failed), {});
|
||||
return;
|
||||
}
|
||||
finish(std::move(creation), std::move(balance));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const QString accountId = creation.accountId;
|
||||
guard->m_impl->logos->logos_execution_zone.get_account_publicAsync(
|
||||
accountId,
|
||||
[guard, sessionGeneration, creation = std::move(creation),
|
||||
finish = std::move(finish)](QString payload) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletAccountCreation failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
finish(std::move(failed), {});
|
||||
return;
|
||||
}
|
||||
creation.publicAccount = parsePublicAccount(
|
||||
creation.accountId, payload);
|
||||
if (creation.publicAccount.ok()) {
|
||||
finish(std::move(creation), {});
|
||||
return;
|
||||
}
|
||||
guard->m_impl->logos->logos_execution_zone.get_balanceAsync(
|
||||
creation.accountId, true,
|
||||
[guard, sessionGeneration,
|
||||
creation = std::move(creation),
|
||||
finish = std::move(finish)](QString balance) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletAccountCreation failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
finish(std::move(failed), {});
|
||||
return;
|
||||
}
|
||||
finish(std::move(creation), std::move(balance));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (isPublic)
|
||||
m_impl->logos->logos_execution_zone.create_account_publicAsync(std::move(created));
|
||||
else
|
||||
m_impl->logos->logos_execution_zone.create_account_privateAsync(std::move(created));
|
||||
}
|
||||
|
||||
WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const
|
||||
{
|
||||
if (!m_impl->logos || !isHex(accountId, 64))
|
||||
@ -385,27 +606,12 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction(
|
||||
submission.failure = WalletFailure::WalletUnavailable;
|
||||
return submission;
|
||||
}
|
||||
if (!isHex(transaction.programId, 64)
|
||||
|| transaction.accountIds.size() != transaction.signingRequirements.size()) {
|
||||
QVariantList signingRequirements;
|
||||
QVariantList instruction;
|
||||
if (!encodeTransaction(transaction, &signingRequirements, &instruction)) {
|
||||
submission.failure = WalletFailure::InvalidRequest;
|
||||
return submission;
|
||||
}
|
||||
for (const QString& accountId : transaction.accountIds) {
|
||||
if (!isHex(accountId, 64)) {
|
||||
submission.failure = WalletFailure::InvalidRequest;
|
||||
return submission;
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList signingRequirements;
|
||||
signingRequirements.reserve(transaction.signingRequirements.size());
|
||||
for (bool required : transaction.signingRequirements)
|
||||
signingRequirements.append(required);
|
||||
|
||||
QVariantList instruction;
|
||||
instruction.reserve(transaction.instruction.size());
|
||||
for (quint32 word : transaction.instruction)
|
||||
instruction.append(word);
|
||||
|
||||
const QString response =
|
||||
m_impl->logos->logos_execution_zone.send_generic_public_transaction(
|
||||
@ -413,36 +619,60 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction(
|
||||
signingRequirements,
|
||||
QVariant::fromValue(instruction),
|
||||
transaction.programId);
|
||||
return parseSubmission(response);
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response.toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
submission.failure = WalletFailure::SubmissionFailed;
|
||||
return submission;
|
||||
void LogosWalletProvider::submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback)
|
||||
{
|
||||
QPointer<LogosWalletProvider> guard(this);
|
||||
WalletSubmission submission;
|
||||
if (!m_connected || !m_impl->logos) {
|
||||
submission.failure = WalletFailure::WalletUnavailable;
|
||||
QTimer::singleShot(0, [guard, callback = std::move(callback),
|
||||
submission = std::move(submission)]() mutable {
|
||||
if (guard)
|
||||
callback(std::move(submission));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject result = document.object();
|
||||
const QJsonValue success = result.value(QStringLiteral("success"));
|
||||
const QJsonValue error = result.value(QStringLiteral("error"));
|
||||
const QString hash = result.value(QStringLiteral("tx_hash")).toString();
|
||||
const bool emptyError = error.isUndefined()
|
||||
|| error.isNull()
|
||||
|| (error.isString() && error.toString().isEmpty());
|
||||
if (!success.isBool()
|
||||
|| !success.toBool()
|
||||
|| !emptyError
|
||||
|| !isHex(hash, 64, false)) {
|
||||
submission.failure = WalletFailure::SubmissionFailed;
|
||||
return submission;
|
||||
QVariantList signingRequirements;
|
||||
QVariantList instruction;
|
||||
if (!encodeTransaction(transaction, &signingRequirements, &instruction)) {
|
||||
submission.failure = WalletFailure::InvalidRequest;
|
||||
QTimer::singleShot(0, [guard, callback = std::move(callback),
|
||||
submission = std::move(submission)]() mutable {
|
||||
if (guard)
|
||||
callback(std::move(submission));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
submission.nativeHash = hash.toLower();
|
||||
return submission;
|
||||
const quint64 sessionGeneration = m_sessionGeneration;
|
||||
m_impl->logos->logos_execution_zone.send_generic_public_transactionAsync(
|
||||
transaction.accountIds,
|
||||
signingRequirements,
|
||||
QVariant::fromValue(instruction),
|
||||
transaction.programId,
|
||||
[guard, sessionGeneration, callback = std::move(callback)](
|
||||
QString response) mutable {
|
||||
if (!guard)
|
||||
return;
|
||||
if (sessionGeneration != guard->m_sessionGeneration) {
|
||||
WalletSubmission failed;
|
||||
failed.failure = WalletFailure::WalletUnavailable;
|
||||
callback(std::move(failed));
|
||||
return;
|
||||
}
|
||||
callback(parseSubmission(response));
|
||||
});
|
||||
}
|
||||
|
||||
void LogosWalletProvider::disconnect()
|
||||
{
|
||||
++m_generation;
|
||||
++m_sessionGeneration;
|
||||
if (m_connected)
|
||||
save();
|
||||
clearSnapshot();
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class LogosAPI;
|
||||
struct LogosModules;
|
||||
|
||||
class LogosWalletProvider final : public WalletProvider {
|
||||
class LogosWalletProvider final : public QObject, public WalletProvider {
|
||||
public:
|
||||
explicit LogosWalletProvider(LogosAPI* api);
|
||||
explicit LogosWalletProvider(LogosModules* logos);
|
||||
@ -21,9 +23,12 @@ public:
|
||||
void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override;
|
||||
void clearSnapshot() override;
|
||||
WalletAccountCreation createAccount(bool isPublic) override;
|
||||
void createAccountAsync(bool isPublic, AccountCreationCallback callback) override;
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override;
|
||||
WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) override;
|
||||
void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) override;
|
||||
void disconnect() override;
|
||||
|
||||
private:
|
||||
@ -38,4 +43,5 @@ private:
|
||||
bool m_snapshotReady = false;
|
||||
bool m_connected = false;
|
||||
quint64 m_generation = 0;
|
||||
quint64 m_sessionGeneration = 0;
|
||||
};
|
||||
|
||||
@ -95,6 +95,8 @@ class WalletProvider {
|
||||
public:
|
||||
using SessionCallback = std::function<void(WalletSession)>;
|
||||
using SnapshotCallback = std::function<void(WalletSnapshot)>;
|
||||
using AccountCreationCallback = std::function<void(WalletAccountCreation)>;
|
||||
using SubmissionCallback = std::function<void(WalletSubmission)>;
|
||||
|
||||
virtual ~WalletProvider() = default;
|
||||
|
||||
@ -106,8 +108,11 @@ public:
|
||||
virtual void snapshotAsync(bool forceRefresh, SnapshotCallback callback) = 0;
|
||||
virtual void clearSnapshot() = 0;
|
||||
virtual WalletAccountCreation createAccount(bool isPublic) = 0;
|
||||
virtual void createAccountAsync(bool isPublic, AccountCreationCallback callback) = 0;
|
||||
virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0;
|
||||
virtual WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) = 0;
|
||||
virtual void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
};
|
||||
|
||||
@ -61,6 +61,9 @@ private slots:
|
||||
void createdAccountDoesNotRescanWallet();
|
||||
void dispatchesExactGenericTransaction();
|
||||
void rejectsInvalidSubmissionResponses();
|
||||
void walletMutationsUseAsyncSdk();
|
||||
void staleAsyncMutationCannotCrossSession();
|
||||
void destroyedProviderIgnoresLateMutation();
|
||||
void exposesStableAccountModelRoles();
|
||||
void fakeProviderImplementsConsumerContract();
|
||||
void controllerOwnsUiWalletFlow();
|
||||
@ -349,6 +352,108 @@ void LogosWalletProviderTest::rejectsInvalidSubmissionResponses()
|
||||
WalletFailure::InvalidRequest);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::walletMutationsUseAsyncSdk()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
|
||||
modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
|
||||
modules.logos_execution_zone.transactionResponse = QStringLiteral(
|
||||
R"({"success":true,"tx_hash":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"})");
|
||||
LogosWalletProvider provider(&modules);
|
||||
QVERIFY(provider.connect({}).ok());
|
||||
|
||||
modules.logos_execution_zone.deferPublicAccountCreation = true;
|
||||
bool creationFinished = false;
|
||||
WalletAccountCreation creation;
|
||||
const int listCalls = modules.logos_execution_zone.listCalls;
|
||||
const int syncCalls = modules.logos_execution_zone.syncCalls;
|
||||
provider.createAccountAsync(true, [&](WalletAccountCreation result) {
|
||||
creation = std::move(result);
|
||||
creationFinished = true;
|
||||
});
|
||||
QVERIFY(!creationFinished);
|
||||
QVERIFY(modules.logos_execution_zone.pendingPublicAccountCreation);
|
||||
modules.logos_execution_zone.finishPublicAccountCreation();
|
||||
QVERIFY(creationFinished);
|
||||
QVERIFY(creation.ok());
|
||||
QVERIFY(creation.publicAccount.ok());
|
||||
QCOMPARE(creation.accountId, ACCOUNT_A);
|
||||
QCOMPARE(creation.snapshot.accounts.size(), 1);
|
||||
QCOMPARE(modules.logos_execution_zone.listCalls, listCalls);
|
||||
QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls);
|
||||
|
||||
WalletTransaction transaction {
|
||||
PROGRAM_ID,
|
||||
{ ACCOUNT_A, ACCOUNT_B },
|
||||
{ true, false },
|
||||
{ 7, 0, 4294967295U },
|
||||
};
|
||||
modules.logos_execution_zone.deferSubmission = true;
|
||||
bool submissionFinished = false;
|
||||
WalletSubmission submission;
|
||||
provider.submitPublicTransactionAsync(
|
||||
transaction, [&](WalletSubmission result) {
|
||||
submission = std::move(result);
|
||||
submissionFinished = true;
|
||||
});
|
||||
QVERIFY(!submissionFinished);
|
||||
QCOMPARE(modules.logos_execution_zone.submittedProgramId, PROGRAM_ID);
|
||||
QCOMPARE(modules.logos_execution_zone.submittedAccountIds, transaction.accountIds);
|
||||
QCOMPARE(modules.logos_execution_zone.submittedSigningRequirements,
|
||||
QVariantList({ true, false }));
|
||||
QCOMPARE(modules.logos_execution_zone.submittedInstruction.toList(),
|
||||
QVariantList({ 7U, 0U, 4294967295U }));
|
||||
modules.logos_execution_zone.finishSubmission();
|
||||
QVERIFY(submissionFinished);
|
||||
QVERIFY(submission.accepted());
|
||||
QCOMPARE(submission.nativeHash, QString(64, QLatin1Char('a')));
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::staleAsyncMutationCannotCrossSession()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
|
||||
modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
|
||||
modules.logos_execution_zone.deferPublicAccountCreation = true;
|
||||
LogosWalletProvider provider(&modules);
|
||||
QVERIFY(provider.connect({}).ok());
|
||||
|
||||
int callbackCount = 0;
|
||||
WalletAccountCreation creation;
|
||||
provider.createAccountAsync(true, [&](WalletAccountCreation result) {
|
||||
++callbackCount;
|
||||
creation = std::move(result);
|
||||
});
|
||||
provider.disconnect();
|
||||
modules.logos_execution_zone.finishPublicAccountCreation();
|
||||
|
||||
QCOMPARE(callbackCount, 1);
|
||||
QCOMPARE(creation.failure, WalletFailure::WalletUnavailable);
|
||||
QCOMPARE(modules.logos_execution_zone.publicReadCalls, 0);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::destroyedProviderIgnoresLateMutation()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
modules.logos_execution_zone.transactionResponse = QStringLiteral(
|
||||
R"({"success":true,"tx_hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})");
|
||||
modules.logos_execution_zone.deferSubmission = true;
|
||||
int callbackCount = 0;
|
||||
{
|
||||
LogosWalletProvider provider(&modules);
|
||||
QVERIFY(provider.connect({}).ok());
|
||||
provider.submitPublicTransactionAsync(
|
||||
{ PROGRAM_ID, { ACCOUNT_A }, { true }, { 1 } },
|
||||
[&](WalletSubmission) { ++callbackCount; });
|
||||
}
|
||||
|
||||
modules.logos_execution_zone.finishSubmission();
|
||||
QCOMPARE(callbackCount, 0);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
{
|
||||
WalletAccountModel model;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <QVariantList>
|
||||
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
@ -32,6 +33,10 @@ public:
|
||||
int listCalls = 0;
|
||||
int publicReadCalls = 0;
|
||||
int submitCalls = 0;
|
||||
bool deferPublicAccountCreation = false;
|
||||
bool deferSubmission = false;
|
||||
std::function<void(QString)> pendingPublicAccountCreation;
|
||||
std::function<void(QString)> pendingSubmission;
|
||||
QString openedConfig;
|
||||
QString openedStorage;
|
||||
QString createdConfig;
|
||||
@ -77,6 +82,17 @@ public:
|
||||
|
||||
QString create_account_public() { return publicAccountId; }
|
||||
QString create_account_private() { return privateAccountId; }
|
||||
void create_account_publicAsync(std::function<void(QString)> callback)
|
||||
{
|
||||
if (deferPublicAccountCreation)
|
||||
pendingPublicAccountCreation = std::move(callback);
|
||||
else
|
||||
callback(create_account_public());
|
||||
}
|
||||
void create_account_privateAsync(std::function<void(QString)> callback)
|
||||
{
|
||||
callback(create_account_private());
|
||||
}
|
||||
|
||||
int get_last_synced_block() const { return lastSyncedBlock; }
|
||||
int get_current_block_height() const { return currentBlockHeight; }
|
||||
@ -150,6 +166,38 @@ public:
|
||||
submittedProgramId = programId;
|
||||
return transactionResponse;
|
||||
}
|
||||
|
||||
void send_generic_public_transactionAsync(
|
||||
const QStringList& accountIds,
|
||||
const QVariantList& signingRequirements,
|
||||
const QVariant& instruction,
|
||||
const QString& programId,
|
||||
std::function<void(QString)> callback)
|
||||
{
|
||||
++submitCalls;
|
||||
submittedAccountIds = accountIds;
|
||||
submittedSigningRequirements = signingRequirements;
|
||||
submittedInstruction = instruction;
|
||||
submittedProgramId = programId;
|
||||
if (deferSubmission)
|
||||
pendingSubmission = std::move(callback);
|
||||
else
|
||||
callback(transactionResponse);
|
||||
}
|
||||
|
||||
void finishPublicAccountCreation()
|
||||
{
|
||||
auto callback = std::move(pendingPublicAccountCreation);
|
||||
if (callback)
|
||||
callback(publicAccountId);
|
||||
}
|
||||
|
||||
void finishSubmission()
|
||||
{
|
||||
auto callback = std::move(pendingSubmission);
|
||||
if (callback)
|
||||
callback(transactionResponse);
|
||||
}
|
||||
};
|
||||
|
||||
struct LogosModules {
|
||||
|
||||
@ -80,6 +80,11 @@ public:
|
||||
return createAccountResult;
|
||||
}
|
||||
|
||||
void createAccountAsync(bool isPublic, AccountCreationCallback callback) override
|
||||
{
|
||||
callback(createAccount(isPublic));
|
||||
}
|
||||
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override
|
||||
{
|
||||
++readCalls;
|
||||
@ -96,6 +101,12 @@ public:
|
||||
return submissionResult;
|
||||
}
|
||||
|
||||
void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) override
|
||||
{
|
||||
callback(submitPublicTransaction(transaction));
|
||||
}
|
||||
|
||||
void disconnect() override { ++disconnectCalls; }
|
||||
|
||||
void finishConnect()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user