mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
chore(wallet): synchronize shared wallet module
This commit is contained in:
@@ -61,13 +61,13 @@ if(LOGOS_WALLET_BUILD_QML)
|
||||
set(wallet_qml_output_dir "${CMAKE_CURRENT_BINARY_DIR}/qml/Logos/Wallet")
|
||||
set(wallet_internal_qml
|
||||
qml/internal/WalletIconButton.qml
|
||||
qml/internal/CopyButton.qml
|
||||
qml/internal/AccountDelegate.qml
|
||||
qml/internal/CreateAccountDialog.qml
|
||||
qml/internal/CreateWalletDialog.qml
|
||||
qml/internal/WalletMessageDialog.qml
|
||||
)
|
||||
set(wallet_public_qml
|
||||
qml/internal/CopyButton.qml
|
||||
qml/WalletControl.qml
|
||||
qml/TransactionConfirmationDialog.qml
|
||||
qml/SubmittedTransaction.qml
|
||||
@@ -172,8 +172,16 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(logos_wallet_qml_test PRIVATE Qt6::QuickTest)
|
||||
add_dependencies(logos_wallet_qml_test logos_wallet_qmlplugin)
|
||||
add_test(NAME logos_wallet_qml COMMAND logos_wallet_qml_test)
|
||||
get_target_property(wallet_qml_library Qt6::Qml IMPORTED_LOCATION)
|
||||
get_filename_component(wallet_qml_library_dir "${wallet_qml_library}" DIRECTORY)
|
||||
get_filename_component(wallet_qml_prefix "${wallet_qml_library_dir}" DIRECTORY)
|
||||
set(wallet_qml_test_import_paths
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/qml"
|
||||
"${wallet_qml_prefix}/${QT6_INSTALL_QML}"
|
||||
)
|
||||
string(JOIN ":" wallet_qml_test_import_path ${wallet_qml_test_import_paths})
|
||||
set_tests_properties(logos_wallet_qml PROPERTIES ENVIRONMENT
|
||||
"QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software;QML2_IMPORT_PATH=${CMAKE_CURRENT_BINARY_DIR}/qml;QML_IMPORT_PATH=${CMAKE_CURRENT_BINARY_DIR}/qml"
|
||||
"QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software;QML2_IMPORT_PATH=${wallet_qml_test_import_path};QML_IMPORT_PATH=${wallet_qml_test_import_path}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -9,12 +9,21 @@ Popup {
|
||||
property string cancelText: qsTr("Cancel")
|
||||
property string confirmText: qsTr("Confirm")
|
||||
property bool busy: false
|
||||
property string busyText: qsTr("Submitting…")
|
||||
property bool activityBusy: false
|
||||
property string activityText: qsTr("Updating…")
|
||||
property bool showInlineBusyIndicator: true
|
||||
property var snapshot: ({})
|
||||
property Component summary: null
|
||||
property bool confirmationPending: false
|
||||
property bool confirmEnabled: true
|
||||
property bool roundedCancelButton: false
|
||||
property bool closeWhenSettled: true
|
||||
readonly property bool actionPending: root.busy || root.activityBusy
|
||||
|
||||
signal canceled
|
||||
signal confirmed(var snapshot)
|
||||
signal summaryEdited(var snapshot)
|
||||
|
||||
modal: true
|
||||
dim: true
|
||||
@@ -40,7 +49,14 @@ Popup {
|
||||
root.snapshot = root.cloneSnapshot(nextSnapshot)
|
||||
root.confirmationPending = false
|
||||
root.open()
|
||||
cancelButton.forceActiveFocus()
|
||||
Qt.callLater(function() {
|
||||
if (cancelButtonLoader.item)
|
||||
cancelButtonLoader.item.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
|
||||
function updateSnapshot(nextSnapshot) {
|
||||
root.snapshot = root.cloneSnapshot(nextSnapshot)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
@@ -52,7 +68,7 @@ Popup {
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (root.busy)
|
||||
if (root.actionPending || !root.confirmEnabled)
|
||||
return
|
||||
root.confirmationPending = true
|
||||
root.confirmed(root.snapshot)
|
||||
@@ -62,10 +78,21 @@ Popup {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: summaryLoader.item
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onSnapshotEdited(snapshot) {
|
||||
root.updateSnapshot(snapshot)
|
||||
root.summaryEdited(root.snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
onBusyChanged: {
|
||||
if (!root.busy && root.confirmationPending) {
|
||||
root.confirmationPending = false
|
||||
root.close()
|
||||
if (root.closeWhenSettled)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,26 +144,37 @@ Popup {
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
Item {
|
||||
id: inlineBusyIndicator
|
||||
|
||||
property bool active: root.showInlineBusyIndicator && root.actionPending
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: root.busy
|
||||
running: root.busy
|
||||
Accessible.name: qsTr("Submitting transaction")
|
||||
Layout.preferredWidth: active ? busySpinner.implicitWidth : 0
|
||||
Layout.preferredHeight: active ? busySpinner.implicitHeight : 0
|
||||
implicitWidth: busySpinner.implicitWidth
|
||||
implicitHeight: busySpinner.implicitHeight
|
||||
visible: active
|
||||
|
||||
BusyIndicator {
|
||||
id: busySpinner
|
||||
|
||||
anchors.centerIn: parent
|
||||
running: inlineBusyIndicator.active
|
||||
Accessible.name: root.busy ? root.busyText : root.activityText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Button {
|
||||
id: cancelButton
|
||||
objectName: "transactionCancelButton"
|
||||
Loader {
|
||||
id: cancelButtonLoader
|
||||
objectName: "transactionCancelButtonLoader"
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 44
|
||||
text: root.cancelText
|
||||
enabled: !root.busy
|
||||
Accessible.name: text
|
||||
onClicked: root.cancel()
|
||||
Layout.preferredHeight: 44
|
||||
sourceComponent: root.roundedCancelButton
|
||||
? roundedCancelButtonComponent : defaultCancelButtonComponent
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -144,8 +182,8 @@ Popup {
|
||||
objectName: "transactionConfirmButton"
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 44
|
||||
text: root.busy ? qsTr("Submitting...") : root.confirmText
|
||||
enabled: !root.busy
|
||||
text: root.busy ? root.busyText : root.confirmText
|
||||
enabled: !root.actionPending && root.confirmEnabled
|
||||
Accessible.name: text
|
||||
onClicked: root.confirm()
|
||||
|
||||
@@ -166,4 +204,48 @@ Popup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: defaultCancelButtonComponent
|
||||
|
||||
Button {
|
||||
objectName: "transactionCancelButton"
|
||||
anchors.fill: parent
|
||||
text: root.cancelText
|
||||
enabled: !root.busy
|
||||
Accessible.name: text
|
||||
onClicked: root.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roundedCancelButtonComponent
|
||||
|
||||
Button {
|
||||
id: cancelButton
|
||||
|
||||
objectName: "transactionCancelButton"
|
||||
anchors.fill: parent
|
||||
text: root.cancelText
|
||||
enabled: !root.busy
|
||||
Accessible.name: text
|
||||
onClicked: root.cancel()
|
||||
|
||||
background: Rectangle {
|
||||
color: cancelButton.pressed ? "#3f3f46"
|
||||
: cancelButton.hovered ? "#27272a" : "#18181b"
|
||||
border.color: "#52525b"
|
||||
border.width: 1
|
||||
radius: 6
|
||||
}
|
||||
|
||||
contentItem: Label {
|
||||
text: cancelButton.text
|
||||
color: "#f4f4f5"
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,15 @@ Item {
|
||||
property bool openPending: false
|
||||
property bool advancedExpanded: false
|
||||
property bool availableExpanded: false
|
||||
property bool primarySelectionQueued: false
|
||||
property string postCreationWarning: ""
|
||||
property bool createdWalletAwaitingAcknowledgement: false
|
||||
property string reportedOpenErrorKey: ""
|
||||
|
||||
readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen
|
||||
readonly property string syncStatus: root.wallet
|
||||
? String(root.wallet.walletSyncStatus || "closed")
|
||||
: "closed"
|
||||
readonly property bool compactLayout: root.compact || root.viewportWidth < 680
|
||||
readonly property bool walletOpening: root.wallet !== null
|
||||
&& (root.wallet.walletSyncStatus === "opening"
|
||||
@@ -52,7 +56,11 @@ Item {
|
||||
required property bool canBePrimary
|
||||
required property string kind
|
||||
}
|
||||
onCountChanged: root.syncPrimarySelection()
|
||||
onCountChanged: {
|
||||
root.syncPrimarySelection()
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
onObjectAdded: root.schedulePrimarySelection()
|
||||
}
|
||||
|
||||
function accountAt(index, field) {
|
||||
@@ -78,6 +86,8 @@ Item {
|
||||
? root.wallet.primaryAccountAddress : ""
|
||||
for (let index = 0; index < accounts.count; ++index) {
|
||||
const account = accounts.objectAt(index)
|
||||
if (!account)
|
||||
continue
|
||||
if ((requested.length > 0 && account.address === requested) || account.isPrimary) {
|
||||
root.selectedIndex = index
|
||||
return
|
||||
@@ -85,6 +95,8 @@ Item {
|
||||
}
|
||||
for (let index = 0; index < accounts.count; ++index) {
|
||||
const account = accounts.objectAt(index)
|
||||
if (!account)
|
||||
continue
|
||||
if (account.kind === "user" && account.canBePrimary) {
|
||||
root.selectedIndex = index
|
||||
return
|
||||
@@ -93,6 +105,19 @@ Item {
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
|
||||
function schedulePrimarySelection() {
|
||||
if (root.primarySelectionQueued)
|
||||
return
|
||||
root.primarySelectionQueued = true
|
||||
Qt.callLater(function() {
|
||||
root.primarySelectionQueued = false
|
||||
if (root.connected)
|
||||
root.syncPrimarySelection()
|
||||
else
|
||||
root.selectedIndex = -1
|
||||
})
|
||||
}
|
||||
|
||||
function shortAddress(address) {
|
||||
return address && address.length > 13
|
||||
? address.substring(0, 6) + "…" + address.substring(address.length - 4)
|
||||
@@ -140,6 +165,7 @@ Item {
|
||||
if (key === root.reportedOpenErrorKey)
|
||||
return
|
||||
root.reportedOpenErrorKey = key
|
||||
root.busy = false
|
||||
root.showError(root.openFailureMessage())
|
||||
}
|
||||
|
||||
@@ -234,16 +260,32 @@ Item {
|
||||
Connections {
|
||||
target: root.accountModel
|
||||
ignoreUnknownSignals: true
|
||||
function onModelReset() { root.syncPrimarySelection() }
|
||||
function onRowsInserted() { root.syncPrimarySelection() }
|
||||
function onRowsRemoved() { root.syncPrimarySelection() }
|
||||
function onDataChanged() { root.syncPrimarySelection() }
|
||||
function onModelReset() {
|
||||
root.syncPrimarySelection()
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
function onRowsInserted() {
|
||||
root.syncPrimarySelection()
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
function onRowsRemoved() {
|
||||
root.syncPrimarySelection()
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
function onDataChanged() {
|
||||
root.syncPrimarySelection()
|
||||
if (root.selectedIndex < 0)
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wallet
|
||||
ignoreUnknownSignals: true
|
||||
function onPrimaryAccountAddressChanged() { root.syncPrimarySelection() }
|
||||
function onPrimaryAccountAddressChanged() {
|
||||
root.syncPrimarySelection()
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
function onWalletSyncStatusChanged() {
|
||||
if (!root.wallet || root.wallet.walletSyncStatus !== "error")
|
||||
root.reportedOpenErrorKey = ""
|
||||
@@ -266,6 +308,7 @@ Item {
|
||||
walletMenu.close()
|
||||
} else {
|
||||
root.syncPrimarySelection()
|
||||
root.schedulePrimarySelection()
|
||||
}
|
||||
}
|
||||
onViewportWidthChanged: {
|
||||
@@ -374,6 +417,7 @@ Item {
|
||||
Popup {
|
||||
id: walletMenu
|
||||
objectName: "walletMenu"
|
||||
palette.windowText: "#d4d4d8"
|
||||
property real lastClosedMs: 0
|
||||
property point anchorPosition: Qt.point(0, 0)
|
||||
readonly property var viewport: Overlay.overlay
|
||||
@@ -395,6 +439,7 @@ Item {
|
||||
height: Math.min(implicitHeight, availableMenuHeight)
|
||||
margins: 12
|
||||
padding: 12
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
function updateAnchor() {
|
||||
@@ -506,6 +551,8 @@ Item {
|
||||
}
|
||||
}
|
||||
Label {
|
||||
objectName: "walletPrimaryAccountType"
|
||||
visible: root.selectedIndex >= 0
|
||||
text: root.selectedIsPublic ? qsTr("Public user account")
|
||||
: qsTr("Private account")
|
||||
color: "#a1a1aa"
|
||||
|
||||
@@ -119,6 +119,7 @@ ItemDelegate {
|
||||
spacing: 6
|
||||
|
||||
Button {
|
||||
objectName: "walletRenameButton"
|
||||
text: qsTr("Rename")
|
||||
flat: true
|
||||
onClicked: root.renameRequested(root.address, root.alias)
|
||||
@@ -127,6 +128,7 @@ ItemDelegate {
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
objectName: "walletMakePrimaryButton"
|
||||
visible: root.canBePrimary && !root.isPrimary
|
||||
text: qsTr("Make primary")
|
||||
flat: true
|
||||
|
||||
@@ -5,9 +5,11 @@ WalletIconButton {
|
||||
|
||||
signal copyRequested
|
||||
|
||||
property string copyText: ""
|
||||
property string copyLabel: qsTr("Copy")
|
||||
property bool copied: false
|
||||
|
||||
accessibleName: root.copied ? qsTr("Copied") : qsTr("Copy")
|
||||
accessibleName: root.copied ? qsTr("Copied") : root.copyLabel
|
||||
iconSource: root.copied
|
||||
? Qt.resolvedUrl("icons/checkmark.svg")
|
||||
: Qt.resolvedUrl("icons/copy.svg")
|
||||
@@ -18,7 +20,24 @@ WalletIconButton {
|
||||
onTriggered: root.copied = false
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: clipboardProxy
|
||||
|
||||
visible: false
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
if (root.copyText.length === 0)
|
||||
return
|
||||
clipboardProxy.text = root.copyText
|
||||
clipboardProxy.selectAll()
|
||||
clipboardProxy.copy()
|
||||
clipboardProxy.deselect()
|
||||
clipboardProxy.text = ""
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.copyToClipboard()
|
||||
root.copyRequested()
|
||||
root.copied = true
|
||||
resetTimer.restart()
|
||||
|
||||
@@ -16,9 +16,13 @@ Popup {
|
||||
x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
|
||||
y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
|
||||
padding: 20
|
||||
focus: true
|
||||
closePolicy: root.busy ? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: privateSwitch.checked = false
|
||||
onOpened: {
|
||||
privateSwitch.checked = false
|
||||
Qt.callLater(function() { privateSwitch.forceActiveFocus() })
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "#18181b"
|
||||
|
||||
@@ -20,6 +20,7 @@ Popup {
|
||||
x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
|
||||
y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
|
||||
padding: 20
|
||||
focus: true
|
||||
closePolicy: root.busy || root.mnemonic.length > 0
|
||||
? Popup.NoAutoClose
|
||||
: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
@@ -15,8 +15,11 @@ Popup {
|
||||
x: parent ? Math.max(0, Math.round((parent.width - width) / 2)) : 0
|
||||
y: parent ? Math.max(0, Math.round((parent.height - height) / 2)) : 0
|
||||
padding: 20
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: Qt.callLater(function() { closeButton.forceActiveFocus() })
|
||||
|
||||
background: Rectangle {
|
||||
color: "#18181b"
|
||||
border.color: "#3f3f46"
|
||||
@@ -44,6 +47,8 @@ Popup {
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeButton
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("Close")
|
||||
onClicked: root.close()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "LogosWalletProvider.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
@@ -114,6 +117,58 @@ void applyPublicRead(WalletAccount& account, const WalletAccountRead& read)
|
||||
account.programOwner = read.programOwner;
|
||||
account.dataHex = read.dataHex;
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -143,12 +198,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);
|
||||
@@ -173,6 +232,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) {
|
||||
@@ -236,6 +296,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);
|
||||
@@ -260,8 +322,6 @@ WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths,
|
||||
return creation;
|
||||
}
|
||||
|
||||
creation.snapshot = snapshot(true);
|
||||
creation.failure = creation.snapshot.failure;
|
||||
return creation;
|
||||
}
|
||||
|
||||
@@ -329,19 +389,203 @@ WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic)
|
||||
return creation;
|
||||
}
|
||||
|
||||
creation.snapshot = snapshot(true);
|
||||
if (isPublic) {
|
||||
for (const WalletAccountRead& read : creation.snapshot.publicAccountReads) {
|
||||
if (read.accountId == creation.accountId) {
|
||||
creation.publicAccount = read;
|
||||
return creation;
|
||||
}
|
||||
}
|
||||
if (isPublic)
|
||||
creation.publicAccount = readPublicAccount(creation.accountId);
|
||||
if (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(
|
||||
m_snapshot.publicAccountReads.begin(),
|
||||
m_snapshot.publicAccountReads.end(),
|
||||
[&creation](const WalletAccountRead& existing) {
|
||||
return existing.accountId == creation.accountId;
|
||||
});
|
||||
if (read == m_snapshot.publicAccountReads.end())
|
||||
m_snapshot.publicAccountReads.append(creation.publicAccount);
|
||||
else
|
||||
*read = creation.publicAccount;
|
||||
} else {
|
||||
account.balance = m_impl->logos->logos_execution_zone.get_balance(
|
||||
creation.accountId, isPublic);
|
||||
}
|
||||
auto existing = std::find_if(
|
||||
m_snapshot.accounts.begin(), m_snapshot.accounts.end(),
|
||||
[&creation](const WalletAccount& candidate) {
|
||||
return candidate.address == creation.accountId;
|
||||
});
|
||||
if (existing == m_snapshot.accounts.end())
|
||||
m_snapshot.accounts.append(account);
|
||||
else
|
||||
*existing = account;
|
||||
creation.snapshot = m_snapshot;
|
||||
}
|
||||
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))
|
||||
@@ -399,27 +643,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(
|
||||
@@ -427,36 +656,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();
|
||||
@@ -589,16 +842,12 @@ void LogosWalletProvider::loadSnapshotAsync(quint64 generation, SnapshotCallback
|
||||
{},
|
||||
entry.value(QStringLiteral("is_public"), true).toBool(),
|
||||
};
|
||||
if (!state->snapshot.accounts.at(index).isPublic) {
|
||||
state->snapshot.accounts[index].readStatus =
|
||||
QStringLiteral("private");
|
||||
}
|
||||
state->publicFlags[index] =
|
||||
state->snapshot.accounts.at(index).isPublic;
|
||||
}
|
||||
|
||||
auto finishOne = std::make_shared<std::function<void()>>();
|
||||
*finishOne = [this, generation, state, finishOne]() mutable {
|
||||
*finishOne = [this, generation, state]() mutable {
|
||||
if (generation != m_generation || --state->remaining > 0)
|
||||
return;
|
||||
for (qsizetype index = 0;
|
||||
|
||||
@@ -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,11 +23,14 @@ 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;
|
||||
void readPublicAccountsAsync(const QStringList& accountIds,
|
||||
AccountReadsCallback callback) override;
|
||||
WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) override;
|
||||
void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) override;
|
||||
void disconnect() override;
|
||||
|
||||
private:
|
||||
@@ -40,4 +45,5 @@ private:
|
||||
bool m_snapshotReady = false;
|
||||
bool m_connected = false;
|
||||
quint64 m_generation = 0;
|
||||
quint64 m_sessionGeneration = 0;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
namespace {
|
||||
constexpr char BASE58_ALPHABET[] =
|
||||
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
constexpr qsizetype ACCOUNT_ID_BYTES = 32;
|
||||
constexpr qsizetype MIN_BASE58_ACCOUNT_ID_SIZE = 32;
|
||||
constexpr qsizetype MAX_BASE58_ACCOUNT_ID_SIZE = 44;
|
||||
|
||||
bool isHexCharacter(QChar character)
|
||||
{
|
||||
@@ -14,6 +17,18 @@ bool isHexCharacter(QChar character)
|
||||
|| (value >= 'a' && value <= 'f')
|
||||
|| (value >= 'A' && value <= 'F');
|
||||
}
|
||||
|
||||
int base58Digit(QChar character)
|
||||
{
|
||||
const ushort value = character.unicode();
|
||||
if (value > 0x7f)
|
||||
return -1;
|
||||
for (int digit = 0; BASE58_ALPHABET[digit] != '\0'; ++digit) {
|
||||
if (BASE58_ALPHABET[digit] == static_cast<char>(value))
|
||||
return digit;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
QString walletAccountIdToBase58(const QString& accountId)
|
||||
@@ -26,7 +41,7 @@ QString walletAccountIdToBase58(const QString& accountId)
|
||||
}
|
||||
|
||||
const QByteArray bytes = QByteArray::fromHex(accountId.toLatin1());
|
||||
if (bytes.size() != 32)
|
||||
if (bytes.size() != ACCOUNT_ID_BYTES)
|
||||
return {};
|
||||
|
||||
qsizetype leadingZeroes = 0;
|
||||
@@ -54,3 +69,44 @@ QString walletAccountIdToBase58(const QString& accountId)
|
||||
encoded.append(QLatin1Char(BASE58_ALPHABET[*digit]));
|
||||
return encoded;
|
||||
}
|
||||
|
||||
QString walletAccountIdFromBase58(const QString& accountId)
|
||||
{
|
||||
if (accountId.size() < MIN_BASE58_ACCOUNT_ID_SIZE
|
||||
|| accountId.size() > MAX_BASE58_ACCOUNT_ID_SIZE) {
|
||||
return {};
|
||||
}
|
||||
|
||||
qsizetype leadingZeroes = 0;
|
||||
while (leadingZeroes < accountId.size()
|
||||
&& accountId.at(leadingZeroes) == QLatin1Char('1')) {
|
||||
++leadingZeroes;
|
||||
}
|
||||
|
||||
QVector<unsigned char> bytes;
|
||||
bytes.reserve(ACCOUNT_ID_BYTES);
|
||||
for (const QChar character : accountId) {
|
||||
int carry = base58Digit(character);
|
||||
if (carry < 0)
|
||||
return {};
|
||||
for (unsigned char& byte : bytes) {
|
||||
carry += static_cast<int>(byte) * 58;
|
||||
byte = static_cast<unsigned char>(carry % 256);
|
||||
carry /= 256;
|
||||
}
|
||||
while (carry > 0) {
|
||||
bytes.append(static_cast<unsigned char>(carry % 256));
|
||||
carry /= 256;
|
||||
}
|
||||
}
|
||||
|
||||
if (leadingZeroes > ACCOUNT_ID_BYTES
|
||||
|| bytes.size() != ACCOUNT_ID_BYTES - leadingZeroes) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray decoded(ACCOUNT_ID_BYTES, '\0');
|
||||
for (qsizetype index = 0; index < bytes.size(); ++index)
|
||||
decoded[ACCOUNT_ID_BYTES - index - 1] = static_cast<char>(bytes.at(index));
|
||||
return QString::fromLatin1(decoded.toHex());
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
#include <QString>
|
||||
|
||||
QString walletAccountIdToBase58(const QString& accountId);
|
||||
QString walletAccountIdFromBase58(const QString& accountId);
|
||||
|
||||
@@ -127,7 +127,7 @@ void WalletAccountModel::replaceAccounts(const QVector<WalletAccount>& accounts,
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void WalletAccountModel::applyPresentations(
|
||||
bool WalletAccountModel::applyPresentations(
|
||||
const QVector<WalletAccountPresentation>& presentations)
|
||||
{
|
||||
QHash<QString, int> rowsByAddress;
|
||||
@@ -141,10 +141,14 @@ void WalletAccountModel::applyPresentations(
|
||||
int firstChanged = m_accounts.size();
|
||||
int lastChanged = -1;
|
||||
for (const WalletAccountPresentation& presentation : presentations) {
|
||||
const auto row = rowsByAddress.constFind(presentation.address);
|
||||
const QString decodedAddress = walletAccountIdFromBase58(presentation.address);
|
||||
const QString& address = decodedAddress.isEmpty()
|
||||
? presentation.address : decodedAddress;
|
||||
const auto row = rowsByAddress.constFind(address);
|
||||
if (row == rowsByAddress.cend())
|
||||
continue;
|
||||
Entry& entry = m_accounts[row.value()];
|
||||
const Entry current = m_accounts.at(row.value());
|
||||
Entry entry = current;
|
||||
if (!presentation.kind.isEmpty())
|
||||
entry.kind = presentation.kind;
|
||||
entry.programName = presentation.programName;
|
||||
@@ -157,13 +161,32 @@ void WalletAccountModel::applyPresentations(
|
||||
if (!entry.canBePrimary)
|
||||
entry.isPrimary = false;
|
||||
updateEntryName(entry);
|
||||
if (entry.alias == current.alias
|
||||
&& entry.semanticName == current.semanticName
|
||||
&& entry.name == current.name
|
||||
&& entry.address == current.address
|
||||
&& entry.displayAddress == current.displayAddress
|
||||
&& entry.balance == current.balance
|
||||
&& entry.isPublic == current.isPublic
|
||||
&& entry.kind == current.kind
|
||||
&& entry.section == current.section
|
||||
&& entry.programOwner == current.programOwner
|
||||
&& entry.readStatus == current.readStatus
|
||||
&& entry.programName == current.programName
|
||||
&& entry.accountType == current.accountType
|
||||
&& entry.definitionId == current.definitionId
|
||||
&& entry.canBePrimary == current.canBePrimary
|
||||
&& entry.isPrimary == current.isPrimary) {
|
||||
continue;
|
||||
}
|
||||
m_accounts[row.value()] = std::move(entry);
|
||||
if (row.value() < firstChanged)
|
||||
firstChanged = row.value();
|
||||
if (row.value() > lastChanged)
|
||||
lastChanged = row.value();
|
||||
}
|
||||
if (lastChanged < 0)
|
||||
return;
|
||||
return false;
|
||||
emit dataChanged(index(firstChanged), index(lastChanged), {
|
||||
NameRole,
|
||||
KindRole,
|
||||
@@ -174,6 +197,7 @@ void WalletAccountModel::applyPresentations(
|
||||
IsPrimaryRole,
|
||||
DefinitionIdRole,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletAccountModel::setAlias(const QString& address, const QString& alias)
|
||||
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
void replaceAccounts(const QVector<WalletAccount>& accounts,
|
||||
const QHash<QString, QString>& aliases = {},
|
||||
const QString& primaryAddress = {});
|
||||
void applyPresentations(const QVector<WalletAccountPresentation>& presentations);
|
||||
bool applyPresentations(const QVector<WalletAccountPresentation>& presentations);
|
||||
void setAlias(const QString& address, const QString& alias);
|
||||
void setPrimaryAddress(const QString& address);
|
||||
bool contains(const QString& address) const;
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPointer>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
@@ -96,12 +98,56 @@ QString WalletController::defaultStoragePath() const
|
||||
return m_state.walletHome + QStringLiteral("/storage.json");
|
||||
}
|
||||
|
||||
void WalletController::setDefaultSequencerAddress(const QString& address)
|
||||
{
|
||||
const QString normalized = address.trimmed();
|
||||
const QUrl endpoint(normalized);
|
||||
const QString scheme = endpoint.scheme().toLower();
|
||||
if (endpoint.isValid()
|
||||
&& !endpoint.host().isEmpty()
|
||||
&& (scheme == QStringLiteral("http") || scheme == QStringLiteral("https"))) {
|
||||
m_defaultSequencerAddress = normalized;
|
||||
} else {
|
||||
m_defaultSequencerAddress.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletController::seedDefaultWalletConfig(const QString& configPath) const
|
||||
{
|
||||
if (m_defaultSequencerAddress.isEmpty() || QFileInfo::exists(configPath))
|
||||
return true;
|
||||
|
||||
const QFileInfo configInfo(configPath);
|
||||
if (!QDir().mkpath(configInfo.absolutePath())) {
|
||||
qWarning() << "WalletController: failed to create wallet configuration directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile config(configPath);
|
||||
if (!config.open(QIODevice::WriteOnly)) {
|
||||
qWarning() << "WalletController: failed to open wallet configuration";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray contents = QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("sequencer_addr"), m_defaultSequencerAddress },
|
||||
{ QStringLiteral("seq_poll_timeout"), QStringLiteral("12s") },
|
||||
{ QStringLiteral("seq_tx_poll_max_blocks"), 5 },
|
||||
{ QStringLiteral("seq_poll_max_retries"), 5 },
|
||||
{ QStringLiteral("seq_block_poll_max_amount"), 100 },
|
||||
}).toJson(QJsonDocument::Compact);
|
||||
if (config.write(contents) != contents.size() || !config.commit()) {
|
||||
qWarning() << "WalletController: failed to save wallet configuration";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletController::start()
|
||||
{
|
||||
if (m_started)
|
||||
return;
|
||||
m_started = true;
|
||||
m_reachabilityTimer->start();
|
||||
QTimer::singleShot(0, this, &WalletController::openOnStartup);
|
||||
}
|
||||
|
||||
@@ -141,73 +187,102 @@ bool WalletController::beginOpen(const QString& config, const QString& storage)
|
||||
emit stateChanged();
|
||||
}
|
||||
});
|
||||
const QPointer<WalletController> guard(this);
|
||||
m_wallet.connectAsync({ config, storage },
|
||||
[this, generation, config, storage](WalletSession session) {
|
||||
if (generation != m_operationGeneration)
|
||||
[guard, generation, config, storage](WalletSession session) {
|
||||
if (!guard || generation != guard->m_operationGeneration)
|
||||
return;
|
||||
if (session.failure == WalletFailure::WalletMissing) {
|
||||
m_state.syncStatus = QStringLiteral("closed");
|
||||
m_state.walletExists = false;
|
||||
emit stateChanged();
|
||||
guard->m_state.syncStatus = QStringLiteral("closed");
|
||||
guard->m_state.walletExists = false;
|
||||
emit guard->stateChanged();
|
||||
return;
|
||||
}
|
||||
if (!session.ok()) {
|
||||
qWarning() << "WalletController: wallet connection failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
m_state.syncStatus = QStringLiteral("error");
|
||||
m_state.syncError = walletFailureCode(session.failure);
|
||||
emit stateChanged();
|
||||
guard->m_state.syncStatus = QStringLiteral("error");
|
||||
guard->m_state.syncError = walletFailureCode(session.failure);
|
||||
emit guard->stateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
|
||||
m_state.isWalletOpen = true;
|
||||
m_state.syncStatus = QStringLiteral("ready");
|
||||
applySnapshot(session.snapshot);
|
||||
guard->m_state.configPath = config;
|
||||
guard->m_state.storagePath = storage;
|
||||
guard->m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
|
||||
guard->m_state.isWalletOpen = true;
|
||||
guard->m_state.syncStatus = QStringLiteral("ready");
|
||||
guard->applySnapshot(session.snapshot);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
QString WalletController::createDefaultWallet(const QString& password)
|
||||
{
|
||||
return createWallet(defaultConfigPath(), defaultStoragePath(), password);
|
||||
const QString config = defaultConfigPath();
|
||||
if (!seedDefaultWalletConfig(config))
|
||||
return {};
|
||||
return createWallet(config, defaultStoragePath(), password);
|
||||
}
|
||||
|
||||
QString WalletController::createWallet(const QString& configPath,
|
||||
const QString& storagePath,
|
||||
const QString& password)
|
||||
{
|
||||
const quint64 generation = ++m_operationGeneration;
|
||||
const QString config = toLocalPath(configPath);
|
||||
const QString storage = toLocalPath(storagePath);
|
||||
const WalletCreation creation = m_wallet.createWallet(
|
||||
{ config, storage }, password);
|
||||
const bool createdButUnreadable = creation.failure == WalletFailure::ReadFailed;
|
||||
if (creation.mnemonic.isEmpty()
|
||||
|| (!creation.ok() && !createdButUnreadable)) {
|
||||
if (creation.mnemonic.isEmpty()) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
stopReachability();
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = true;
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
|
||||
m_state.isWalletOpen = true;
|
||||
if (!creation.snapshot.ok()) {
|
||||
qWarning() << "WalletController: wallet creation refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
m_state.walletExists = QFileInfo::exists(storage);
|
||||
m_state.isWalletOpen = false;
|
||||
m_state.syncStatus = QStringLiteral("error");
|
||||
m_state.syncError = walletFailureCode(creation.snapshot.failure);
|
||||
m_state.syncError = walletFailureCode(creation.failure);
|
||||
emit stateChanged();
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
m_state.syncStatus = QStringLiteral("ready");
|
||||
m_state.walletExists = true;
|
||||
m_state.isWalletOpen = true;
|
||||
m_state.syncStatus = QStringLiteral("syncing");
|
||||
m_state.syncError.clear();
|
||||
applySnapshot(creation.snapshot);
|
||||
m_accountModel->replaceAccounts({});
|
||||
emit stateChanged();
|
||||
|
||||
const QPointer<WalletController> guard(this);
|
||||
QTimer::singleShot(0, this, [guard, generation]() {
|
||||
if (!guard || generation != guard->m_operationGeneration)
|
||||
return;
|
||||
guard->m_wallet.snapshotAsync(true,
|
||||
[guard, generation](WalletSnapshot snapshot) {
|
||||
if (!guard || generation != guard->m_operationGeneration)
|
||||
return;
|
||||
if (snapshot.ok()) {
|
||||
guard->m_state.syncStatus = QStringLiteral("ready");
|
||||
guard->applySnapshot(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << "WalletController: initial wallet sync failed"
|
||||
<< walletFailureCode(snapshot.failure);
|
||||
guard->m_state.syncStatus = QStringLiteral("error");
|
||||
guard->m_state.syncError = walletFailureCode(snapshot.failure);
|
||||
emit guard->stateChanged();
|
||||
});
|
||||
});
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
@@ -224,14 +299,11 @@ bool WalletController::open()
|
||||
void WalletController::disconnect()
|
||||
{
|
||||
++m_operationGeneration;
|
||||
stopReachability();
|
||||
m_wallet.disconnect();
|
||||
m_state.isWalletOpen = false;
|
||||
m_state.syncStatus = QStringLiteral("closed");
|
||||
m_state.syncError.clear();
|
||||
m_state.primaryAccountAddress.clear();
|
||||
m_state.primaryAccountName.clear();
|
||||
m_snapshot = {};
|
||||
m_aliases.clear();
|
||||
m_accountModel->replaceAccounts({});
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true);
|
||||
emit stateChanged();
|
||||
@@ -270,14 +342,22 @@ bool WalletController::setPrimaryAccount(const QString& address)
|
||||
void WalletController::applyAccountPresentations(
|
||||
const QVector<WalletAccountPresentation>& presentations)
|
||||
{
|
||||
m_accountModel->applyPresentations(presentations);
|
||||
if (!m_accountModel->applyPresentations(presentations))
|
||||
return;
|
||||
|
||||
const QString previousPrimary = m_state.primaryAccountAddress;
|
||||
const QString previousPrimaryName = m_state.primaryAccountName;
|
||||
QString primary = m_state.primaryAccountAddress;
|
||||
if (!m_accountModel->canBePrimary(primary))
|
||||
primary = m_accountModel->firstAutomaticPrimary();
|
||||
m_accountModel->setPrimaryAddress(primary);
|
||||
storePrimaryAccount(primary);
|
||||
if (primary != previousPrimary)
|
||||
storePrimaryAccount(primary);
|
||||
updatePrimaryState(primary);
|
||||
emit stateChanged();
|
||||
if (m_state.primaryAccountAddress != previousPrimary
|
||||
|| m_state.primaryAccountName != previousPrimaryName) {
|
||||
emit stateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString WalletController::createAccount(bool isPublic)
|
||||
@@ -310,18 +390,19 @@ void WalletController::refresh()
|
||||
m_state.syncStatus = QStringLiteral("syncing");
|
||||
m_state.syncError.clear();
|
||||
emit stateChanged();
|
||||
m_wallet.snapshotAsync(true, [this, generation](WalletSnapshot next) {
|
||||
if (generation != m_operationGeneration)
|
||||
const QPointer<WalletController> guard(this);
|
||||
m_wallet.snapshotAsync(true, [guard, generation](WalletSnapshot next) {
|
||||
if (!guard || generation != guard->m_operationGeneration)
|
||||
return;
|
||||
if (next.ok()) {
|
||||
m_state.syncStatus = QStringLiteral("ready");
|
||||
applySnapshot(next);
|
||||
guard->m_state.syncStatus = QStringLiteral("ready");
|
||||
guard->applySnapshot(next);
|
||||
} else {
|
||||
qWarning() << "WalletController: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
m_state.syncStatus = QStringLiteral("error");
|
||||
m_state.syncError = walletFailureCode(next.failure);
|
||||
emit stateChanged();
|
||||
guard->m_state.syncStatus = QStringLiteral("error");
|
||||
guard->m_state.syncError = walletFailureCode(next.failure);
|
||||
emit guard->stateChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -353,6 +434,8 @@ void WalletController::applySnapshot(const WalletSnapshot& snapshot)
|
||||
m_state.sequencerAddress = snapshot.sequencerAddress;
|
||||
emit snapshotChanged();
|
||||
emit stateChanged();
|
||||
if (!m_reachabilityTimer->isActive())
|
||||
m_reachabilityTimer->start();
|
||||
checkReachability();
|
||||
}
|
||||
|
||||
@@ -417,16 +500,44 @@ void WalletController::updatePrimaryState(const QString& address)
|
||||
}
|
||||
}
|
||||
|
||||
void WalletController::stopReachability()
|
||||
{
|
||||
m_reachabilityTimer->stop();
|
||||
++m_reachabilityGeneration;
|
||||
if (m_reachabilityReply) {
|
||||
QNetworkReply* reply = m_reachabilityReply;
|
||||
m_reachabilityReply = nullptr;
|
||||
m_reachabilityEndpoint.clear();
|
||||
reply->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void WalletController::checkReachability()
|
||||
{
|
||||
if (!m_state.isWalletOpen || m_state.sequencerAddress.isEmpty())
|
||||
return;
|
||||
|
||||
QNetworkRequest request{QUrl(m_state.sequencerAddress)};
|
||||
const QString endpoint = m_state.sequencerAddress;
|
||||
if (m_reachabilityReply && endpoint == m_reachabilityEndpoint)
|
||||
return;
|
||||
|
||||
const quint64 generation = ++m_reachabilityGeneration;
|
||||
if (m_reachabilityReply)
|
||||
m_reachabilityReply->abort();
|
||||
QNetworkRequest request{QUrl(endpoint)};
|
||||
request.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_network->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (!m_state.isWalletOpen) {
|
||||
m_reachabilityReply = reply;
|
||||
m_reachabilityEndpoint = endpoint;
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, reply, generation, endpoint]() {
|
||||
if (m_reachabilityReply == reply) {
|
||||
m_reachabilityReply = nullptr;
|
||||
m_reachabilityEndpoint.clear();
|
||||
}
|
||||
if (!m_state.isWalletOpen
|
||||
|| generation != m_reachabilityGeneration
|
||||
|| endpoint != m_state.sequencerAddress) {
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QTimer;
|
||||
class WalletAccountModel;
|
||||
struct WalletAccountPresentation;
|
||||
@@ -48,6 +49,7 @@ public:
|
||||
const WalletSnapshot& snapshot() const { return m_snapshot; }
|
||||
|
||||
void start();
|
||||
void setDefaultSequencerAddress(const QString& address);
|
||||
QString createAccount(bool isPublic);
|
||||
void refresh();
|
||||
QString balance(const QString& accountId, bool isPublic);
|
||||
@@ -70,11 +72,13 @@ private:
|
||||
static QString defaultWalletHome();
|
||||
QString defaultConfigPath() const;
|
||||
QString defaultStoragePath() const;
|
||||
bool seedDefaultWalletConfig(const QString& configPath) const;
|
||||
|
||||
void openOnStartup();
|
||||
bool beginOpen(const QString& config, const QString& storage);
|
||||
void applySnapshot(const WalletSnapshot& snapshot);
|
||||
void checkReachability();
|
||||
void stopReachability();
|
||||
QString walletSettingsGroup() const;
|
||||
QHash<QString, QString> loadAliases() const;
|
||||
QString loadPrimaryAccount() const;
|
||||
@@ -89,7 +93,11 @@ private:
|
||||
QHash<QString, QString> m_aliases;
|
||||
WalletAccountModel* m_accountModel;
|
||||
QNetworkAccessManager* m_network;
|
||||
QNetworkReply* m_reachabilityReply = nullptr;
|
||||
QString m_reachabilityEndpoint;
|
||||
QString m_defaultSequencerAddress;
|
||||
QTimer* m_reachabilityTimer;
|
||||
bool m_started = false;
|
||||
quint64 m_operationGeneration = 0;
|
||||
quint64 m_reachabilityGeneration = 0;
|
||||
};
|
||||
|
||||
@@ -99,6 +99,8 @@ public:
|
||||
using SessionCallback = std::function<void(WalletSession)>;
|
||||
using SnapshotCallback = std::function<void(WalletSnapshot)>;
|
||||
using AccountReadsCallback = std::function<void(QVector<WalletAccountRead>)>;
|
||||
using AccountCreationCallback = std::function<void(WalletAccountCreation)>;
|
||||
using SubmissionCallback = std::function<void(WalletSubmission)>;
|
||||
|
||||
virtual ~WalletProvider() = default;
|
||||
|
||||
@@ -110,10 +112,13 @@ 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 void readPublicAccountsAsync(const QStringList& accountIds,
|
||||
AccountReadsCallback callback) = 0;
|
||||
virtual WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) = 0;
|
||||
virtual void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QHostAddress>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QSettings>
|
||||
#include <QSignalSpy>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "FakeWalletProvider.h"
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "WalletAccountId.h"
|
||||
@@ -22,6 +29,30 @@ const QString ACCOUNT_C(64, QLatin1Char('d'));
|
||||
const QString PROGRAM_ID(64, QLatin1Char('c'));
|
||||
const QString EOA_OWNER(64, QLatin1Char('0'));
|
||||
|
||||
class ScopedEnvironment final {
|
||||
public:
|
||||
ScopedEnvironment(QByteArray name, QByteArray value)
|
||||
: m_name(std::move(name)),
|
||||
m_hadValue(qEnvironmentVariableIsSet(m_name.constData())),
|
||||
m_previous(qgetenv(m_name.constData()))
|
||||
{
|
||||
qputenv(m_name.constData(), value);
|
||||
}
|
||||
|
||||
~ScopedEnvironment()
|
||||
{
|
||||
if (m_hadValue)
|
||||
qputenv(m_name.constData(), m_previous);
|
||||
else
|
||||
qunsetenv(m_name.constData());
|
||||
}
|
||||
|
||||
private:
|
||||
QByteArray m_name;
|
||||
bool m_hadValue;
|
||||
QByteArray m_previous;
|
||||
};
|
||||
|
||||
QString publicAccountJson(const QString& owner = PROGRAM_ID,
|
||||
const QString& balance = QStringLiteral("01000000000000000000000000000000"),
|
||||
const QString& nonce = QString(32, QLatin1Char('0')),
|
||||
@@ -59,16 +90,28 @@ private slots:
|
||||
void fallsBackToBalanceWhenPublicReadFails();
|
||||
void createsAndPersistsAccounts();
|
||||
void preservesCreatedAccountWhenPublicReadFails();
|
||||
void preservesCreatedAccountWhenSnapshotRefreshFails();
|
||||
void createdAccountDoesNotRescanWallet();
|
||||
void dispatchesExactGenericTransaction();
|
||||
void rejectsInvalidSubmissionResponses();
|
||||
void encodesAccountIdsForDisplay();
|
||||
void walletMutationsUseAsyncSdk();
|
||||
void staleAsyncMutationCannotCrossSession();
|
||||
void destroyedProviderIgnoresLateMutation();
|
||||
void exposesStableAccountModelRoles();
|
||||
void encodesAccountIdsForDisplay();
|
||||
void persistsHumanizedWalletPreferences();
|
||||
void fakeProviderImplementsConsumerContract();
|
||||
void controllerOwnsUiWalletFlow();
|
||||
void controllerReportsCreationPersistenceAndRefreshFailures();
|
||||
void controllerSeparatesSnapshotsFromCosmeticState();
|
||||
void controllerOpenDoesNotWaitForWalletSync();
|
||||
void controllerCreationDoesNotWaitForWalletSync();
|
||||
void controllerSeedsDefaultWalletConfigWithConfiguredEndpoint();
|
||||
void controllerPreservesExistingDefaultWalletConfig();
|
||||
void controllerStopsReachabilityChecksAfterDisconnect();
|
||||
void completedAsyncSnapshotReleasesCallback();
|
||||
void deferredCallbacksIgnoreDestroyedController();
|
||||
void newerReachabilityResultWins();
|
||||
void coalescesReachabilityChecksForSameEndpoint();
|
||||
void controllerReportsPartialWalletCreation();
|
||||
};
|
||||
|
||||
void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||
@@ -103,6 +146,7 @@ void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||
|
||||
const int listCalls = modules.logos_execution_zone.listCalls;
|
||||
const int readCalls = modules.logos_execution_zone.publicReadCalls;
|
||||
const int saveCalls = modules.logos_execution_zone.saveCalls;
|
||||
QVERIFY(provider.snapshot().ok());
|
||||
QCOMPARE(modules.logos_execution_zone.listCalls, listCalls);
|
||||
QCOMPARE(modules.logos_execution_zone.publicReadCalls, readCalls);
|
||||
@@ -110,6 +154,7 @@ void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||
QVERIFY(provider.snapshot(true).ok());
|
||||
QVERIFY(modules.logos_execution_zone.listCalls > listCalls);
|
||||
QVERIFY(modules.logos_execution_zone.publicReadCalls > readCalls);
|
||||
QCOMPARE(modules.logos_execution_zone.saveCalls, saveCalls);
|
||||
|
||||
modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = publicAccountJson(
|
||||
PROGRAM_ID, QString(32, QLatin1Char('f')));
|
||||
@@ -236,6 +281,9 @@ void LogosWalletProviderTest::createsAndPersistsWallet()
|
||||
QVERIFY(directory.isValid());
|
||||
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.currentBlockHeight = 12;
|
||||
modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
|
||||
modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
|
||||
LogosWalletProvider provider(&modules);
|
||||
const WalletPaths paths {
|
||||
directory.filePath(QStringLiteral("config/wallet.json")),
|
||||
@@ -249,6 +297,9 @@ void LogosWalletProviderTest::createsAndPersistsWallet()
|
||||
QCOMPARE(modules.logos_execution_zone.createdStorage, paths.storage);
|
||||
QCOMPARE(modules.logos_execution_zone.createdPassword, QStringLiteral("secret"));
|
||||
QVERIFY(modules.logos_execution_zone.saveCalls >= 1);
|
||||
QCOMPARE(modules.logos_execution_zone.syncCalls, 0);
|
||||
QCOMPARE(modules.logos_execution_zone.listCalls, 0);
|
||||
QCOMPARE(modules.logos_execution_zone.publicReadCalls, 0);
|
||||
|
||||
LogosModules rejectedModules;
|
||||
rejectedModules.logos_execution_zone.mnemonic.clear();
|
||||
@@ -351,7 +402,7 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenPublicReadFails()
|
||||
QCOMPARE(creation.snapshot.accounts.at(0).balance, QStringLiteral("7"));
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails()
|
||||
void LogosWalletProviderTest::createdAccountDoesNotRescanWallet()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
@@ -362,11 +413,13 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails()
|
||||
|
||||
modules.logos_execution_zone.currentBlockHeight = 1;
|
||||
modules.logos_execution_zone.syncResult = 1;
|
||||
const int syncCalls = modules.logos_execution_zone.syncCalls;
|
||||
const WalletAccountCreation creation = provider.createAccount(true);
|
||||
|
||||
QVERIFY(creation.ok());
|
||||
QCOMPARE(creation.accountId, ACCOUNT_A);
|
||||
QCOMPARE(creation.snapshot.failure, WalletFailure::ReadFailed);
|
||||
QVERIFY(creation.snapshot.ok());
|
||||
QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::dispatchesExactGenericTransaction()
|
||||
@@ -427,6 +480,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;
|
||||
@@ -459,7 +614,7 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
QVERIFY(!model.data(model.index(2), WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
|
||||
QSignalSpy presentationsChanged(&model, &QAbstractItemModel::dataChanged);
|
||||
model.applyPresentations({
|
||||
const QVector<WalletAccountPresentation> presentations {
|
||||
{
|
||||
ACCOUNT_A,
|
||||
QStringLiteral("program"),
|
||||
@@ -470,7 +625,7 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
false,
|
||||
},
|
||||
{
|
||||
ACCOUNT_C,
|
||||
walletAccountIdToBase58(ACCOUNT_C),
|
||||
QStringLiteral("token_holding"),
|
||||
QStringLiteral("TEST holding"),
|
||||
QStringLiteral("Token"),
|
||||
@@ -478,7 +633,8 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
ACCOUNT_A,
|
||||
true,
|
||||
},
|
||||
});
|
||||
};
|
||||
model.applyPresentations(presentations);
|
||||
QCOMPARE(presentationsChanged.count(), 1);
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::SectionRole).toString(),
|
||||
QStringLiteral("hidden"));
|
||||
@@ -490,6 +646,10 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
model.setAlias(ACCOUNT_C, {});
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("TEST holding"));
|
||||
|
||||
QSignalSpy redundantPresentation(&model, &QAbstractItemModel::dataChanged);
|
||||
QVERIFY(!model.applyPresentations(presentations));
|
||||
QCOMPARE(redundantPresentation.count(), 0);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::encodesAccountIdsForDisplay()
|
||||
@@ -497,9 +657,16 @@ void LogosWalletProviderTest::encodesAccountIdsForDisplay()
|
||||
QCOMPARE(walletAccountIdToBase58(
|
||||
QStringLiteral("00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb")),
|
||||
QStringLiteral("14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La"));
|
||||
QCOMPARE(walletAccountIdFromBase58(
|
||||
QStringLiteral("14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La")),
|
||||
QStringLiteral("00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb"));
|
||||
QCOMPARE(walletAccountIdToBase58(QString(64, QLatin1Char('0'))),
|
||||
QString(32, QLatin1Char('1')));
|
||||
QCOMPARE(walletAccountIdFromBase58(QString(32, QLatin1Char('1'))),
|
||||
QString(64, QLatin1Char('0')));
|
||||
QVERIFY(walletAccountIdToBase58(QStringLiteral("not-an-account-id")).isEmpty());
|
||||
QVERIFY(walletAccountIdFromBase58(QString(32, QLatin1Char('0'))).isEmpty());
|
||||
QVERIFY(walletAccountIdFromBase58(QString(45, QLatin1Char('1'))).isEmpty());
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::persistsHumanizedWalletPreferences()
|
||||
@@ -599,62 +766,161 @@ void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerReportsCreationPersistenceAndRefreshFailures()
|
||||
void LogosWalletProviderTest::controllerSeparatesSnapshotsFromCosmeticState()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletCreationFailureTest");
|
||||
const QString settingsApplication = QStringLiteral("WalletSnapshotSignalTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("5"), true },
|
||||
};
|
||||
provider.snapshotResult = provider.connectResult.snapshot;
|
||||
WalletController controller(provider, settingsApplication);
|
||||
const WalletUiState baseline = controller.state();
|
||||
const int baselineAccountCount = controller.accountModel()->count();
|
||||
QSignalSpy stateChanged(&controller, &WalletController::stateChanged);
|
||||
QSignalSpy snapshotChanged(&controller, &WalletController::snapshotChanged);
|
||||
|
||||
provider.createWalletResult.mnemonic = QStringLiteral("alpha beta gamma");
|
||||
provider.createWalletResult.failure = WalletFailure::SaveFailed;
|
||||
provider.createWalletResult.snapshot.failure = WalletFailure::SaveFailed;
|
||||
QCOMPARE(controller.createWallet(QStringLiteral("config"), QStringLiteral("storage"),
|
||||
QStringLiteral("secret")),
|
||||
QString());
|
||||
QCOMPARE(controller.state().isWalletOpen, baseline.isWalletOpen);
|
||||
QCOMPARE(controller.state().walletExists, baseline.walletExists);
|
||||
QCOMPARE(controller.state().syncStatus, baseline.syncStatus);
|
||||
QCOMPARE(controller.state().syncError, baseline.syncError);
|
||||
QCOMPARE(controller.accountModel()->count(), baselineAccountCount);
|
||||
QVERIFY(controller.open());
|
||||
stateChanged.clear();
|
||||
snapshotChanged.clear();
|
||||
|
||||
QVERIFY(controller.setAccountAlias(ACCOUNT_A, QStringLiteral("Spending")));
|
||||
QCOMPARE(stateChanged.count(), 1);
|
||||
QCOMPARE(snapshotChanged.count(), 0);
|
||||
|
||||
provider.createWalletResult.failure = WalletFailure::ReadFailed;
|
||||
provider.createWalletResult.snapshot.failure = WalletFailure::ReadFailed;
|
||||
QCOMPARE(controller.createWallet(QStringLiteral("config"), QStringLiteral("storage"),
|
||||
QStringLiteral("secret")),
|
||||
QStringLiteral("alpha beta gamma"));
|
||||
QVERIFY(controller.state().isWalletOpen);
|
||||
QVERIFY(controller.state().walletExists);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("error"));
|
||||
QCOMPARE(controller.state().syncError, QStringLiteral("read_failed"));
|
||||
QCOMPARE(controller.accountModel()->count(), baselineAccountCount);
|
||||
QCOMPARE(snapshotChanged.count(), 0);
|
||||
|
||||
provider.createAccountResult.accountId = ACCOUNT_B;
|
||||
provider.createAccountResult.snapshot.failure = WalletFailure::ReadFailed;
|
||||
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("error"));
|
||||
QCOMPARE(controller.state().syncError, QStringLiteral("read_failed"));
|
||||
QCOMPARE(controller.accountModel()->count(), baselineAccountCount);
|
||||
QCOMPARE(snapshotChanged.count(), 0);
|
||||
|
||||
provider.createAccountResult.snapshot = {};
|
||||
provider.createAccountResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("5"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
{ ACCOUNT_B, QStringLiteral("3"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
};
|
||||
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
|
||||
QVERIFY(controller.state().syncError.isEmpty());
|
||||
QCOMPARE(controller.accountModel()->count(), 2);
|
||||
controller.refresh();
|
||||
QCOMPARE(stateChanged.count(), 3);
|
||||
QCOMPARE(snapshotChanged.count(), 1);
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerOpenDoesNotWaitForWalletSync()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletAsyncOpenTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.deferAsync = true;
|
||||
provider.connectResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("5"), true },
|
||||
};
|
||||
WalletController controller(provider, settingsApplication);
|
||||
|
||||
QVERIFY(controller.open());
|
||||
QCOMPARE(provider.connectCalls, 1);
|
||||
QVERIFY(!controller.state().isWalletOpen);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("opening"));
|
||||
QCOMPARE(controller.accountModel()->count(), 0);
|
||||
|
||||
provider.finishConnect();
|
||||
QVERIFY(controller.state().isWalletOpen);
|
||||
QVERIFY(controller.state().canSubmit());
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
|
||||
QCOMPARE(controller.accountModel()->count(), 1);
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerCreationDoesNotWaitForWalletSync()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletAsyncCreationTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.deferAsync = true;
|
||||
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
|
||||
provider.snapshotResult.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("5"), true },
|
||||
};
|
||||
WalletController controller(provider, settingsApplication);
|
||||
|
||||
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
|
||||
provider.createWalletResult.mnemonic);
|
||||
QCOMPARE(provider.createWalletCalls, 1);
|
||||
QCOMPARE(provider.snapshotCalls, 0);
|
||||
QVERIFY(controller.state().isWalletOpen);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("syncing"));
|
||||
QVERIFY(!controller.state().canSubmit());
|
||||
QCOMPARE(controller.accountModel()->count(), 0);
|
||||
|
||||
QTRY_COMPARE(provider.snapshotCalls, 1);
|
||||
QVERIFY(provider.lastForceRefresh);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("syncing"));
|
||||
provider.finishSnapshot();
|
||||
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
|
||||
QVERIFY(controller.state().canSubmit());
|
||||
QCOMPARE(controller.accountModel()->count(), 1);
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerSeedsDefaultWalletConfigWithConfiguredEndpoint()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
QVERIFY(directory.isValid());
|
||||
const QString walletHome = directory.filePath(QStringLiteral("wallet"));
|
||||
ScopedEnvironment walletHomeEnvironment(
|
||||
QByteArrayLiteral("LEE_WALLET_HOME_DIR"), walletHome.toLocal8Bit());
|
||||
const QString settingsApplication = QStringLiteral("WalletDefaultEndpointTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
|
||||
WalletController controller(provider, settingsApplication);
|
||||
controller.setDefaultSequencerAddress(QStringLiteral("https://testnet.lez.logos.co/"));
|
||||
|
||||
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
|
||||
provider.createWalletResult.mnemonic);
|
||||
const QString configPath = walletHome + QStringLiteral("/wallet_config.json");
|
||||
QCOMPARE(provider.lastPaths.config, configPath);
|
||||
|
||||
QFile config(configPath);
|
||||
QVERIFY(config.open(QIODevice::ReadOnly));
|
||||
const QJsonDocument document = QJsonDocument::fromJson(config.readAll());
|
||||
QVERIFY(document.isObject());
|
||||
const QJsonObject values = document.object();
|
||||
QCOMPARE(values.value(QStringLiteral("sequencer_addr")).toString(),
|
||||
QStringLiteral("https://testnet.lez.logos.co/"));
|
||||
QCOMPARE(values.value(QStringLiteral("seq_poll_timeout")).toString(),
|
||||
QStringLiteral("12s"));
|
||||
QCOMPARE(values.value(QStringLiteral("seq_tx_poll_max_blocks")).toInt(), 5);
|
||||
QCOMPARE(values.value(QStringLiteral("seq_poll_max_retries")).toInt(), 5);
|
||||
QCOMPARE(values.value(QStringLiteral("seq_block_poll_max_amount")).toInt(), 100);
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerPreservesExistingDefaultWalletConfig()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
QVERIFY(directory.isValid());
|
||||
const QString walletHome = directory.filePath(QStringLiteral("wallet"));
|
||||
QVERIFY(QDir().mkpath(walletHome));
|
||||
const QString configPath = walletHome + QStringLiteral("/wallet_config.json");
|
||||
const QByteArray existingConfig = QByteArrayLiteral(
|
||||
"{\"sequencer_addr\":\"http://127.0.0.1:3040/\",\"custom\":true}");
|
||||
QFile config(configPath);
|
||||
QVERIFY(config.open(QIODevice::WriteOnly));
|
||||
QCOMPARE(config.write(existingConfig), qint64(existingConfig.size()));
|
||||
config.close();
|
||||
|
||||
ScopedEnvironment walletHomeEnvironment(
|
||||
QByteArrayLiteral("LEE_WALLET_HOME_DIR"), walletHome.toLocal8Bit());
|
||||
const QString settingsApplication = QStringLiteral("WalletExistingEndpointTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
|
||||
WalletController controller(provider, settingsApplication);
|
||||
controller.setDefaultSequencerAddress(QStringLiteral("https://testnet.lez.logos.co/"));
|
||||
|
||||
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
|
||||
provider.createWalletResult.mnemonic);
|
||||
QVERIFY(config.open(QIODevice::ReadOnly));
|
||||
QCOMPARE(config.readAll(), existingConfig);
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
@@ -673,19 +939,164 @@ void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
|
||||
|
||||
QVERIFY(controller.open());
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!finished.isEmpty(), 1000);
|
||||
controller.disconnect();
|
||||
finished.clear();
|
||||
|
||||
auto* timer = controller.findChild<QTimer*>();
|
||||
QVERIFY(timer);
|
||||
QVERIFY(timer->isActive());
|
||||
controller.disconnect();
|
||||
QVERIFY(!timer->isActive());
|
||||
finished.clear();
|
||||
|
||||
timer->setInterval(1);
|
||||
controller.start();
|
||||
QTest::qWait(50);
|
||||
QVERIFY(!timer->isActive());
|
||||
QCOMPARE(finished.count(), 0);
|
||||
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::completedAsyncSnapshotReleasesCallback()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
LogosWalletProvider provider(&modules);
|
||||
QVERIFY(provider.connect({}).ok());
|
||||
|
||||
bool completed = false;
|
||||
std::weak_ptr<int> callbackLifetime;
|
||||
{
|
||||
auto lifetime = std::make_shared<int>(1);
|
||||
callbackLifetime = lifetime;
|
||||
provider.snapshotAsync(true,
|
||||
[lifetime = std::move(lifetime), &completed](WalletSnapshot snapshot) {
|
||||
QVERIFY(snapshot.ok());
|
||||
completed = true;
|
||||
});
|
||||
}
|
||||
|
||||
QVERIFY(completed);
|
||||
QVERIFY(callbackLifetime.expired());
|
||||
QCOMPARE(modules.logos_execution_zone.saveCalls, 0);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::deferredCallbacksIgnoreDestroyedController()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletDestroyedCallbackTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.deferAsync = true;
|
||||
{
|
||||
auto controller = std::make_unique<WalletController>(provider, settingsApplication);
|
||||
QVERIFY(controller->open());
|
||||
}
|
||||
provider.finishConnect();
|
||||
|
||||
provider.deferAsync = false;
|
||||
{
|
||||
auto controller = std::make_unique<WalletController>(provider, settingsApplication);
|
||||
QVERIFY(controller->open());
|
||||
provider.deferAsync = true;
|
||||
controller->refresh();
|
||||
}
|
||||
provider.finishSnapshot();
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::newerReachabilityResultWins()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletReachabilityOrderTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
QTcpServer firstServer;
|
||||
QTcpServer secondServer;
|
||||
QVERIFY(firstServer.listen(QHostAddress::LocalHost));
|
||||
QVERIFY(secondServer.listen(QHostAddress::LocalHost));
|
||||
const QString firstEndpoint = QStringLiteral("http://127.0.0.1:%1")
|
||||
.arg(firstServer.serverPort());
|
||||
const QString secondEndpoint = QStringLiteral("http://127.0.0.1:%1")
|
||||
.arg(secondServer.serverPort());
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.snapshot.sequencerAddress = firstEndpoint;
|
||||
WalletController controller(provider, settingsApplication);
|
||||
auto* network = controller.findChild<QNetworkAccessManager*>();
|
||||
QVERIFY(network);
|
||||
QSignalSpy finished(network, &QNetworkAccessManager::finished);
|
||||
|
||||
QVERIFY(controller.open());
|
||||
QTRY_VERIFY(firstServer.hasPendingConnections());
|
||||
QTcpSocket* first = firstServer.nextPendingConnection();
|
||||
QVERIFY(first);
|
||||
|
||||
provider.createAccountResult.accountId = ACCOUNT_B;
|
||||
provider.createAccountResult.snapshot.sequencerAddress = secondEndpoint;
|
||||
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
|
||||
QTRY_VERIFY(secondServer.hasPendingConnections());
|
||||
QTcpSocket* second = secondServer.nextPendingConnection();
|
||||
QVERIFY(second);
|
||||
|
||||
second->write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||
second->disconnectFromHost();
|
||||
QTRY_COMPARE(finished.size(), 1);
|
||||
QVERIFY(controller.state().sequencerReachable);
|
||||
|
||||
first->disconnectFromHost();
|
||||
QTRY_COMPARE(finished.size(), 2);
|
||||
QVERIFY(controller.state().sequencerReachable);
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::coalescesReachabilityChecksForSameEndpoint()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletReachabilityCoalesceTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
QTcpServer server;
|
||||
QVERIFY(server.listen(QHostAddress::LocalHost));
|
||||
const QString endpoint = QStringLiteral("http://127.0.0.1:%1").arg(server.serverPort());
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.snapshot.sequencerAddress = endpoint;
|
||||
WalletController controller(provider, settingsApplication);
|
||||
QVERIFY(controller.open());
|
||||
QTRY_VERIFY(server.hasPendingConnections());
|
||||
QTcpSocket* request = server.nextPendingConnection();
|
||||
QVERIFY(request);
|
||||
|
||||
provider.createAccountResult.accountId = ACCOUNT_B;
|
||||
provider.createAccountResult.snapshot.sequencerAddress = endpoint;
|
||||
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
|
||||
QTest::qWait(50);
|
||||
QVERIFY(!server.hasPendingConnections());
|
||||
|
||||
request->write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||
request->disconnectFromHost();
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerReportsPartialWalletCreation()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletPartialCreationTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
|
||||
provider.createWalletResult.failure = WalletFailure::SaveFailed;
|
||||
WalletController controller(provider, settingsApplication);
|
||||
|
||||
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
|
||||
provider.createWalletResult.mnemonic);
|
||||
QVERIFY(!controller.state().isWalletOpen);
|
||||
QCOMPARE(controller.state().syncStatus, QStringLiteral("error"));
|
||||
QCOMPARE(controller.state().syncError, QStringLiteral("save_failed"));
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(LogosWalletProviderTest)
|
||||
|
||||
#include "LogosWalletProviderTest.moc"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
|
||||
import Logos.Wallet as Wallet
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
width: 360
|
||||
height: 240
|
||||
|
||||
Component {
|
||||
id: copyButtonComponent
|
||||
|
||||
Wallet.CopyButton {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: clipboardSinkComponent
|
||||
|
||||
TextEdit {}
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: copyRequestedSpy
|
||||
}
|
||||
|
||||
TestCase {
|
||||
name: "CopyButton"
|
||||
when: windowShown
|
||||
|
||||
function test_copiesTextAndRetainsCopySignal() {
|
||||
const value = "1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"
|
||||
const copyButton = createTemporaryObject(copyButtonComponent, root, {
|
||||
"copyText": value,
|
||||
"copyLabel": "Copy address"
|
||||
})
|
||||
const sink = createTemporaryObject(clipboardSinkComponent, root)
|
||||
verify(copyButton, "Copy button exists")
|
||||
verify(sink, "Clipboard sink exists")
|
||||
compare(copyButton.implicitWidth, 36)
|
||||
compare(copyButton.implicitHeight, 36)
|
||||
|
||||
copyRequestedSpy.target = copyButton
|
||||
copyRequestedSpy.signalName = "copyRequested"
|
||||
copyRequestedSpy.clear()
|
||||
copyButton.click()
|
||||
|
||||
verify(copyButton.copied)
|
||||
compare(copyRequestedSpy.count, 1)
|
||||
sink.paste()
|
||||
tryCompare(sink, "text", value)
|
||||
copyRequestedSpy.target = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,38 @@ Item {
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_activityStateDoesNotBlockCancellation() {
|
||||
const dialog = createTemporaryObject(dialogComponent, root)
|
||||
verify(dialog, "Dialog exists")
|
||||
dialog.openWithSnapshot({ amount: "5" })
|
||||
tryCompare(dialog, "opened", true)
|
||||
|
||||
dialog.activityBusy = true
|
||||
const cancelButton = findChild(dialog, "transactionCancelButton")
|
||||
const confirmButton = findChild(dialog, "transactionConfirmButton")
|
||||
verify(cancelButton.enabled)
|
||||
verify(!confirmButton.enabled)
|
||||
|
||||
dialog.cancel()
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_cancelUsesTheConfirmationButtonShape() {
|
||||
const dialog = createTemporaryObject(dialogComponent, root)
|
||||
verify(dialog, "Dialog exists")
|
||||
dialog.roundedCancelButton = true
|
||||
dialog.openWithSnapshot({ amount: "5" })
|
||||
tryCompare(dialog, "opened", true)
|
||||
|
||||
const cancelButtonLoader = findChild(dialog, "transactionCancelButtonLoader")
|
||||
const confirmButton = findChild(dialog, "transactionConfirmButton")
|
||||
verify(cancelButtonLoader)
|
||||
tryVerify(function() {
|
||||
return cancelButtonLoader.item
|
||||
&& cancelButtonLoader.item.background.radius === confirmButton.background.radius
|
||||
})
|
||||
}
|
||||
|
||||
function test_keepsActionsInsideShortViewport() {
|
||||
const viewport = createTemporaryObject(viewportComponent, root)
|
||||
verify(viewport, "Short viewport exists")
|
||||
|
||||
@@ -20,6 +20,7 @@ Item {
|
||||
property string walletHome: "/wallet"
|
||||
property string walletSyncStatus: "closed"
|
||||
property string walletSyncError: ""
|
||||
property bool deferOpen: false
|
||||
property int openCalls: 0
|
||||
property int createCalls: 0
|
||||
property int publicAccountCalls: 0
|
||||
@@ -37,9 +38,11 @@ Item {
|
||||
|
||||
function openExisting() {
|
||||
openCalls++
|
||||
if (completeOpenImmediately) {
|
||||
walletSyncStatus = "ready"
|
||||
if (deferOpen) {
|
||||
walletSyncStatus = "opening"
|
||||
} else {
|
||||
isWalletOpen = true
|
||||
walletSyncStatus = "ready"
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -192,79 +195,17 @@ Item {
|
||||
tryCompare(fixture.control, "connected", true)
|
||||
}
|
||||
|
||||
function test_disablesConnectWhileWalletIsOpening() {
|
||||
const fixture = createControl({
|
||||
walletExists: true,
|
||||
walletSyncStatus: "syncing"
|
||||
}, [])
|
||||
const connectButton = findChild(fixture.control, "walletConnectButton")
|
||||
verify(!connectButton.enabled)
|
||||
compare(connectButton.text, "Connecting…")
|
||||
mouseClick(connectButton)
|
||||
compare(fixture.backend.openCalls, 0)
|
||||
}
|
||||
|
||||
function test_showsAsyncOpenFailure() {
|
||||
const fixture = createControl({
|
||||
walletExists: true,
|
||||
completeOpenImmediately: false
|
||||
}, [])
|
||||
const connectButton = findChild(fixture.control, "walletConnectButton")
|
||||
mouseClick(connectButton)
|
||||
function test_surfacesDeferredOpenFailure() {
|
||||
const fixture = createControl({ walletExists: true, deferOpen: true }, [])
|
||||
mouseClick(findChild(fixture.control, "walletConnectButton"))
|
||||
compare(fixture.backend.openCalls, 1)
|
||||
verify(fixture.control.busy)
|
||||
compare(fixture.control.syncStatus, "opening")
|
||||
|
||||
fixture.backend.walletSyncStatus = "error"
|
||||
fixture.backend.walletSyncError = "open_failed"
|
||||
|
||||
const dialog = findChild(fixture.control, "walletMessageDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
compare(fixture.control.busy, false)
|
||||
compare(dialog.message, "Wallet could not be opened: open_failed")
|
||||
}
|
||||
|
||||
function test_showsAsyncMissingWallet() {
|
||||
const fixture = createControl({
|
||||
walletExists: true,
|
||||
completeOpenImmediately: false
|
||||
}, [])
|
||||
mouseClick(findChild(fixture.control, "walletConnectButton"))
|
||||
verify(fixture.control.busy)
|
||||
|
||||
fixture.backend.walletExists = false
|
||||
|
||||
const dialog = findChild(fixture.control, "walletMessageDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
compare(fixture.control.busy, false)
|
||||
compare(dialog.message, "Wallet could not be opened.")
|
||||
}
|
||||
|
||||
function test_showsStartupOpenFailure() {
|
||||
const fixture = createControl({
|
||||
walletExists: true,
|
||||
walletSyncStatus: "error",
|
||||
walletSyncError: "open_failed"
|
||||
}, [])
|
||||
const dialog = findChild(fixture.control, "walletMessageDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
compare(dialog.message, "Wallet could not be opened: open_failed")
|
||||
}
|
||||
|
||||
function test_cancellingWalletCreationDoesNotClaimSuccess() {
|
||||
const fixture = createControl({ walletExists: false }, [])
|
||||
mouseClick(findChild(fixture.control, "walletConnectButton"))
|
||||
const creation = findChild(fixture.control, "createWalletDialog")
|
||||
tryCompare(creation, "opened", true)
|
||||
|
||||
fixture.backend.walletSyncStatus = "error"
|
||||
fixture.backend.walletSyncError = "wallet_unavailable"
|
||||
const message = findChild(fixture.control, "walletMessageDialog")
|
||||
tryCompare(message, "opened", true)
|
||||
compare(message.message, "Wallet could not be opened: wallet_unavailable")
|
||||
|
||||
creation.close()
|
||||
wait(0)
|
||||
compare(message.message, "Wallet could not be opened: wallet_unavailable")
|
||||
verify(dialog.message.includes("open_failed"))
|
||||
}
|
||||
|
||||
function test_requiresSeedBackupAcknowledgement() {
|
||||
@@ -357,6 +298,31 @@ Item {
|
||||
tryCompare(fixture.control, "connected", false)
|
||||
}
|
||||
|
||||
function test_waitsForPrimaryDelegateBeforeShowingAccountType() {
|
||||
const address = "a".repeat(64)
|
||||
const fixture = createControl({
|
||||
isWalletOpen: true,
|
||||
primaryAccountAddress: address,
|
||||
primaryAccountName: "Primary"
|
||||
}, [])
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
|
||||
const accountType = findChild(fixture.control, "walletPrimaryAccountType")
|
||||
verify(accountType, "Primary account type exists")
|
||||
verify(!accountType.visible, "Account type waits for its selected delegate")
|
||||
|
||||
fixture.model.append(accountData({
|
||||
name: "Primary",
|
||||
address: address,
|
||||
balance: "10",
|
||||
isPublic: true,
|
||||
isPrimary: true
|
||||
}))
|
||||
tryCompare(fixture.control, "selectedAddress", address)
|
||||
tryCompare(accountType, "visible", true)
|
||||
compare(accountType.text, "Public user account")
|
||||
}
|
||||
|
||||
function test_connectedButtonClosesOpenMenu() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
|
||||
@@ -370,6 +336,53 @@ Item {
|
||||
tryCompare(menu, "opened", false)
|
||||
}
|
||||
|
||||
function test_walletMenuClosesWithEscape() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
|
||||
])
|
||||
const accountButton = findChild(fixture.control, "walletAccountButton")
|
||||
const menu = findChild(fixture.control, "walletMenu")
|
||||
|
||||
mouseClick(accountButton)
|
||||
tryCompare(menu, "opened", true)
|
||||
keyClick(Qt.Key_Escape)
|
||||
tryCompare(menu, "opened", false)
|
||||
}
|
||||
|
||||
function test_createAccountDialogOwnsKeyboardFocus() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
|
||||
])
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
mouseClick(findChild(fixture.control, "walletAccountsButton"))
|
||||
|
||||
const backButton = findChild(fixture.control, "walletAccountsBackButton")
|
||||
const addButton = findChild(fixture.control, "walletAddAccountButton")
|
||||
verify(backButton && addButton, "Account controls exist")
|
||||
mouseClick(addButton)
|
||||
|
||||
const dialog = findChild(fixture.control, "createAccountDialog")
|
||||
const privateSwitch = findChild(dialog, "privateAccountSwitch")
|
||||
tryCompare(dialog, "opened", true)
|
||||
tryVerify(function() { return privateSwitch.activeFocus })
|
||||
for (let index = 0; index < 6; ++index) {
|
||||
keyClick(Qt.Key_Tab)
|
||||
verify(!backButton.activeFocus, "Focus remains inside the dialog")
|
||||
}
|
||||
keyClick(Qt.Key_Escape)
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_walletMessageDialogClosesWithEscape() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [])
|
||||
const dialog = findChild(fixture.control, "walletMessageDialog")
|
||||
|
||||
dialog.open()
|
||||
tryCompare(dialog, "opened", true)
|
||||
keyClick(Qt.Key_Escape)
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_openMenuTracksControlMovement() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
|
||||
@@ -427,6 +440,62 @@ Item {
|
||||
compare(fixture.control.selectedDisplayAddress, "base58-two")
|
||||
}
|
||||
|
||||
function test_flatWalletActionsUseReadableForeground() {
|
||||
const fixture = createControl({
|
||||
isWalletOpen: true,
|
||||
assets: [{
|
||||
name: "Available",
|
||||
balance: "0",
|
||||
definitionId: "c".repeat(64),
|
||||
displayDefinitionId: "base58-available",
|
||||
status: "ready",
|
||||
section: "available"
|
||||
}]
|
||||
}, [
|
||||
{
|
||||
name: "One",
|
||||
address: "a".repeat(64),
|
||||
balance: "10",
|
||||
isPublic: true,
|
||||
isPrimary: true
|
||||
},
|
||||
{
|
||||
name: "Two",
|
||||
address: "b".repeat(64),
|
||||
balance: "20",
|
||||
isPublic: true
|
||||
}
|
||||
])
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
|
||||
const available = findChild(fixture.control, "walletAvailableAssetsButton")
|
||||
tryVerify(function() { return available && available.visible })
|
||||
compare(available.flat, true)
|
||||
compare(available.palette.windowText, "#d4d4d8")
|
||||
compare(available.contentItem.color, "#d4d4d8")
|
||||
mouseClick(available)
|
||||
tryCompare(fixture.control, "availableExpanded", true)
|
||||
|
||||
mouseClick(findChild(fixture.control, "walletAccountsButton"))
|
||||
const advanced = findChild(fixture.control, "walletAdvancedAccountsButton")
|
||||
tryVerify(function() { return advanced && advanced.visible })
|
||||
compare(advanced.flat, true)
|
||||
compare(advanced.palette.windowText, "#d4d4d8")
|
||||
compare(advanced.contentItem.color, "#d4d4d8")
|
||||
|
||||
const accountList = findChild(fixture.control, "walletAccountList")
|
||||
tryVerify(function() { return accountList.itemAtIndex(1) !== null })
|
||||
const secondAccount = accountList.itemAtIndex(1)
|
||||
const rename = findChild(secondAccount, "walletRenameButton")
|
||||
const makePrimary = findChild(secondAccount, "walletMakePrimaryButton")
|
||||
verify(rename && makePrimary, "Account action buttons exist")
|
||||
for (const action of [rename, makePrimary]) {
|
||||
compare(action.flat, true)
|
||||
compare(action.palette.windowText, "#d4d4d8")
|
||||
compare(action.contentItem.color, "#d4d4d8")
|
||||
}
|
||||
}
|
||||
|
||||
function test_accountNavigationKeepsOverviewInsidePopup() {
|
||||
const assets = []
|
||||
for (let index = 0; index < 10; ++index) {
|
||||
|
||||
@@ -35,6 +35,9 @@ public:
|
||||
AccountReadsCallback callback;
|
||||
};
|
||||
QVector<PendingPublicAccountRead> pendingPublicAccountReads;
|
||||
bool deferAsync = false;
|
||||
SessionCallback pendingConnectCallback;
|
||||
SnapshotCallback pendingSnapshotCallback;
|
||||
|
||||
WalletSession connect(const WalletPaths& paths) override
|
||||
{
|
||||
@@ -47,7 +50,10 @@ public:
|
||||
{
|
||||
++connectCalls;
|
||||
lastPaths = paths;
|
||||
callback(connectResult);
|
||||
if (deferAsync)
|
||||
pendingConnectCallback = std::move(callback);
|
||||
else
|
||||
callback(connectResult);
|
||||
}
|
||||
|
||||
WalletCreation createWallet(const WalletPaths& paths,
|
||||
@@ -69,7 +75,10 @@ public:
|
||||
{
|
||||
++snapshotCalls;
|
||||
lastForceRefresh = forceRefresh;
|
||||
callback(snapshotResult);
|
||||
if (deferAsync)
|
||||
pendingSnapshotCallback = std::move(callback);
|
||||
else
|
||||
callback(snapshotResult);
|
||||
}
|
||||
|
||||
void clearSnapshot() override { ++clearCalls; }
|
||||
@@ -81,6 +90,11 @@ public:
|
||||
return createAccountResult;
|
||||
}
|
||||
|
||||
void createAccountAsync(bool isPublic, AccountCreationCallback callback) override
|
||||
{
|
||||
callback(createAccount(isPublic));
|
||||
}
|
||||
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override
|
||||
{
|
||||
++readCalls;
|
||||
@@ -117,8 +131,28 @@ public:
|
||||
return submissionResult;
|
||||
}
|
||||
|
||||
void submitPublicTransactionAsync(
|
||||
const WalletTransaction& transaction, SubmissionCallback callback) override
|
||||
{
|
||||
callback(submitPublicTransaction(transaction));
|
||||
}
|
||||
|
||||
void disconnect() override { ++disconnectCalls; }
|
||||
|
||||
void finishConnect()
|
||||
{
|
||||
SessionCallback callback = std::move(pendingConnectCallback);
|
||||
if (callback)
|
||||
callback(connectResult);
|
||||
}
|
||||
|
||||
void finishSnapshot()
|
||||
{
|
||||
SnapshotCallback callback = std::move(pendingSnapshotCallback);
|
||||
if (callback)
|
||||
callback(snapshotResult);
|
||||
}
|
||||
|
||||
private:
|
||||
QVector<WalletAccountRead> accountReads(const QStringList& accountIds) const
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user