lez-programs/apps/amm/src/NewPositionRuntime.cpp
2026-07-17 21:38:24 -03:00

1093 lines
48 KiB
C++

#include "NewPositionRuntime.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <utility>
#include <QByteArray>
#include <QDateTime>
#include <QJsonObject>
#include <QPointer>
#include <QScopedValueRollback>
#include <libbase58.h>
#include "AmmClient.h"
#include "SequencerClient.h"
#include "WalletProvider.h"
namespace {
const char SCHEMA[] = "new-position.v2";
constexpr qsizetype HASH_BYTES = 32;
constexpr qsizetype ACCOUNT_ID_HEX_SIZE = HASH_BYTES * 2;
constexpr std::size_t BASE58_BUFFER_SIZE = 45;
QString base58TransactionId(const QString& transactionHash)
{
const QByteArray hex = transactionHash.toLatin1();
const QByteArray bytes = QByteArray::fromHex(hex);
if (bytes.size() != HASH_BYTES || bytes.toHex() != hex)
return {};
std::array<char, BASE58_BUFFER_SIZE> encoded {};
std::size_t size = encoded.size();
if (!b58enc(encoded.data(), &size, bytes.constData(),
static_cast<std::size_t>(bytes.size()))) {
return {};
}
return QString::fromLatin1(
encoded.data(), static_cast<qsizetype>(size - 1));
}
QString accountIdHex(const QString& accountId)
{
const QByteArray encoded = accountId.toLatin1();
std::array<unsigned char, HASH_BYTES> bytes {};
std::size_t size = bytes.size();
if (!b58tobin(bytes.data(), &size, encoded.constData(),
static_cast<std::size_t>(encoded.size()))
|| size != bytes.size()) {
return {};
}
return QString::fromLatin1(
QByteArray(reinterpret_cast<const char*>(bytes.data()),
static_cast<qsizetype>(bytes.size())).toHex());
}
bool isLowerHex(const QString& value, qsizetype size)
{
if (value.size() != size)
return false;
for (qsizetype index = 0; index < value.size(); ++index) {
const char16_t character = value.at(index).unicode();
if (!((character >= u'0' && character <= u'9')
|| (character >= u'a' && character <= u'f'))) {
return false;
}
}
return true;
}
bool isDefaultAccountRead(const WalletAccountRead& read,
const QString& expectedAccountId)
{
return read.ok()
&& read.accountId == expectedAccountId
&& isLowerHex(read.accountId, ACCOUNT_ID_HEX_SIZE)
&& read.programOwner == QString(ACCOUNT_ID_HEX_SIZE, QLatin1Char('0'))
&& read.balanceHex == QString(32, QLatin1Char('0'))
&& read.nonceHex == QString(32, QLatin1Char('0'))
&& read.dataHex.isEmpty();
}
QJsonObject issue(const QString& code,
const QJsonArray& blockingFields = {})
{
return {
{ QStringLiteral("code"), code },
{ QStringLiteral("recoverable"), true },
{ QStringLiteral("blockingFields"), blockingFields },
{ QStringLiteral("details"), QJsonObject() },
};
}
QJsonObject publicError(const QString& code,
const QJsonArray& blockingFields = {},
const QJsonObject& details = {})
{
QJsonObject error = issue(code, blockingFields);
error.insert(QStringLiteral("details"), details);
return {
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
{ QStringLiteral("status"), QStringLiteral("error") },
{ QStringLiteral("canSubmit"), false },
{ QStringLiteral("code"), code },
{ QStringLiteral("errors"), QJsonArray { error } },
{ QStringLiteral("warnings"), QJsonArray() },
{ QStringLiteral("accountPreview"), QJsonArray() },
};
}
QJsonObject contextState(const QString& status,
const ActiveNetworkSnapshot& network,
const QString& code = {})
{
QJsonObject state {
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
{ QStringLiteral("status"), status },
{ QStringLiteral("networkId"), network.id },
{ QStringLiteral("networkFingerprint"), network.fingerprint },
{ QStringLiteral("tokens"), QJsonArray() },
{ QStringLiteral("feeTiers"), QJsonArray() },
{ QStringLiteral("warnings"), QJsonArray() },
};
if (!code.isEmpty())
state.insert(QStringLiteral("code"), code);
return state;
}
QJsonArray variantStringArray(const QVariant& value)
{
QJsonArray result;
for (const QVariant& item : value.toList())
result.append(item.toString());
return result;
}
QStringList jsonStringList(const QJsonArray& values)
{
QStringList result;
result.reserve(values.size());
for (const QJsonValue& value : values)
result.append(value.toString());
return result;
}
QVector<bool> jsonBoolList(const QJsonArray& values)
{
QVector<bool> result;
result.reserve(values.size());
for (const QJsonValue& value : values)
result.append(value.toBool());
return result;
}
QVector<quint32> jsonUIntList(const QJsonArray& values)
{
QVector<quint32> result;
result.reserve(values.size());
for (const QJsonValue& value : values)
result.append(static_cast<quint32>(value.toInteger()));
return result;
}
QJsonObject accountReadJson(const WalletAccountRead& read)
{
QJsonObject result {
{ QStringLiteral("id"), read.accountId },
{ QStringLiteral("status"), read.status },
};
if (read.ok()) {
result.insert(QStringLiteral("account"), QJsonObject {
{ QStringLiteral("program_owner"), read.programOwner },
{ QStringLiteral("balance"), read.balanceHex },
{ QStringLiteral("nonce"), read.nonceHex },
{ QStringLiteral("data"), read.dataHex },
});
}
return result;
}
QJsonArray accountReadsJson(const QVector<WalletAccountRead>& reads)
{
QJsonArray result;
for (const WalletAccountRead& read : reads)
result.append(accountReadJson(read));
return result;
}
}
NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet,
AmmClient* client,
SequencerClient* sequencer)
: m_wallet(wallet),
m_client(client),
m_sequencer(sequencer)
{
}
void NewPositionRuntime::clearWalletAccounts()
{
++m_walletGeneration;
m_walletPublicAccountIds.clear();
clearPendingFreshLp();
m_wallet->clearSnapshot();
cancelSubmit();
}
void NewPositionRuntime::setWalletAccounts(const QVector<WalletAccount>& accounts)
{
QStringList publicAccountIds;
for (const WalletAccount& account : accounts) {
if (account.isPublic)
publicAccountIds.append(account.address);
}
publicAccountIds.sort(Qt::CaseSensitive);
if (publicAccountIds != m_walletPublicAccountIds) {
if (!m_pendingFreshLpAccountId.isEmpty()
&& !publicAccountIds.contains(m_pendingFreshLpAccountId)
&& m_freshLpState == FreshLpState::Ready) {
clearPendingFreshLp();
}
m_walletPublicAccountIds = std::move(publicAccountIds);
cancelSubmit();
return;
}
}
void NewPositionRuntime::rememberPendingFreshLp(const QString& accountId)
{
m_pendingFreshLpAccountId = accountId;
m_freshLpState = FreshLpState::Ready;
if (!m_walletPublicAccountIds.contains(accountId)) {
m_walletPublicAccountIds.append(accountId);
m_walletPublicAccountIds.sort(Qt::CaseSensitive);
}
}
void NewPositionRuntime::clearPendingFreshLp()
{
m_pendingFreshLpAccountId.clear();
m_freshLpState = FreshLpState::None;
}
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)
return {};
return accountReadsJson(m_wallet->snapshot(refresh).publicAccountReads);
}
QJsonObject NewPositionRuntime::buildQuoteInput(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool freshWalletAccounts,
QJsonObject* error) const
{
if (network.status != QStringLiteral("ready")) {
*error = publicError(network.status);
return {};
}
const AmmClientResult configResult = m_client->configId(
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
if (!configResult.ok) {
*error = publicError(QStringLiteral("backend_error"));
return {};
}
const QJsonObject configManifest = configResult.value;
const QJsonObject config = accountReadJson(m_wallet->readPublicAccount(
configManifest.value(QStringLiteral("configId")).toString()));
const QJsonObject requestObject = QJsonObject::fromVariantMap(request);
const AmmClientResult pairResult = m_client->pairIds(
QJsonObject {
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("config"), config },
{ QStringLiteral("tokenAId"), requestObject.value(QStringLiteral("tokenAId")) },
{ QStringLiteral("tokenBId"), requestObject.value(QStringLiteral("tokenBId")) },
});
if (!pairResult.ok) {
*error = publicError(QStringLiteral("backend_error"));
return {};
}
const QJsonObject pairManifest = pairResult.value;
if (pairManifest.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
*error = publicError(pairManifest.value(QStringLiteral("code")).toString());
return {};
}
const QJsonArray walletAccounts = walletAccountReads(walletOpen, freshWalletAccounts);
const QJsonObject snapshot {
{ QStringLiteral("config"), config },
{ QStringLiteral("tokenA"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("tokenAId")).toString())) },
{ QStringLiteral("tokenB"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("tokenBId")).toString())) },
{ QStringLiteral("pool"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("poolId")).toString())) },
{ QStringLiteral("vaultA"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("vaultAId")).toString())) },
{ QStringLiteral("vaultB"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("vaultBId")).toString())) },
{ QStringLiteral("lpDefinition"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("lpDefinitionId")).toString())) },
{ QStringLiteral("lpLockHolding"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("lpLockHoldingId")).toString())) },
{ QStringLiteral("currentTick"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("currentTickId")).toString())) },
{ QStringLiteral("clock"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("clockId")).toString())) },
{ QStringLiteral("walletAvailable"), walletOpen },
{ QStringLiteral("walletAccounts"), walletAccounts },
};
return {
{ QStringLiteral("networkId"), network.id },
{ QStringLiteral("networkFingerprint"), network.fingerprint },
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("request"), requestObject },
{ QStringLiteral("snapshot"), snapshot },
};
}
void NewPositionRuntime::contextAsync(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool refreshPublicData,
ResultCallback callback)
{
if (network.status != QStringLiteral("ready")) {
callback(contextState(network.status, network).toVariantMap());
return;
}
if (!m_sequencer || !m_sequencer->isConfigured()) {
callback(contextState(QStringLiteral("error"), network,
QStringLiteral("sequencer_config_required")).toVariantMap());
return;
}
const AmmClientResult configResult = m_client->configId(
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
if (!configResult.ok) {
callback(contextState(QStringLiteral("error"), network,
QStringLiteral("backend_error")).toVariantMap());
return;
}
const QString configId = configResult.value.value(
QStringLiteral("configId")).toString();
QPointer<NewPositionRuntime> guard(this);
m_sequencer->readAccounts({ configId }, refreshPublicData,
[guard, request, network, walletOpen, refreshPublicData,
callback = std::move(callback)](QVector<WalletAccountRead> configReads) mutable {
if (!guard)
return;
const QJsonObject config = accountReadJson(configReads.value(0));
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)
configured.append(id);
const QJsonArray recent = variantStringArray(
hints.value(QStringLiteral("recentTokenIds")).toVariant());
const QJsonArray resolved = variantStringArray(
hints.value(QStringLiteral("resolvedTokenIds")).toVariant());
const QJsonArray walletAccounts = accountReadsJson(walletReads);
const AmmClientResult tokenResult = guard->m_client->tokenIds(QJsonObject {
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("config"), config },
{ QStringLiteral("walletAccounts"), walletAccounts },
{ QStringLiteral("configuredTokenIds"), configured },
{ QStringLiteral("recentTokenIds"), recent },
{ QStringLiteral("resolvedTokenIds"), resolved },
});
if (!tokenResult.ok
|| tokenResult.value.value(QStringLiteral("status")).toString()
!= QStringLiteral("ok")) {
const QString code = tokenResult.ok
? tokenResult.value.value(QStringLiteral("code")).toString()
: QStringLiteral("backend_error");
callback(contextState(QStringLiteral("error"), network,
code.isEmpty() ? QStringLiteral("backend_error") : code)
.toVariantMap());
return;
}
QStringList definitionIds;
for (const QJsonValue& id : tokenResult.value
.value(QStringLiteral("tokenIds")).toArray()) {
definitionIds.append(id.toString());
}
guard->m_sequencer->readAccounts(definitionIds, refreshPublicData,
[guard, network, walletOpen, config, walletAccounts,
configured, recent, resolved,
callback = std::move(callback)](
QVector<WalletAccountRead> definitions) mutable {
if (!guard)
return;
const AmmClientResult result = guard->m_client->context(QJsonObject {
{ QStringLiteral("networkId"), network.id },
{ QStringLiteral("networkFingerprint"), network.fingerprint },
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("walletAvailable"), walletOpen },
{ QStringLiteral("config"), config },
{ QStringLiteral("walletAccounts"), walletAccounts },
{ QStringLiteral("tokenDefinitions"),
accountReadsJson(definitions) },
{ QStringLiteral("configuredTokenIds"), configured },
{ QStringLiteral("recentTokenIds"), recent },
{ QStringLiteral("resolvedTokenIds"), resolved },
});
callback((result.ok
? result.value
: contextState(QStringLiteral("error"), network,
QStringLiteral("backend_error"))).toVariantMap());
});
});
});
}
void NewPositionRuntime::buildQuoteInputAsync(
const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool forceRefresh,
std::function<void(QJsonObject, QJsonObject)> callback)
{
if (network.status != QStringLiteral("ready")) {
callback({}, publicError(network.status));
return;
}
if (!m_sequencer || !m_sequencer->isConfigured()) {
callback({}, publicError(QStringLiteral("sequencer_config_required")));
return;
}
const AmmClientResult configResult = m_client->configId(
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
if (!configResult.ok) {
callback({}, publicError(QStringLiteral("backend_error")));
return;
}
const QString configId = configResult.value.value(
QStringLiteral("configId")).toString();
QPointer<NewPositionRuntime> guard(this);
m_sequencer->readAccounts({ configId }, 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 = guard->m_client->pairIds(QJsonObject {
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("config"), config },
{ QStringLiteral("tokenAId"),
requestObject.value(QStringLiteral("tokenAId")) },
{ QStringLiteral("tokenBId"),
requestObject.value(QStringLiteral("tokenBId")) },
});
if (!pairResult.ok
|| pairResult.value.value(QStringLiteral("status")).toString()
!= QStringLiteral("ok")) {
const QString code = pairResult.ok
? pairResult.value.value(QStringLiteral("code")).toString()
: QStringLiteral("backend_error");
callback({}, publicError(code.isEmpty()
? QStringLiteral("backend_error") : code));
return;
}
const QJsonObject pair = pairResult.value;
const QStringList fixedIds {
pair.value(QStringLiteral("tokenAId")).toString(),
pair.value(QStringLiteral("tokenBId")).toString(),
pair.value(QStringLiteral("poolId")).toString(),
pair.value(QStringLiteral("vaultAId")).toString(),
pair.value(QStringLiteral("vaultBId")).toString(),
pair.value(QStringLiteral("lpDefinitionId")).toString(),
pair.value(QStringLiteral("lpLockHoldingId")).toString(),
pair.value(QStringLiteral("currentTickId")).toString(),
pair.value(QStringLiteral("clockId")).toString(),
};
guard->m_sequencer->readAccounts(fixedIds, forceRefresh,
[guard, requestObject, network, walletOpen, config, pair,
callback = std::move(callback)](
QVector<WalletAccountRead> fixedReads) mutable {
if (!guard)
return;
QStringList selectedIds;
for (const QString& key : {
QStringLiteral("holdingAId"),
QStringLiteral("holdingBId"),
QStringLiteral("lpHoldingId") }) {
const QString id = accountIdHex(
requestObject.value(key).toString());
if (!id.isEmpty() && !selectedIds.contains(id))
selectedIds.append(id);
}
QStringList walletIds = guard->m_walletPublicAccountIds;
for (const QString& id : selectedIds)
walletIds.removeAll(id);
guard->m_sequencer->readAccounts(
walletIds, false,
[guard, requestObject, network, walletOpen, config, pair,
selectedIds = std::move(selectedIds),
fixedReads = std::move(fixedReads),
callback = std::move(callback)](
QVector<WalletAccountRead> walletReads) mutable {
if (!guard)
return;
auto finish = [requestObject, network, walletOpen, config,
fixedReads = std::move(fixedReads),
walletReads = std::move(walletReads),
callback = std::move(callback)](
QVector<WalletAccountRead> selectedReads) mutable {
QHash<QString, WalletAccountRead> walletById;
for (const WalletAccountRead& read : walletReads)
walletById.insert(read.accountId, read);
for (const WalletAccountRead& read : selectedReads)
walletById.insert(read.accountId, read);
const QJsonArray walletAccounts = accountReadsJson(
walletById.values());
const QJsonObject snapshot {
{ QStringLiteral("config"), config },
{ QStringLiteral("tokenA"), accountReadJson(fixedReads.value(0)) },
{ QStringLiteral("tokenB"), accountReadJson(fixedReads.value(1)) },
{ QStringLiteral("pool"), accountReadJson(fixedReads.value(2)) },
{ QStringLiteral("vaultA"), accountReadJson(fixedReads.value(3)) },
{ QStringLiteral("vaultB"), accountReadJson(fixedReads.value(4)) },
{ QStringLiteral("lpDefinition"), accountReadJson(fixedReads.value(5)) },
{ QStringLiteral("lpLockHolding"), accountReadJson(fixedReads.value(6)) },
{ QStringLiteral("currentTick"), accountReadJson(fixedReads.value(7)) },
{ QStringLiteral("clock"), accountReadJson(fixedReads.value(8)) },
{ QStringLiteral("walletAvailable"), walletOpen },
{ QStringLiteral("walletAccounts"), walletAccounts },
};
callback(QJsonObject {
{ QStringLiteral("networkId"), network.id },
{ QStringLiteral("networkFingerprint"), network.fingerprint },
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("request"), requestObject },
{ QStringLiteral("snapshot"), snapshot },
}, {});
};
if (selectedIds.isEmpty())
finish({});
else
guard->m_sequencer->readAccounts(
selectedIds, true, std::move(finish));
});
});
});
}
void NewPositionRuntime::quoteAsync(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool forceRefresh,
ResultCallback callback)
{
QPointer<NewPositionRuntime> guard(this);
buildQuoteInputAsync(request, network, walletOpen, forceRefresh,
[guard, callback = std::move(callback)](
QJsonObject input, QJsonObject error) mutable {
if (!guard)
return;
if (!error.isEmpty()) {
callback(error.toVariantMap());
return;
}
const AmmClientResult result = guard->m_client->quote(input);
callback((result.ok ? result.value
: publicError(QStringLiteral("backend_error")))
.toVariantMap());
});
}
void NewPositionRuntime::submitAsync(const QVariantMap& request,
const QString& quoteHash,
const ActiveNetworkSnapshot& network,
bool walletCanSubmit,
ResultCallback callback)
{
if (m_submitInFlight) {
callback(publicError(QStringLiteral("submit_in_progress")).toVariantMap());
return;
}
if (!walletCanSubmit) {
callback(publicError(QStringLiteral("wallet_syncing")).toVariantMap());
return;
}
if (m_freshLpState == FreshLpState::Creating
|| m_freshLpState == FreshLpState::Submitting) {
callback(publicError(QStringLiteral("submit_in_progress")).toVariantMap());
return;
}
m_submitInFlight = true;
const quint64 submitGeneration = ++m_submitGeneration;
const quint64 walletGeneration = m_walletGeneration;
m_submitCallback = std::move(callback);
QPointer<NewPositionRuntime> guard(this);
buildQuoteInputAsync(request, network, true, true,
[guard, quoteHash, submitGeneration, walletGeneration](
QJsonObject input, QJsonObject error) mutable {
if (!guard
|| !guard->submitIsCurrent(submitGeneration, walletGeneration))
return;
if (!error.isEmpty()) {
guard->finishSubmit(submitGeneration, error.toVariantMap());
return;
}
const AmmClientResult quoteResult = guard->m_client->quote(input);
if (!quoteResult.ok) {
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);
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);
guard->finishSubmit(submitGeneration, result.toVariantMap());
return;
}
if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
guard->prepareFreshLpAsync(
std::move(input), quoteHash,
submitGeneration, walletGeneration);
return;
}
guard->submitPlanAsync(
std::move(input), quoteHash, {}, {},
submitGeneration, walletGeneration);
});
}
void NewPositionRuntime::prepareFreshLpAsync(QJsonObject input,
const QString& quoteHash,
quint64 submitGeneration,
quint64 walletGeneration)
{
if (!submitIsCurrent(submitGeneration, walletGeneration))
return;
if (m_freshLpState == FreshLpState::Ready
&& !m_pendingFreshLpAccountId.isEmpty()) {
validatePendingFreshLpAsync(
std::move(input), quoteHash,
submitGeneration, walletGeneration);
return;
}
if (m_freshLpState != FreshLpState::None) {
finishSubmit(
submitGeneration,
publicError(QStringLiteral("submit_in_progress")).toVariantMap());
return;
}
m_freshLpState = FreshLpState::Creating;
QPointer<NewPositionRuntime> guard(this);
m_wallet->createAccountAsync(
true,
[guard, input = std::move(input), quoteHash,
submitGeneration, walletGeneration](
WalletAccountCreation creation) mutable {
if (!guard || walletGeneration != guard->m_walletGeneration)
return;
const bool reusable = creation.ok()
&& isLowerHex(creation.accountId, ACCOUNT_ID_HEX_SIZE);
if (reusable)
guard->rememberPendingFreshLp(creation.accountId);
else
guard->clearPendingFreshLp();
if (!guard->submitIsCurrent(submitGeneration, walletGeneration))
return;
if (!reusable) {
const QString code = creation.failure
== WalletFailure::WalletUnavailable
? QStringLiteral("wallet_unavailable")
: QStringLiteral("wallet_submission_failed");
guard->finishSubmit(
submitGeneration, publicError(code).toVariantMap());
return;
}
WalletAccountRead read = std::move(creation.publicAccount);
read.accountId = creation.accountId;
if (isDefaultAccountRead(read, creation.accountId)) {
guard->submitPlanAsync(
std::move(input), quoteHash, accountReadJson(read),
creation.accountId, submitGeneration, walletGeneration);
return;
}
guard->validatePendingFreshLpAsync(
std::move(input), quoteHash,
submitGeneration, walletGeneration);
});
}
void NewPositionRuntime::validatePendingFreshLpAsync(
QJsonObject input,
const QString& quoteHash,
quint64 submitGeneration,
quint64 walletGeneration)
{
if (!submitIsCurrent(submitGeneration, walletGeneration))
return;
const QString accountId = m_pendingFreshLpAccountId;
if (m_freshLpState != FreshLpState::Ready
|| accountId.isEmpty()
|| !m_sequencer
|| !m_sequencer->isConfigured()) {
finishSubmit(
submitGeneration,
publicError(QStringLiteral("account_read_failed")).toVariantMap());
return;
}
QPointer<NewPositionRuntime> guard(this);
m_sequencer->readAccounts(
{ accountId }, true,
[guard, input = std::move(input), quoteHash, accountId,
submitGeneration, walletGeneration](
QVector<WalletAccountRead> reads) mutable {
if (!guard
|| !guard->submitIsCurrent(
submitGeneration, walletGeneration)) {
return;
}
if (guard->m_pendingFreshLpAccountId != accountId) {
guard->finishSubmit(
submitGeneration,
publicError(QStringLiteral("wallet_unavailable")).toVariantMap());
return;
}
const WalletAccountRead read = reads.value(0);
if (!read.ok()) {
guard->finishSubmit(
submitGeneration,
publicError(QStringLiteral("account_read_failed")).toVariantMap());
return;
}
if (!isDefaultAccountRead(read, accountId)) {
guard->clearPendingFreshLp();
guard->finishSubmit(
submitGeneration,
publicError(QStringLiteral("submission_status_unknown")).toVariantMap());
return;
}
guard->submitPlanAsync(
std::move(input), quoteHash, accountReadJson(read), accountId,
submitGeneration, walletGeneration);
});
}
void NewPositionRuntime::submitPlanAsync(QJsonObject input,
const QString& quoteHash,
QJsonValue freshLp,
QString freshLpAccountId,
quint64 submitGeneration,
quint64 walletGeneration)
{
if (!submitIsCurrent(submitGeneration, walletGeneration))
return;
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()),
};
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
m_freshLpState = FreshLpState::Submitting;
}
QPointer<NewPositionRuntime> guard(this);
m_wallet->submitPublicTransactionAsync(
transaction,
[guard, submitGeneration, walletGeneration,
freshLpAccountId = std::move(freshLpAccountId), deadlineMs =
plan.value(QStringLiteral("deadlineMs")), affectedAccountIds =
plan.value(QStringLiteral("affectedAccountIds"))](
WalletSubmission submission) mutable {
if (!guard)
return;
const QString transactionId = submission.accepted()
? base58TransactionId(submission.nativeHash) : QString();
if (walletGeneration == guard->m_walletGeneration
&& !freshLpAccountId.isEmpty()
&& freshLpAccountId == guard->m_pendingFreshLpAccountId) {
if (submission.accepted())
guard->clearPendingFreshLp();
else
guard->m_freshLpState = FreshLpState::Ready;
}
if (!guard->submitIsCurrent(submitGeneration, walletGeneration))
return;
if (transactionId.isEmpty()) {
const QString code = submission.failure
== WalletFailure::WalletUnavailable
? QStringLiteral("wallet_unavailable")
: QStringLiteral("wallet_submission_failed");
guard->finishSubmit(
submitGeneration, publicError(code).toVariantMap());
return;
}
guard->finishSubmit(submitGeneration, QJsonObject {
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
{ QStringLiteral("status"), QStringLiteral("submitted") },
{ QStringLiteral("transactionId"), transactionId },
{ QStringLiteral("nativeTransactionHash"), submission.nativeHash },
{ QStringLiteral("deadlineMs"), deadlineMs },
{ QStringLiteral("affectedAccountIds"),
affectedAccountIds },
}.toVariantMap());
});
}
QVariantMap NewPositionRuntime::context(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool refreshWalletAccounts)
{
if (network.status != QStringLiteral("ready"))
return contextState(network.status, network).toVariantMap();
const QJsonArray walletAccounts = walletAccountReads(walletOpen, refreshWalletAccounts);
const QJsonObject hints = QJsonObject::fromVariantMap(request);
const AmmClientResult configResult = m_client->configId(
QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } });
if (!configResult.ok)
return contextState(
QStringLiteral("error"), network, QStringLiteral("backend_error")).toVariantMap();
const QJsonObject configManifest = configResult.value;
const QJsonObject config = accountReadJson(m_wallet->readPublicAccount(
configManifest.value(QStringLiteral("configId")).toString()));
QJsonArray configured;
for (const QString& id : network.tokenIds)
configured.append(id);
const QJsonArray recent = variantStringArray(hints.value(QStringLiteral("recentTokenIds")).toVariant());
const QJsonArray resolved = variantStringArray(hints.value(QStringLiteral("resolvedTokenIds")).toVariant());
const AmmClientResult tokenResult = m_client->tokenIds(
QJsonObject {
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("config"), config },
{ QStringLiteral("walletAccounts"), walletAccounts },
{ QStringLiteral("configuredTokenIds"), configured },
{ QStringLiteral("recentTokenIds"), recent },
{ QStringLiteral("resolvedTokenIds"), resolved },
});
const QJsonObject tokenManifest = tokenResult.value;
if (!tokenResult.ok
|| tokenManifest.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) {
const QString code = tokenResult.ok
? tokenManifest.value(QStringLiteral("code")).toString()
: QStringLiteral("backend_error");
return contextState(
QStringLiteral("error"),
network,
code.isEmpty() ? QStringLiteral("backend_error") : code).toVariantMap();
}
QJsonArray definitions;
for (const QJsonValue& id : tokenManifest.value(QStringLiteral("tokenIds")).toArray())
definitions.append(accountReadJson(m_wallet->readPublicAccount(id.toString())));
const AmmClientResult contextResult = m_client->context(
QJsonObject {
{ QStringLiteral("networkId"), network.id },
{ QStringLiteral("networkFingerprint"), network.fingerprint },
{ QStringLiteral("ammProgramId"), network.ammProgramId },
{ QStringLiteral("walletAvailable"), walletOpen },
{ QStringLiteral("config"), config },
{ QStringLiteral("walletAccounts"), walletAccounts },
{ QStringLiteral("tokenDefinitions"), definitions },
{ QStringLiteral("configuredTokenIds"), configured },
{ QStringLiteral("recentTokenIds"), recent },
{ QStringLiteral("resolvedTokenIds"), resolved },
});
return (contextResult.ok
? contextResult.value
: contextState(
QStringLiteral("error"), network, QStringLiteral("backend_error"))).toVariantMap();
}
QVariantMap NewPositionRuntime::quote(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen)
{
QJsonObject error;
const QJsonObject input = buildQuoteInput(request, network, walletOpen, false, &error);
if (!error.isEmpty())
return error.toVariantMap();
const AmmClientResult result = m_client->quote(input);
return (result.ok ? result.value : publicError(QStringLiteral("backend_error"))).toVariantMap();
}
QVariantMap NewPositionRuntime::submit(const QVariantMap& request,
const QString& quoteHash,
const ActiveNetworkSnapshot& network,
bool walletOpen)
{
if (m_submitInFlight)
return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
if (!walletOpen)
return publicError(QStringLiteral("wallet_unavailable")).toVariantMap();
if (m_freshLpState == FreshLpState::Creating
|| m_freshLpState == FreshLpState::Submitting) {
return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
}
QScopedValueRollback<bool> submitGuard(m_submitInFlight, true);
QJsonObject error;
const QJsonObject input = buildQuoteInput(request, network, walletOpen, true, &error);
if (!error.isEmpty())
return error.toVariantMap();
const AmmClientResult quoteResult = m_client->quote(input);
if (!quoteResult.ok)
return publicError(QStringLiteral("backend_error")).toVariantMap();
const QJsonObject quote = quoteResult.value;
if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) {
QJsonObject result = publicError(QStringLiteral("quote_changed"));
result.insert(QStringLiteral("quote"), quote);
return result.toVariantMap();
}
if (!quote.value(QStringLiteral("canSubmit")).toBool(false)) {
QJsonObject result = publicError(QStringLiteral("quote_not_submittable"));
result.insert(QStringLiteral("quote"), quote);
return result.toVariantMap();
}
QJsonValue freshLp;
QString freshLpAccountId;
if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
WalletAccountRead read;
if (m_freshLpState == FreshLpState::None) {
m_freshLpState = FreshLpState::Creating;
const WalletAccountCreation creation = m_wallet->createAccount(true);
if (!creation.ok()
|| !isLowerHex(creation.accountId, ACCOUNT_ID_HEX_SIZE)) {
clearPendingFreshLp();
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
}
rememberPendingFreshLp(creation.accountId);
read = creation.publicAccount;
read.accountId = creation.accountId;
} else if (m_freshLpState == FreshLpState::Ready
&& !m_pendingFreshLpAccountId.isEmpty()) {
read = m_wallet->readPublicAccount(m_pendingFreshLpAccountId);
} else {
return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
}
freshLpAccountId = m_pendingFreshLpAccountId;
if (!read.ok())
return publicError(QStringLiteral("account_read_failed")).toVariantMap();
if (!isDefaultAccountRead(read, freshLpAccountId)) {
clearPendingFreshLp();
return publicError(QStringLiteral("submission_status_unknown")).toVariantMap();
}
freshLp = accountReadJson(read);
}
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)
return publicError(QStringLiteral("backend_error")).toVariantMap();
const QJsonObject plan = planResult.value;
if (plan.value(QStringLiteral("status")).toString() != QStringLiteral("ready")) {
const QString code = plan.value(QStringLiteral("code")).toString();
return publicError(code.isEmpty() ? QStringLiteral("wallet_submission_failed") : code)
.toVariantMap();
}
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());
const QString programId = plan.value(QStringLiteral("programId")).toString();
bool deadlineValid = false;
const qulonglong deadline = plan.value(QStringLiteral("deadlineMs")).toString().toULongLong(&deadlineValid);
if (!deadlineValid
|| static_cast<qulonglong>(QDateTime::currentMSecsSinceEpoch()) >= deadline) {
return publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap();
}
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
m_freshLpState = FreshLpState::Submitting;
}
const WalletSubmission submission = m_wallet->submitPublicTransaction({
programId,
accountIds,
signingRequirements,
instruction,
});
if (!submission.accepted()) {
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
m_freshLpState = FreshLpState::Ready;
}
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
}
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
clearPendingFreshLp();
}
const QString transactionId = base58TransactionId(submission.nativeHash);
if (transactionId.isEmpty())
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
return QJsonObject {
{ QStringLiteral("schema"), QString::fromLatin1(SCHEMA) },
{ QStringLiteral("status"), QStringLiteral("submitted") },
{ QStringLiteral("transactionId"), transactionId },
{ QStringLiteral("nativeTransactionHash"), submission.nativeHash },
{ QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) },
}.toVariantMap();
}