mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(wallet): humanize shared wallet experience
This commit is contained in:
Generated
+11
@@ -4352,6 +4352,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wallet-idl-decoder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base58",
|
||||
"hex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"spel-framework-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
||||
@@ -18,6 +18,7 @@ members = [
|
||||
"programs/integration_tests",
|
||||
"tools/idl-gen",
|
||||
"tools/risc0-packager",
|
||||
"tools/wallet-idl-decoder",
|
||||
]
|
||||
exclude = [
|
||||
"programs/token/methods/guest",
|
||||
|
||||
@@ -31,6 +31,10 @@ logos_module(
|
||||
src/AmmUiPlugin.cpp
|
||||
src/AmmUiBackend.h
|
||||
src/AmmUiBackend.cpp
|
||||
src/ActiveNetwork.h
|
||||
src/ActiveNetwork.cpp
|
||||
src/WalletIdlDecoder.h
|
||||
src/WalletIdlDecoder.cpp
|
||||
FIND_PACKAGES
|
||||
Qt6Gui
|
||||
Qt6Network
|
||||
@@ -39,4 +43,35 @@ logos_module(
|
||||
Qt6::Network
|
||||
LINK_TARGETS
|
||||
logos_wallet_access
|
||||
EXTERNAL_LIBS
|
||||
wallet_idl_decoder
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
config/idl/token-idl.json
|
||||
PROPERTIES QT_RESOURCE_ALIAS "idl/token-idl.json"
|
||||
)
|
||||
set_source_files_properties(
|
||||
config/idl/amm-idl.json
|
||||
PROPERTIES QT_RESOURCE_ALIAS "idl/amm-idl.json"
|
||||
)
|
||||
qt_add_resources(amm_ui_module_plugin amm_ui_wallet_data
|
||||
PREFIX "/amm"
|
||||
FILES
|
||||
config/networks.json
|
||||
config/idl/token-idl.json
|
||||
config/idl/amm-idl.json
|
||||
)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Test)
|
||||
add_executable(amm_active_network_test
|
||||
tests/cpp/ActiveNetworkTest.cpp
|
||||
src/ActiveNetwork.cpp
|
||||
)
|
||||
set_target_properties(amm_active_network_test PROPERTIES AUTOMOC ON)
|
||||
target_include_directories(amm_active_network_test PRIVATE src)
|
||||
target_link_libraries(amm_active_network_test PRIVATE Qt6::Core Qt6::Test)
|
||||
add_test(NAME amm_active_network COMMAND amm_active_network_test)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,791 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"name": "amm",
|
||||
"instructions": [
|
||||
{
|
||||
"name": "initialize",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "token_program_id",
|
||||
"type": "program_id"
|
||||
},
|
||||
{
|
||||
"name": "twap_oracle_program_id",
|
||||
"type": "program_id"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update_config",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"writable": false,
|
||||
"signer": true,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "token_program_id",
|
||||
"type": {
|
||||
"option": "program_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "twap_oracle_program_id",
|
||||
"type": {
|
||||
"option": "program_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "new_authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "create_price_observations",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "price_observations",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "window_duration",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "create_oracle_price_account",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "oracle_price_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "window_duration",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "new_definition",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool_definition_lp",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "lp_lock_holding",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "user_holding_a",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_b",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_lp",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "token_a_amount",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "token_b_amount",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "fees",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "add_liquidity",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool_definition_lp",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_a",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_b",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_lp",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "min_amount_liquidity",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "max_amount_to_add_token_a",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "max_amount_to_add_token_b",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "remove_liquidity",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool_definition_lp",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_a",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_b",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_lp",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "remove_liquidity_amount",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "min_amount_to_remove_token_a",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "min_amount_to_remove_token_b",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "swap_exact_input",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_a",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_b",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "swap_amount_in",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "min_amount_out",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "token_definition_id_in",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "swap_exact_output",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_a",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_b",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "exact_amount_out",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "max_amount_in",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "token_definition_id_in",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sync_reserves",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "PoolDefinition",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_token_a_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "definition_token_b_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "vault_a_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "vault_b_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "liquidity_pool_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "liquidity_pool_supply",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "reserve_a",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "reserve_b",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "fees",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AmmConfig",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "token_program_id",
|
||||
"type": "program_id"
|
||||
},
|
||||
{
|
||||
"name": "twap_oracle_program_id",
|
||||
"type": "program_id"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TokenDefinition",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Fungible",
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "total_supply",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "metadata_id",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NonFungible",
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "printable_supply",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "metadata_id",
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TokenHolding",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Fungible",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NftMaster",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "print_balance",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NftPrintedCopy",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "owned",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TokenMetadata",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"type": {
|
||||
"defined": "MetadataStandard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "creators",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "primary_sale_date",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "MetadataStandard",
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Simple"
|
||||
},
|
||||
{
|
||||
"name": "Expanded"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instruction_type": "amm_core::Instruction"
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"name": "token",
|
||||
"instructions": [
|
||||
{
|
||||
"name": "transfer",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "sender",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount_to_transfer",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "new_fungible_definition",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_target_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "holding_target_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "total_supply",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "mint_authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "new_definition_with_metadata",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_target_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "holding_target_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
},
|
||||
{
|
||||
"name": "metadata_target_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "new_definition",
|
||||
"type": {
|
||||
"defined": "NewTokenDefinition"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "metadata",
|
||||
"type": {
|
||||
"defined": "Box"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "initialize_account",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "account_to_initialize",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "burn",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount_to_burn",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mint",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount_to_mint",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mint_with_authority",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "authority_account",
|
||||
"writable": false,
|
||||
"signer": true,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount_to_mint",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "set_authority",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "new_authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "set_authority_with_authority",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "definition_account",
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "authority_account",
|
||||
"writable": false,
|
||||
"signer": true,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "new_authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "print_nft",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "master_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "printed_account",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": true
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "TokenDefinition",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Fungible",
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "total_supply",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "metadata_id",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NonFungible",
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "printable_supply",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "metadata_id",
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TokenHolding",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Fungible",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NftMaster",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "print_balance",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NftPrintedCopy",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "owned",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TokenMetadata",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"type": {
|
||||
"defined": "MetadataStandard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "creators",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "primary_sale_date",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "MetadataStandard",
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Simple"
|
||||
},
|
||||
{
|
||||
"name": "Expanded"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instruction_type": "token_core::Instruction"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"testnet": {
|
||||
"checkpointHash": "0d25d71fca70d7008a892f6b3f768a4c66badbcd64e67d79ca595b92f1db544a",
|
||||
"ammProgramId": "77eeaa23668ad2675fb768cd7ecb1893387be464b9a51f16756006c1d307db07",
|
||||
"tokenDefinitionIds": [
|
||||
"7b464ff9dd0d3bc07f7e2e0b0667ccd066d85ad12be4c79fc55687a863910aa6",
|
||||
"48c81cf032e601ca367fc9816b957dbf5c0e4c11cf7008e8f4581ec1a67aab42",
|
||||
"159caef810ea545951b3bd913efe625ee45008c80865c330e72a72ed48b61649",
|
||||
"75f33110b185717209e3955f228d4a4448801d0ce8ba438a4a268050eeff3f44",
|
||||
"fbd107ca4bb66bc58f59ac2d32a759be3ee0fb453f8fecd1991c11837d9660c7",
|
||||
"5547fcb72644d95a385d313b887a96be41ff263bce6150b49fd87276839822bf",
|
||||
"fa43e74a97d79c5f907ff3edabda5ad89bfbd3b0922572e675d4ad3c7b6029c7",
|
||||
"4f3231d8a01e1d79f163bc27fce0c860a4a2f6890280e9d135eafbde0d68ed79",
|
||||
"fa32f354408857006f8ea396b0419823bd04436eadb2d273d2618a46b4793ed8",
|
||||
"00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb"
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
+35
-1
@@ -1,6 +1,22 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1779041105,
|
||||
"narHash": "sha256-nnGD2f8OlAZT2i5OfwikJsw+ifWfiA4d6A8BWlgOXV0=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "10e6e3cb966f7cfcc789fe5eee7a85f3188ce08b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"ref": "v0.23.4",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane_2": {
|
||||
"locked": {
|
||||
"lastModified": 1780532242,
|
||||
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
|
||||
@@ -2225,7 +2241,7 @@
|
||||
},
|
||||
"logos-execution-zone": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"crane": "crane_2",
|
||||
"logos-blockchain-circuits": "logos-blockchain-circuits",
|
||||
"logos-liblogos": "logos-liblogos_4",
|
||||
"nixpkgs": [
|
||||
@@ -25408,6 +25424,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_399": {
|
||||
"locked": {
|
||||
"lastModified": 1782841183,
|
||||
"narHash": "sha256-Ndt/5R7UN4rBdhFR1lxHZZZ42cD6vGlnuxC2VvvsKE4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c9bfd86ed684d27e63b0ff9ebb18699f84f27a3b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1759036355,
|
||||
@@ -26762,8 +26794,10 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"logos-module-builder": "logos-module-builder",
|
||||
"logos_execution_zone": "logos_execution_zone",
|
||||
"nixpkgs": "nixpkgs_399",
|
||||
"shared_wallet": "shared_wallet"
|
||||
}
|
||||
},
|
||||
|
||||
+11
-2
@@ -3,6 +3,8 @@
|
||||
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
|
||||
crane.url = "github:ipetkov/crane/v0.23.4";
|
||||
|
||||
# Shared C++ wallet access and Logos.Wallet QML sources.
|
||||
shared_wallet = {
|
||||
@@ -28,14 +30,21 @@
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, shared_wallet, ... }:
|
||||
logos-module-builder.lib.mkLogosQmlModule {
|
||||
outputs = inputs@{ logos-module-builder, shared_wallet, nixpkgs, crane, ... }:
|
||||
let
|
||||
walletDecoderInput = (import ../../flake.nix).outputs {
|
||||
inherit nixpkgs crane;
|
||||
};
|
||||
in logos-module-builder.lib.mkLogosQmlModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
preConfigure = ''
|
||||
cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${shared_wallet}")
|
||||
'';
|
||||
externalLibInputs = {
|
||||
wallet_idl_decoder = walletDecoderInput;
|
||||
};
|
||||
postInstall = ''
|
||||
# The builder installs the view under lib/qml after this hook. Its
|
||||
# import descriptor points back to this compiled shared QML module.
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
"build": [],
|
||||
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp"]
|
||||
},
|
||||
"external_libraries": [],
|
||||
"external_libraries": [
|
||||
{
|
||||
"name": "wallet_idl_decoder"
|
||||
}
|
||||
],
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"extra_sources": [],
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "ActiveNetwork.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace {
|
||||
const char NETWORK_ENV[] = "AMM_UI_NETWORK";
|
||||
const char DEVNET_FILE_ENV[] = "AMM_UI_DEVNET_FILE";
|
||||
|
||||
bool isLowerHex(const QString& value, int size)
|
||||
{
|
||||
if (value.size() != size)
|
||||
return false;
|
||||
for (const QChar character : value) {
|
||||
const bool digit = character >= QLatin1Char('0')
|
||||
&& character <= QLatin1Char('9');
|
||||
if (!digit && (character < QLatin1Char('a') || character > QLatin1Char('f')))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ActiveNetwork::load()
|
||||
{
|
||||
m_network = {};
|
||||
m_network.status = QStringLiteral("config_missing");
|
||||
m_expectedIdentity.clear();
|
||||
const QByteArray selected = qgetenv(NETWORK_ENV);
|
||||
m_network.id = selected.isEmpty()
|
||||
? QStringLiteral("testnet")
|
||||
: QString::fromLocal8Bit(selected).trimmed();
|
||||
|
||||
QJsonObject entry;
|
||||
if (isDevnet()) {
|
||||
const QString path = QString::fromLocal8Bit(qgetenv(DEVNET_FILE_ENV));
|
||||
QFile file(path);
|
||||
if (path.isEmpty() || !file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
|
||||
if (!document.isObject())
|
||||
return false;
|
||||
entry = document.object();
|
||||
m_expectedIdentity = entry.value(QStringLiteral("channelId")).toString();
|
||||
} else {
|
||||
QFile file(QStringLiteral(":/amm/config/networks.json"));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
|
||||
if (!document.isObject())
|
||||
return false;
|
||||
entry = document.object().value(m_network.id).toObject();
|
||||
m_expectedIdentity = entry.value(QStringLiteral("checkpointHash")).toString();
|
||||
}
|
||||
|
||||
m_network.ammProgramId = entry.value(QStringLiteral("ammProgramId")).toString();
|
||||
if (!isValidIdentity(m_expectedIdentity)
|
||||
|| !isLowerHex(m_network.ammProgramId, 64)) {
|
||||
return false;
|
||||
}
|
||||
for (const QJsonValue& value : entry.value(QStringLiteral("tokenDefinitionIds")).toArray()) {
|
||||
const QString id = value.toString();
|
||||
if (!isLowerHex(id, 64)) {
|
||||
m_network.tokenIds.clear();
|
||||
return false;
|
||||
}
|
||||
m_network.tokenIds.append(id);
|
||||
}
|
||||
if (m_network.tokenIds.isEmpty())
|
||||
return false;
|
||||
m_network.status = QStringLiteral("network_unknown");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ActiveNetwork::isConfigured() const
|
||||
{
|
||||
return m_network.status != QStringLiteral("config_missing");
|
||||
}
|
||||
|
||||
bool ActiveNetwork::isDevnet() const
|
||||
{
|
||||
return m_network.id == QStringLiteral("devnet");
|
||||
}
|
||||
|
||||
bool ActiveNetwork::needsIdentityProbe() const
|
||||
{
|
||||
return m_network.status == QStringLiteral("loading")
|
||||
|| m_network.status == QStringLiteral("network_unknown");
|
||||
}
|
||||
|
||||
void ActiveNetwork::sequencerChanged(bool available)
|
||||
{
|
||||
if (isConfigured())
|
||||
clearIdentity(available ? QStringLiteral("loading")
|
||||
: QStringLiteral("network_unknown"));
|
||||
}
|
||||
|
||||
void ActiveNetwork::reachabilityChanged(bool reachable, bool wasReachable)
|
||||
{
|
||||
if (!isConfigured())
|
||||
return;
|
||||
if (!reachable)
|
||||
clearIdentity(QStringLiteral("network_unknown"));
|
||||
else if (!wasReachable)
|
||||
clearIdentity(QStringLiteral("loading"));
|
||||
}
|
||||
|
||||
void ActiveNetwork::beginIdentityProbe()
|
||||
{
|
||||
if (isConfigured())
|
||||
clearIdentity(QStringLiteral("loading"));
|
||||
}
|
||||
|
||||
void ActiveNetwork::finishIdentityProbe(const QString& identity)
|
||||
{
|
||||
if (identity.isEmpty())
|
||||
clearIdentity(QStringLiteral("network_unknown"));
|
||||
else if (identity != m_expectedIdentity)
|
||||
clearIdentity(QStringLiteral("network_mismatch"));
|
||||
else {
|
||||
m_network.status = QStringLiteral("ready");
|
||||
m_network.fingerprint = (isDevnet() ? QStringLiteral("channel:")
|
||||
: QStringLiteral("block10:"))
|
||||
+ identity;
|
||||
}
|
||||
}
|
||||
|
||||
bool ActiveNetwork::isValidIdentity(const QString& value)
|
||||
{
|
||||
return isLowerHex(value, 64);
|
||||
}
|
||||
|
||||
void ActiveNetwork::clearIdentity(const QString& status)
|
||||
{
|
||||
m_network.status = status;
|
||||
m_network.fingerprint.clear();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
struct ActiveNetworkSnapshot {
|
||||
QString id;
|
||||
QString status;
|
||||
QString fingerprint;
|
||||
QString ammProgramId;
|
||||
QStringList tokenIds;
|
||||
};
|
||||
|
||||
class ActiveNetwork final {
|
||||
public:
|
||||
bool load();
|
||||
|
||||
const QString& status() const { return m_network.status; }
|
||||
bool isConfigured() const;
|
||||
bool isDevnet() const;
|
||||
bool needsIdentityProbe() const;
|
||||
ActiveNetworkSnapshot snapshot() const { return m_network; }
|
||||
|
||||
void sequencerChanged(bool available);
|
||||
void reachabilityChanged(bool reachable, bool wasReachable);
|
||||
void beginIdentityProbe();
|
||||
void finishIdentityProbe(const QString& identity);
|
||||
|
||||
static bool isValidIdentity(const QString& value);
|
||||
|
||||
private:
|
||||
void clearIdentity(const QString& status);
|
||||
|
||||
ActiveNetworkSnapshot m_network;
|
||||
QString m_expectedIdentity;
|
||||
};
|
||||
@@ -1,18 +1,132 @@
|
||||
#include "AmmUiBackend.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QUrl>
|
||||
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "WalletAccountId.h"
|
||||
#include "WalletController.h"
|
||||
#include "WalletIdlDecoder.h"
|
||||
#include "logos_api.h"
|
||||
|
||||
namespace {
|
||||
constexpr int CHECKPOINT_BLOCK_ID = 10;
|
||||
constexpr int BLOCK_HASH_OFFSET = 40;
|
||||
constexpr int BLOCK_HASH_SIZE = 32;
|
||||
const QString DEFAULT_PROGRAM_OWNER(64, QLatin1Char('0'));
|
||||
|
||||
QByteArray resource(const QString& path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
QByteArray jsonRpcBody(const QString& method, const QJsonArray& params)
|
||||
{
|
||||
return QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("jsonrpc"), QStringLiteral("2.0") },
|
||||
{ QStringLiteral("id"), 1 },
|
||||
{ QStringLiteral("method"), method },
|
||||
{ QStringLiteral("params"), params },
|
||||
}).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
QString blockHashFromResponse(const QByteArray& payload)
|
||||
{
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||
return {};
|
||||
const QByteArray block = QByteArray::fromBase64(
|
||||
document.object().value(QStringLiteral("result")).toString().toLatin1());
|
||||
if (block.size() < BLOCK_HASH_OFFSET + BLOCK_HASH_SIZE)
|
||||
return {};
|
||||
return QString::fromLatin1(block.mid(BLOCK_HASH_OFFSET, BLOCK_HASH_SIZE).toHex());
|
||||
}
|
||||
|
||||
QString channelIdFromResponse(const QByteArray& payload)
|
||||
{
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(payload, &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||
return {};
|
||||
const QString channel = document.object().value(QStringLiteral("result")).toString();
|
||||
return ActiveNetwork::isValidIdentity(channel) ? channel : QString();
|
||||
}
|
||||
|
||||
QString decimalAdd(const QString& left, const QString& right)
|
||||
{
|
||||
if (left.isEmpty() || right.isEmpty())
|
||||
return {};
|
||||
if (!std::all_of(left.cbegin(), left.cend(), [](QChar value) { return value.isDigit(); })
|
||||
|| !std::all_of(right.cbegin(), right.cend(), [](QChar value) { return value.isDigit(); })) {
|
||||
return {};
|
||||
}
|
||||
QString result;
|
||||
result.reserve(std::max(left.size(), right.size()) + 1);
|
||||
qsizetype leftIndex = left.size();
|
||||
qsizetype rightIndex = right.size();
|
||||
int carry = 0;
|
||||
while (leftIndex > 0 || rightIndex > 0 || carry > 0) {
|
||||
const int leftDigit = leftIndex > 0
|
||||
? left.at(--leftIndex).digitValue() : 0;
|
||||
const int rightDigit = rightIndex > 0
|
||||
? right.at(--rightIndex).digitValue() : 0;
|
||||
const int sum = leftDigit + rightDigit + carry;
|
||||
result.prepend(QChar(QLatin1Char('0').unicode() + sum % 10));
|
||||
carry = sum / 10;
|
||||
}
|
||||
while (result.size() > 1 && result.startsWith(QLatin1Char('0')))
|
||||
result.remove(0, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonObject enumFields(const QJsonValue& value, const QString& variant)
|
||||
{
|
||||
return value.toObject().value(variant).toObject();
|
||||
}
|
||||
|
||||
WalletAccountRead accountRead(const WalletAccount& account)
|
||||
{
|
||||
WalletAccountRead read;
|
||||
read.accountId = account.address;
|
||||
read.status = account.readStatus;
|
||||
read.programOwner = account.programOwner;
|
||||
read.dataHex = account.dataHex;
|
||||
return read;
|
||||
}
|
||||
}
|
||||
|
||||
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
: AmmUiBackendSimpleSource(parent),
|
||||
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
|
||||
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
||||
m_walletController(std::make_unique<WalletController>(
|
||||
*m_wallet, QStringLiteral("AmmUI")))
|
||||
*m_wallet, QStringLiteral("AmmUI"))),
|
||||
m_networkManager(new QNetworkAccessManager(this)),
|
||||
m_tokenIdl(resource(QStringLiteral(":/amm/idl/token-idl.json"))),
|
||||
m_ammIdl(resource(QStringLiteral(":/amm/idl/amm-idl.json")))
|
||||
{
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("idle"));
|
||||
setAssetError({});
|
||||
m_network.load();
|
||||
m_idlRegistry.registerProgram(
|
||||
m_network.snapshot().ammProgramId, QStringLiteral("AMM"), m_ammIdl);
|
||||
publishNetworkState();
|
||||
connect(m_walletController.get(), &WalletController::stateChanged,
|
||||
this, &AmmUiBackend::syncWalletState);
|
||||
connect(m_walletController.get(), &WalletController::snapshotChanged,
|
||||
this, &AmmUiBackend::refreshPortfolio);
|
||||
syncWalletState();
|
||||
m_walletController->start();
|
||||
}
|
||||
@@ -71,10 +185,27 @@ void AmmUiBackend::disconnectWallet()
|
||||
m_walletController->disconnect();
|
||||
}
|
||||
|
||||
bool AmmUiBackend::setAccountAlias(QString accountId, QString alias)
|
||||
{
|
||||
return m_walletController->setAccountAlias(accountId, alias);
|
||||
}
|
||||
|
||||
bool AmmUiBackend::setPrimaryAccount(QString accountId)
|
||||
{
|
||||
return m_walletController->setPrimaryAccount(accountId);
|
||||
}
|
||||
|
||||
void AmmUiBackend::syncWalletState()
|
||||
{
|
||||
const WalletUiState& state = m_walletController->state();
|
||||
const QString previousAddress = sequencerAddr();
|
||||
const bool wasReachable = sequencerReachable();
|
||||
setIsWalletOpen(state.isWalletOpen);
|
||||
setWalletStateReady(state.syncStatus != QStringLiteral("opening")
|
||||
&& state.syncStatus != QStringLiteral("syncing"));
|
||||
setWalletSyncStatus(state.syncStatus);
|
||||
setWalletSyncError(state.syncError);
|
||||
setWalletCanSubmit(state.canSubmit());
|
||||
setWalletExists(state.walletExists);
|
||||
setConfigPath(state.configPath);
|
||||
setStoragePath(state.storagePath);
|
||||
@@ -83,4 +214,245 @@ void AmmUiBackend::syncWalletState()
|
||||
setCurrentBlockHeight(state.currentBlockHeight);
|
||||
setSequencerAddr(state.sequencerAddress);
|
||||
setSequencerReachable(state.sequencerReachable);
|
||||
setPrimaryAccountAddress(state.primaryAccountAddress);
|
||||
setPrimaryAccountName(state.primaryAccountName);
|
||||
|
||||
const bool addressChanged = previousAddress != state.sequencerAddress;
|
||||
if (addressChanged)
|
||||
m_network.sequencerChanged(!state.sequencerAddress.isEmpty());
|
||||
if (addressChanged || wasReachable != state.sequencerReachable)
|
||||
m_network.reachabilityChanged(state.sequencerReachable, wasReachable);
|
||||
publishNetworkState();
|
||||
if (state.sequencerReachable && m_network.needsIdentityProbe())
|
||||
probeNetworkIdentity();
|
||||
}
|
||||
|
||||
void AmmUiBackend::publishNetworkState()
|
||||
{
|
||||
const ActiveNetworkSnapshot network = m_network.snapshot();
|
||||
setActiveNetwork(network.id);
|
||||
setNetworkStatus(network.status);
|
||||
setNetworkFingerprint(network.fingerprint);
|
||||
}
|
||||
|
||||
void AmmUiBackend::probeNetworkIdentity()
|
||||
{
|
||||
if (m_identityProbeInFlight || !m_network.isConfigured() || sequencerAddr().isEmpty())
|
||||
return;
|
||||
m_identityProbeInFlight = true;
|
||||
m_network.beginIdentityProbe();
|
||||
publishNetworkState();
|
||||
const QString address = sequencerAddr();
|
||||
const bool devnet = m_network.isDevnet();
|
||||
const QString method = devnet ? QStringLiteral("getChannelId")
|
||||
: QStringLiteral("getBlock");
|
||||
const QJsonArray params = devnet ? QJsonArray()
|
||||
: QJsonArray { CHECKPOINT_BLOCK_ID };
|
||||
QNetworkRequest request{QUrl(address)};
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||
QStringLiteral("application/json"));
|
||||
request.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_networkManager->post(request, jsonRpcBody(method, params));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, address, devnet]() {
|
||||
m_identityProbeInFlight = false;
|
||||
if (address != sequencerAddr()) {
|
||||
reply->deleteLater();
|
||||
probeNetworkIdentity();
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = reply->readAll();
|
||||
const QString identity = devnet ? channelIdFromResponse(payload)
|
||||
: blockHashFromResponse(payload);
|
||||
m_network.finishIdentityProbe(identity);
|
||||
reply->deleteLater();
|
||||
publishNetworkState();
|
||||
refreshPortfolio();
|
||||
});
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshPortfolio()
|
||||
{
|
||||
const quint64 generation = ++m_portfolioGeneration;
|
||||
if (!m_walletController->state().isWalletOpen) {
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("idle"));
|
||||
setAssetError({});
|
||||
return;
|
||||
}
|
||||
if (m_network.status() != QStringLiteral("ready")) {
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("blocked"));
|
||||
setAssetError(m_network.status());
|
||||
return;
|
||||
}
|
||||
if (m_tokenIdl.isEmpty()) {
|
||||
setAssetStatus(QStringLiteral("error"));
|
||||
setAssetError(QStringLiteral("token_idl_missing"));
|
||||
return;
|
||||
}
|
||||
setAssetStatus(QStringLiteral("loading"));
|
||||
setAssetError({});
|
||||
m_wallet->readPublicAccountsAsync(
|
||||
m_network.snapshot().tokenIds,
|
||||
[this, generation](QVector<WalletAccountRead> reads) {
|
||||
applyDefinitions(generation, reads);
|
||||
});
|
||||
}
|
||||
|
||||
void AmmUiBackend::applyDefinitions(
|
||||
quint64 generation,
|
||||
const QVector<WalletAccountRead>& reads)
|
||||
{
|
||||
if (generation != m_portfolioGeneration)
|
||||
return;
|
||||
const ActiveNetworkSnapshot network = m_network.snapshot();
|
||||
const WalletDecodeResult decoded = WalletIdlDecoder::decode(m_tokenIdl, reads);
|
||||
if (!decoded.ok() || reads.size() != network.tokenIds.size()
|
||||
|| decoded.accounts.size() != reads.size()) {
|
||||
setAssetStatus(QStringLiteral("error"));
|
||||
setAssetError(decoded.error.isEmpty()
|
||||
? QStringLiteral("definition_decode_failed")
|
||||
: decoded.error);
|
||||
return;
|
||||
}
|
||||
|
||||
m_tokens.clear();
|
||||
m_tokenProgramId.clear();
|
||||
int unavailable = 0;
|
||||
for (qsizetype index = 0; index < reads.size(); ++index) {
|
||||
const WalletAccountRead& read = reads.at(index);
|
||||
const WalletDecodedAccount& account = decoded.accounts.at(index);
|
||||
TokenInfo token;
|
||||
token.id = network.tokenIds.at(index);
|
||||
token.name = QStringLiteral("Unknown token");
|
||||
token.status = QStringLiteral("unavailable");
|
||||
const QJsonObject fungible = enumFields(account.value, QStringLiteral("Fungible"));
|
||||
if (read.ok() && account.status == QStringLiteral("decoded")
|
||||
&& account.typeName == QStringLiteral("TokenDefinition")
|
||||
&& !fungible.isEmpty() && read.programOwner != DEFAULT_PROGRAM_OWNER) {
|
||||
token.name = fungible.value(QStringLiteral("name")).toString().trimmed();
|
||||
if (token.name.isEmpty())
|
||||
token.name = QStringLiteral("Unnamed token");
|
||||
token.programOwner = read.programOwner;
|
||||
token.status = QStringLiteral("ready");
|
||||
if (m_tokenProgramId.isEmpty())
|
||||
m_tokenProgramId = read.programOwner;
|
||||
else if (m_tokenProgramId != read.programOwner) {
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("error"));
|
||||
setAssetError(QStringLiteral("token_program_mismatch"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
++unavailable;
|
||||
}
|
||||
m_tokens.append(std::move(token));
|
||||
}
|
||||
if (m_tokenProgramId.isEmpty()) {
|
||||
setAssets({});
|
||||
setAssetStatus(QStringLiteral("error"));
|
||||
setAssetError(QStringLiteral("definitions_unavailable"));
|
||||
return;
|
||||
}
|
||||
m_idlRegistry.registerProgram(
|
||||
m_tokenProgramId, QStringLiteral("Token"), m_tokenIdl);
|
||||
setAssetError(unavailable > 0
|
||||
? QStringLiteral("some_definitions_unavailable")
|
||||
: QString());
|
||||
applyWalletPortfolio(generation);
|
||||
}
|
||||
|
||||
void AmmUiBackend::applyWalletPortfolio(quint64 generation)
|
||||
{
|
||||
if (generation != m_portfolioGeneration)
|
||||
return;
|
||||
const WalletSnapshot snapshot = m_walletController->snapshot();
|
||||
QVector<WalletAccountRead> programReads;
|
||||
for (const WalletAccount& account : snapshot.accounts) {
|
||||
if (!account.isPublic || account.readStatus != QStringLiteral("ok"))
|
||||
continue;
|
||||
programReads.append(accountRead(account));
|
||||
}
|
||||
|
||||
QHash<QString, QString> balances;
|
||||
QVector<WalletAccountPresentation> presentations;
|
||||
const QVector<WalletDecodedProgram> programs = m_idlRegistry.decode(programReads);
|
||||
for (const WalletDecodedProgram& program : programs) {
|
||||
for (const WalletDecodedAccount& account : program.result.accounts) {
|
||||
WalletAccountPresentation presentation;
|
||||
presentation.address = account.id;
|
||||
presentation.programName = program.programName;
|
||||
presentation.accountType = account.typeName;
|
||||
if (program.programId == m_tokenProgramId
|
||||
&& account.typeName == QStringLiteral("TokenHolding")) {
|
||||
const QJsonObject fungible = enumFields(
|
||||
account.value, QStringLiteral("Fungible"));
|
||||
if (fungible.isEmpty())
|
||||
continue;
|
||||
const QString encodedId = fungible
|
||||
.value(QStringLiteral("definition_id")).toString();
|
||||
const QString definitionId = account.accountIds.value(encodedId);
|
||||
const QString amount = fungible.value(QStringLiteral("balance")).toString();
|
||||
const QString current = balances.value(definitionId, QStringLiteral("0"));
|
||||
const QString total = decimalAdd(current, amount);
|
||||
if (!definitionId.isEmpty() && !total.isEmpty())
|
||||
balances.insert(definitionId, total);
|
||||
presentation.kind = QStringLiteral("token_holding");
|
||||
presentation.definitionId = definitionId;
|
||||
presentation.hiddenFromAccounts = true;
|
||||
for (const TokenInfo& token : m_tokens) {
|
||||
if (token.id == definitionId) {
|
||||
presentation.semanticName = token.name + QStringLiteral(" holding");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (program.programId == m_tokenProgramId
|
||||
&& account.typeName == QStringLiteral("TokenDefinition")) {
|
||||
presentation.kind = QStringLiteral("token_definition");
|
||||
const QJsonObject fungible = enumFields(
|
||||
account.value, QStringLiteral("Fungible"));
|
||||
presentation.semanticName = fungible.value(QStringLiteral("name")).toString();
|
||||
} else if (program.programId == m_tokenProgramId
|
||||
&& account.typeName == QStringLiteral("TokenMetadata")) {
|
||||
presentation.kind = QStringLiteral("token_metadata");
|
||||
} else {
|
||||
presentation.kind = QStringLiteral("program");
|
||||
presentation.semanticName = account.typeName;
|
||||
}
|
||||
presentations.append(std::move(presentation));
|
||||
}
|
||||
}
|
||||
m_walletController->applyAccountPresentations(presentations);
|
||||
|
||||
QVariantList assets;
|
||||
QVariantList available;
|
||||
int unavailableCount = 0;
|
||||
for (const TokenInfo& token : m_tokens) {
|
||||
const QString balance = balances.value(token.id, QStringLiteral("0"));
|
||||
const bool positive = balance != QStringLiteral("0");
|
||||
QString displayDefinitionId = walletAccountIdToBase58(token.id);
|
||||
if (displayDefinitionId.isEmpty())
|
||||
displayDefinitionId = token.id;
|
||||
QVariantMap asset {
|
||||
{ QStringLiteral("name"), token.name },
|
||||
{ QStringLiteral("symbol"), token.name },
|
||||
{ QStringLiteral("balance"), balance },
|
||||
{ QStringLiteral("definitionId"), token.id },
|
||||
{ QStringLiteral("displayDefinitionId"), displayDefinitionId },
|
||||
{ QStringLiteral("programOwner"), token.programOwner },
|
||||
{ QStringLiteral("status"), token.status },
|
||||
{ QStringLiteral("section"), positive ? QStringLiteral("assets")
|
||||
: QStringLiteral("available") },
|
||||
};
|
||||
if (positive)
|
||||
assets.append(std::move(asset));
|
||||
else
|
||||
available.append(std::move(asset));
|
||||
if (token.status != QStringLiteral("ready"))
|
||||
++unavailableCount;
|
||||
}
|
||||
assets.append(available);
|
||||
setAssets(assets);
|
||||
setAssetStatus(unavailableCount > 0 ? QStringLiteral("partial")
|
||||
: QStringLiteral("ready"));
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
#include "rep_AmmUiBackend_source.h"
|
||||
|
||||
#include "ActiveNetwork.h"
|
||||
#include "WalletAccountModel.h"
|
||||
#include "WalletIdlDecoder.h"
|
||||
|
||||
class LogosAPI;
|
||||
class LogosWalletProvider;
|
||||
class QNetworkAccessManager;
|
||||
class WalletController;
|
||||
|
||||
class AmmUiBackend : public AmmUiBackendSimpleSource {
|
||||
@@ -33,13 +38,37 @@ public slots:
|
||||
QString createNew(QString configPath, QString storagePath, QString password) override;
|
||||
bool openExisting() override;
|
||||
void disconnectWallet() override;
|
||||
bool setAccountAlias(QString accountId, QString alias) override;
|
||||
bool setPrimaryAccount(QString accountId) override;
|
||||
|
||||
private:
|
||||
struct TokenInfo {
|
||||
QString id;
|
||||
QString name;
|
||||
QString programOwner;
|
||||
QString status;
|
||||
};
|
||||
|
||||
void syncWalletState();
|
||||
void publishNetworkState();
|
||||
void probeNetworkIdentity();
|
||||
void refreshPortfolio();
|
||||
void applyDefinitions(quint64 generation,
|
||||
const QVector<WalletAccountRead>& reads);
|
||||
void applyWalletPortfolio(quint64 generation);
|
||||
|
||||
LogosAPI* m_logosAPI;
|
||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||
std::unique_ptr<WalletController> m_walletController;
|
||||
QNetworkAccessManager* m_networkManager;
|
||||
ActiveNetwork m_network;
|
||||
QByteArray m_tokenIdl;
|
||||
QByteArray m_ammIdl;
|
||||
WalletIdlRegistry m_idlRegistry;
|
||||
QVector<TokenInfo> m_tokens;
|
||||
QString m_tokenProgramId;
|
||||
bool m_identityProbeInFlight = false;
|
||||
quint64 m_portfolioGeneration = 0;
|
||||
};
|
||||
|
||||
#endif // AMM_UI_BACKEND_H
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
class AmmUiBackend
|
||||
{
|
||||
PROP(bool isWalletOpen READONLY)
|
||||
PROP(bool walletStateReady READONLY)
|
||||
PROP(QString walletSyncStatus READONLY)
|
||||
PROP(QString walletSyncError READONLY)
|
||||
PROP(bool walletCanSubmit READONLY)
|
||||
PROP(bool walletExists READONLY)
|
||||
PROP(QString configPath READONLY)
|
||||
PROP(QString storagePath READONLY)
|
||||
@@ -15,6 +19,15 @@ class AmmUiBackend
|
||||
// Whether the configured sequencer answered the last reachability probe.
|
||||
// Defaults true so the UI doesn't flash a warning before the first check.
|
||||
PROP(bool sequencerReachable READONLY)
|
||||
PROP(QString primaryAccountAddress READONLY)
|
||||
PROP(QString primaryAccountName READONLY)
|
||||
|
||||
PROP(QString activeNetwork READONLY)
|
||||
PROP(QString networkStatus READONLY)
|
||||
PROP(QString networkFingerprint READONLY)
|
||||
PROP(QVariantList assets READONLY)
|
||||
PROP(QString assetStatus READONLY)
|
||||
PROP(QString assetError READONLY)
|
||||
|
||||
// Account management
|
||||
SLOT(QString createAccountPublic())
|
||||
@@ -22,6 +35,8 @@ class AmmUiBackend
|
||||
SLOT(void refreshAccounts())
|
||||
SLOT(void refreshBalances())
|
||||
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
|
||||
SLOT(bool setAccountAlias(QString accountId, QString alias))
|
||||
SLOT(bool setPrimaryAccount(QString accountId))
|
||||
|
||||
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
|
||||
// fresh per-app wallet at walletHome with no path picking. createNew()
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include "WalletIdlDecoder.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#include <wallet_idl_decoder.h>
|
||||
|
||||
WalletDecodeResult WalletIdlDecoder::decode(
|
||||
const QByteArray& idlJson,
|
||||
const QVector<WalletAccountRead>& accounts)
|
||||
{
|
||||
WalletDecodeResult result;
|
||||
QJsonParseError idlError;
|
||||
const QJsonDocument idl = QJsonDocument::fromJson(idlJson, &idlError);
|
||||
if (idlError.error != QJsonParseError::NoError || !idl.isObject()) {
|
||||
result.status = QStringLiteral("error");
|
||||
result.error = QStringLiteral("invalid_idl");
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonArray inputs;
|
||||
for (const WalletAccountRead& account : accounts) {
|
||||
inputs.append(QJsonObject {
|
||||
{ QStringLiteral("id"), account.accountId },
|
||||
{ QStringLiteral("dataHex"), account.dataHex },
|
||||
});
|
||||
}
|
||||
const QByteArray request = QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("idl"), idl.object() },
|
||||
{ QStringLiteral("accounts"), inputs },
|
||||
}).toJson(QJsonDocument::Compact);
|
||||
|
||||
char* responsePointer = wallet_idl_decode_accounts(request.constData());
|
||||
if (!responsePointer) {
|
||||
result.status = QStringLiteral("error");
|
||||
result.error = QStringLiteral("decoder_unavailable");
|
||||
return result;
|
||||
}
|
||||
const QByteArray response(responsePointer);
|
||||
wallet_idl_decoder_free(responsePointer);
|
||||
|
||||
QJsonParseError responseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response, &responseError);
|
||||
if (responseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
result.status = QStringLiteral("error");
|
||||
result.error = QStringLiteral("invalid_decoder_response");
|
||||
return result;
|
||||
}
|
||||
|
||||
const QJsonObject root = document.object();
|
||||
result.status = root.value(QStringLiteral("status")).toString();
|
||||
result.error = root.value(QStringLiteral("error")).toString();
|
||||
for (const QJsonValue& value : root.value(QStringLiteral("accounts")).toArray()) {
|
||||
const QJsonObject decoded = value.toObject();
|
||||
WalletDecodedAccount account;
|
||||
account.id = decoded.value(QStringLiteral("id")).toString();
|
||||
account.status = decoded.value(QStringLiteral("status")).toString();
|
||||
account.typeName = decoded.value(QStringLiteral("typeName")).toString();
|
||||
account.value = decoded.value(QStringLiteral("value"));
|
||||
const QJsonObject ids = decoded.value(QStringLiteral("accountIds")).toObject();
|
||||
for (auto iterator = ids.begin(); iterator != ids.end(); ++iterator)
|
||||
account.accountIds.insert(iterator.key(), iterator.value().toString());
|
||||
result.accounts.append(std::move(account));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void WalletIdlRegistry::registerProgram(const QString& programId,
|
||||
const QString& programName,
|
||||
const QByteArray& idlJson)
|
||||
{
|
||||
if (!programId.isEmpty() && !programName.isEmpty() && !idlJson.isEmpty())
|
||||
m_programs.insert(programId, { programName, idlJson });
|
||||
}
|
||||
|
||||
QVector<WalletDecodedProgram> WalletIdlRegistry::decode(
|
||||
const QVector<WalletAccountRead>& accounts) const
|
||||
{
|
||||
QHash<QString, QVector<WalletAccountRead>> grouped;
|
||||
for (const WalletAccountRead& account : accounts) {
|
||||
if (m_programs.contains(account.programOwner))
|
||||
grouped[account.programOwner].append(account);
|
||||
}
|
||||
|
||||
QVector<WalletDecodedProgram> decoded;
|
||||
decoded.reserve(grouped.size());
|
||||
for (auto iterator = grouped.cbegin(); iterator != grouped.cend(); ++iterator) {
|
||||
const Program program = m_programs.value(iterator.key());
|
||||
decoded.append({
|
||||
iterator.key(),
|
||||
program.name,
|
||||
WalletIdlDecoder::decode(program.idl, iterator.value()),
|
||||
});
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QJsonValue>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
struct WalletDecodedAccount {
|
||||
QString id;
|
||||
QString status;
|
||||
QString typeName;
|
||||
QJsonValue value;
|
||||
QHash<QString, QString> accountIds;
|
||||
};
|
||||
|
||||
struct WalletDecodeResult {
|
||||
QString status;
|
||||
QString error;
|
||||
QVector<WalletDecodedAccount> accounts;
|
||||
|
||||
bool ok() const { return status == QStringLiteral("ok"); }
|
||||
};
|
||||
|
||||
class WalletIdlDecoder final {
|
||||
public:
|
||||
static WalletDecodeResult decode(const QByteArray& idlJson,
|
||||
const QVector<WalletAccountRead>& accounts);
|
||||
};
|
||||
|
||||
struct WalletDecodedProgram {
|
||||
QString programId;
|
||||
QString programName;
|
||||
WalletDecodeResult result;
|
||||
};
|
||||
|
||||
class WalletIdlRegistry final {
|
||||
public:
|
||||
void registerProgram(const QString& programId,
|
||||
const QString& programName,
|
||||
const QByteArray& idlJson);
|
||||
QVector<WalletDecodedProgram> decode(
|
||||
const QVector<WalletAccountRead>& accounts) const;
|
||||
|
||||
private:
|
||||
struct Program {
|
||||
QString name;
|
||||
QByteArray idl;
|
||||
};
|
||||
|
||||
QHash<QString, Program> m_programs;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "ActiveNetwork.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTemporaryFile>
|
||||
#include <QtTest>
|
||||
|
||||
class ActiveNetworkTest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void validatesIdentityBeforeReadiness();
|
||||
};
|
||||
|
||||
void ActiveNetworkTest::validatesIdentityBeforeReadiness()
|
||||
{
|
||||
const QString identity(64, QLatin1Char('a'));
|
||||
const QString programId(64, QLatin1Char('b'));
|
||||
const QString tokenId(64, QLatin1Char('c'));
|
||||
QTemporaryFile config;
|
||||
QVERIFY(config.open());
|
||||
config.write(QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("channelId"), identity },
|
||||
{ QStringLiteral("ammProgramId"), programId },
|
||||
{ QStringLiteral("tokenDefinitionIds"), QJsonArray { tokenId } },
|
||||
}).toJson(QJsonDocument::Compact));
|
||||
config.flush();
|
||||
qputenv("AMM_UI_NETWORK", "devnet");
|
||||
qputenv("AMM_UI_DEVNET_FILE", config.fileName().toLocal8Bit());
|
||||
|
||||
ActiveNetwork network;
|
||||
QVERIFY(network.load());
|
||||
QCOMPARE(network.status(), QStringLiteral("network_unknown"));
|
||||
network.sequencerChanged(true);
|
||||
network.finishIdentityProbe(QString(64, QLatin1Char('d')));
|
||||
QCOMPARE(network.status(), QStringLiteral("network_mismatch"));
|
||||
network.reachabilityChanged(false, true);
|
||||
network.reachabilityChanged(true, false);
|
||||
network.finishIdentityProbe(identity);
|
||||
QCOMPARE(network.status(), QStringLiteral("ready"));
|
||||
QCOMPARE(network.snapshot().fingerprint, QStringLiteral("channel:") + identity);
|
||||
QCOMPARE(network.snapshot().tokenIds, QStringList { tokenId });
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(ActiveNetworkTest)
|
||||
#include "ActiveNetworkTest.moc"
|
||||
@@ -29,6 +29,8 @@ if(LOGOS_WALLET_BUILD_ACCESS)
|
||||
src/WalletProvider.cpp
|
||||
src/LogosWalletProvider.h
|
||||
src/LogosWalletProvider.cpp
|
||||
src/WalletAccountId.h
|
||||
src/WalletAccountId.cpp
|
||||
src/WalletAccountModel.h
|
||||
src/WalletAccountModel.cpp
|
||||
src/WalletController.h
|
||||
@@ -140,6 +142,8 @@ if(BUILD_TESTING)
|
||||
tests/cpp/LogosWalletProviderTest.cpp
|
||||
src/WalletProvider.cpp
|
||||
src/LogosWalletProvider.cpp
|
||||
src/WalletAccountId.cpp
|
||||
src/WalletAccountId.h
|
||||
src/WalletAccountModel.cpp
|
||||
src/WalletAccountModel.h
|
||||
src/WalletController.cpp
|
||||
|
||||
@@ -12,15 +12,29 @@ Item {
|
||||
property var watchCall: null
|
||||
property bool compact: false
|
||||
property real viewportWidth: width
|
||||
property int selectedIndex: 0
|
||||
property int selectedIndex: -1
|
||||
property bool busy: false
|
||||
property bool openPending: false
|
||||
property bool advancedExpanded: false
|
||||
property bool availableExpanded: false
|
||||
property string postCreationWarning: ""
|
||||
property bool createdWalletAwaitingAcknowledgement: false
|
||||
property string reportedOpenErrorKey: ""
|
||||
|
||||
readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen
|
||||
readonly property bool compactLayout: root.compact || root.viewportWidth < 680
|
||||
readonly property bool walletOpening: root.wallet !== null
|
||||
&& (root.wallet.walletSyncStatus === "opening"
|
||||
|| root.wallet.walletSyncStatus === "syncing")
|
||||
readonly property string selectedAddress: root.accountAt(root.selectedIndex, "address")
|
||||
readonly property string selectedDisplayAddress: root.accountAt(root.selectedIndex, "displayAddress")
|
||||
readonly property string selectedName: root.accountAt(root.selectedIndex, "name")
|
||||
readonly property string selectedBalance: root.accountAt(root.selectedIndex, "balance")
|
||||
readonly property bool selectedIsPublic: root.accountAt(root.selectedIndex, "isPublic") === true
|
||||
readonly property var walletAssets: root.wallet && root.wallet.assets ? root.wallet.assets : []
|
||||
readonly property int availableAssetCount: root.assetCount("available")
|
||||
readonly property string primaryName: root.wallet && root.wallet.primaryAccountName
|
||||
? root.wallet.primaryAccountName : root.selectedName
|
||||
|
||||
implicitWidth: root.connected ? connectedButton.implicitWidth : connectButton.implicitWidth
|
||||
implicitHeight: 40
|
||||
@@ -30,11 +44,15 @@ Item {
|
||||
model: root.accountModel
|
||||
delegate: QtObject {
|
||||
required property string address
|
||||
required property string displayAddress
|
||||
required property string name
|
||||
required property string balance
|
||||
required property bool isPublic
|
||||
required property bool isPrimary
|
||||
required property bool canBePrimary
|
||||
required property string kind
|
||||
}
|
||||
onCountChanged: root.clampSelection()
|
||||
onCountChanged: root.syncPrimarySelection()
|
||||
}
|
||||
|
||||
function accountAt(index, field) {
|
||||
@@ -42,26 +60,50 @@ Item {
|
||||
return entry ? entry[field] : (field === "isPublic" ? false : "")
|
||||
}
|
||||
|
||||
function clampSelection() {
|
||||
if (accounts.count === 0) {
|
||||
root.selectedIndex = 0
|
||||
} else {
|
||||
root.selectedIndex = Math.max(0, Math.min(root.selectedIndex, accounts.count - 1))
|
||||
function assetCount(sectionName) {
|
||||
let count = 0
|
||||
for (const asset of root.walletAssets) {
|
||||
if (asset.section === sectionName)
|
||||
++count
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
function syncPrimarySelection() {
|
||||
if (accounts.count === 0) {
|
||||
root.selectedIndex = -1
|
||||
return
|
||||
}
|
||||
const requested = root.wallet && root.wallet.primaryAccountAddress
|
||||
? root.wallet.primaryAccountAddress : ""
|
||||
for (let index = 0; index < accounts.count; ++index) {
|
||||
const account = accounts.objectAt(index)
|
||||
if ((requested.length > 0 && account.address === requested) || account.isPrimary) {
|
||||
root.selectedIndex = index
|
||||
return
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < accounts.count; ++index) {
|
||||
const account = accounts.objectAt(index)
|
||||
if (account.kind === "user" && account.canBePrimary) {
|
||||
root.selectedIndex = index
|
||||
return
|
||||
}
|
||||
}
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
|
||||
function shortAddress(address) {
|
||||
return address && address.length > 13
|
||||
? address.substring(0, 6) + "..." + address.substring(address.length - 4)
|
||||
? address.substring(0, 6) + "…" + address.substring(address.length - 4)
|
||||
: address || ""
|
||||
}
|
||||
|
||||
function watchResult(result, success, failure) {
|
||||
if (root.watchCall) {
|
||||
if (root.watchCall)
|
||||
root.watchCall(result, success, failure)
|
||||
} else {
|
||||
else
|
||||
success(result)
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
@@ -69,22 +111,108 @@ Item {
|
||||
messageDialog.open()
|
||||
}
|
||||
|
||||
function walletRefreshWarning(subject) {
|
||||
if (!root.wallet || root.wallet.walletSyncStatus !== "error")
|
||||
return ""
|
||||
return qsTr("%1 was created, but could not be refreshed. Reconnect the wallet to refresh it.")
|
||||
.arg(subject)
|
||||
}
|
||||
|
||||
function openFailureKey() {
|
||||
if (!root.wallet || root.wallet.walletSyncStatus !== "error")
|
||||
return ""
|
||||
return root.wallet.walletSyncError || "unknown"
|
||||
}
|
||||
|
||||
function openFailureMessage() {
|
||||
const error = root.wallet ? root.wallet.walletSyncError : ""
|
||||
return error
|
||||
? qsTr("Wallet could not be opened: %1").arg(error)
|
||||
: qsTr("Wallet could not be opened.")
|
||||
}
|
||||
|
||||
function reportUnhandledOpenFailure() {
|
||||
if (!root.wallet || root.openPending || root.wallet.isWalletOpen
|
||||
|| root.wallet.walletSyncStatus !== "error") {
|
||||
return
|
||||
}
|
||||
const key = root.openFailureKey()
|
||||
if (key === root.reportedOpenErrorKey)
|
||||
return
|
||||
root.reportedOpenErrorKey = key
|
||||
root.showError(root.openFailureMessage())
|
||||
}
|
||||
|
||||
function openAccepted(result) {
|
||||
return result === true || result === "true" || result === 1 || result === "1"
|
||||
}
|
||||
|
||||
function finishOpen() {
|
||||
root.openPending = false
|
||||
root.busy = false
|
||||
}
|
||||
|
||||
function failOpen(message) {
|
||||
if (!root.openPending)
|
||||
return
|
||||
root.reportedOpenErrorKey = root.openFailureKey()
|
||||
root.finishOpen()
|
||||
root.showError(message)
|
||||
}
|
||||
|
||||
function settleOpenFromWalletState() {
|
||||
if (!root.openPending || !root.wallet)
|
||||
return
|
||||
const status = root.wallet.walletSyncStatus
|
||||
if (status === undefined) {
|
||||
root.finishOpen()
|
||||
return
|
||||
}
|
||||
if (status === "ready" && root.wallet.isWalletOpen) {
|
||||
root.finishOpen()
|
||||
return
|
||||
}
|
||||
if (status === "error") {
|
||||
root.failOpen(root.openFailureMessage())
|
||||
return
|
||||
}
|
||||
if (status === "closed" && root.wallet.walletExists === false)
|
||||
root.failOpen(qsTr("Wallet could not be opened."))
|
||||
}
|
||||
|
||||
function openWallet() {
|
||||
if (!root.wallet || root.busy)
|
||||
if (!root.wallet || root.busy || root.walletOpening)
|
||||
return
|
||||
root.busy = true
|
||||
root.openPending = true
|
||||
try {
|
||||
root.watchResult(root.wallet.openExisting(), function(ok) {
|
||||
root.busy = false
|
||||
if (!ok)
|
||||
root.showError(qsTr("Wallet could not be opened."))
|
||||
if (!root.openAccepted(ok)) {
|
||||
root.failOpen(qsTr("Wallet could not be opened."))
|
||||
return
|
||||
}
|
||||
Qt.callLater(root.settleOpenFromWalletState)
|
||||
}, function(error) {
|
||||
root.busy = false
|
||||
root.showError(qsTr("Wallet could not be opened: %1").arg(error))
|
||||
root.failOpen(qsTr("Wallet could not be opened: %1").arg(error))
|
||||
})
|
||||
} catch (error) {
|
||||
root.busy = false
|
||||
root.showError(qsTr("Wallet could not be opened: %1").arg(error))
|
||||
root.failOpen(qsTr("Wallet could not be opened: %1").arg(error))
|
||||
}
|
||||
}
|
||||
|
||||
function makePrimary(address) {
|
||||
if (!root.wallet || !address)
|
||||
return
|
||||
try {
|
||||
root.watchResult(root.wallet.setPrimaryAccount(address), function(ok) {
|
||||
if (!ok)
|
||||
root.showError(qsTr("This account cannot be primary."))
|
||||
root.syncPrimarySelection()
|
||||
}, function(error) {
|
||||
root.showError(qsTr("Primary account could not be changed: %1").arg(error))
|
||||
})
|
||||
} catch (error) {
|
||||
root.showError(qsTr("Primary account could not be changed: %1").arg(error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,18 +234,40 @@ Item {
|
||||
Connections {
|
||||
target: root.accountModel
|
||||
ignoreUnknownSignals: true
|
||||
function onModelReset() { root.clampSelection() }
|
||||
function onRowsInserted() { root.clampSelection() }
|
||||
function onRowsRemoved() { root.clampSelection() }
|
||||
function onModelReset() { root.syncPrimarySelection() }
|
||||
function onRowsInserted() { root.syncPrimarySelection() }
|
||||
function onRowsRemoved() { root.syncPrimarySelection() }
|
||||
function onDataChanged() { root.syncPrimarySelection() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wallet
|
||||
ignoreUnknownSignals: true
|
||||
function onPrimaryAccountAddressChanged() { root.syncPrimarySelection() }
|
||||
function onWalletSyncStatusChanged() {
|
||||
if (!root.wallet || root.wallet.walletSyncStatus !== "error")
|
||||
root.reportedOpenErrorKey = ""
|
||||
Qt.callLater(root.settleOpenFromWalletState)
|
||||
Qt.callLater(root.reportUnhandledOpenFailure)
|
||||
}
|
||||
function onWalletSyncErrorChanged() {
|
||||
Qt.callLater(root.settleOpenFromWalletState)
|
||||
Qt.callLater(root.reportUnhandledOpenFailure)
|
||||
}
|
||||
function onIsWalletOpenChanged() { Qt.callLater(root.settleOpenFromWalletState) }
|
||||
function onWalletExistsChanged() { Qt.callLater(root.settleOpenFromWalletState) }
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(root.reportUnhandledOpenFailure)
|
||||
|
||||
onConnectedChanged: {
|
||||
if (!root.connected) {
|
||||
root.selectedIndex = 0
|
||||
root.selectedIndex = -1
|
||||
walletMenu.close()
|
||||
} else {
|
||||
root.syncPrimarySelection()
|
||||
}
|
||||
}
|
||||
|
||||
onViewportWidthChanged: {
|
||||
if (walletMenu.opened)
|
||||
Qt.callLater(walletMenu.updateAnchor)
|
||||
@@ -129,24 +279,20 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !root.connected
|
||||
enabled: root.wallet !== null && !root.busy
|
||||
enabled: root.wallet !== null && !root.busy && !root.walletOpening
|
||||
implicitHeight: 40
|
||||
implicitWidth: root.compactLayout ? 40 : 108
|
||||
text: root.compactLayout ? "" : root.busy ? qsTr("Connecting...") : qsTr("Connect")
|
||||
display: root.compactLayout ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
text: root.compactLayout ? "" : (root.busy || root.walletOpening
|
||||
? qsTr("Connecting…") : qsTr("Connect"))
|
||||
icon.source: Qt.resolvedUrl("icons/account.svg")
|
||||
icon.color: "#ffffff"
|
||||
icon.width: 18
|
||||
icon.height: 18
|
||||
Accessible.name: qsTr("Connect wallet")
|
||||
Accessible.name: qsTr("Connect AMM Wallet")
|
||||
ToolTip.text: Accessible.name
|
||||
ToolTip.visible: hovered && root.compactLayout
|
||||
|
||||
background: Rectangle {
|
||||
color: connectButton.pressed ? "#d95c1e" : "#f26a21"
|
||||
radius: 6
|
||||
color: connectButton.pressed ? "#d97706" : "#f59e0b"
|
||||
radius: 8
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 6
|
||||
Image {
|
||||
@@ -159,12 +305,11 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.compactLayout
|
||||
text: connectButton.text
|
||||
color: "#ffffff"
|
||||
color: "#18181b"
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.wallet && root.wallet.walletExists)
|
||||
root.openWallet()
|
||||
@@ -181,42 +326,43 @@ Item {
|
||||
visible: root.connected
|
||||
enabled: !root.busy
|
||||
implicitHeight: 40
|
||||
implicitWidth: root.compactLayout ? 44 : Math.max(140, accountButtonLabel.implicitWidth + 58)
|
||||
Accessible.name: qsTr("Wallet account %1").arg(root.selectedAddress)
|
||||
implicitWidth: root.compactLayout ? 44 : Math.max(176, accountButtonLabel.implicitWidth + 54)
|
||||
Accessible.name: qsTr("AMM Wallet, primary account %1").arg(root.primaryName)
|
||||
|
||||
background: Rectangle {
|
||||
color: connectedButton.pressed ? "#3f3f46" : "#27272a"
|
||||
border.width: walletMenu.opened || connectedButton.activeFocus ? 1 : 0
|
||||
border.color: "#f26a21"
|
||||
radius: 6
|
||||
border.color: "#f59e0b"
|
||||
radius: 8
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 8
|
||||
Layout.preferredHeight: 8
|
||||
radius: 4
|
||||
color: "#22c55e"
|
||||
color: !root.wallet || root.wallet.networkStatus === undefined
|
||||
|| root.wallet.networkStatus === "ready"
|
||||
? "#22c55e"
|
||||
: root.wallet.networkStatus === "loading" ? "#f59e0b" : "#ef4444"
|
||||
}
|
||||
|
||||
Label {
|
||||
id: accountButtonLabel
|
||||
Layout.fillWidth: true
|
||||
visible: !root.compactLayout
|
||||
text: root.shortAddress(root.selectedAddress) || qsTr("Connected")
|
||||
color: "#f4f4f5"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.primaryName.length > 0
|
||||
? qsTr("AMM Wallet · %1").arg(root.primaryName)
|
||||
: qsTr("AMM Wallet")
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: !root.compactLayout
|
||||
text: walletMenu.opened ? "\u25b4" : "\u25be"
|
||||
text: walletMenu.opened ? "▴" : "▾"
|
||||
color: "#a1a1aa"
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (walletMenu.opened || Date.now() - walletMenu.lastClosedMs < 200)
|
||||
walletMenu.close()
|
||||
@@ -243,10 +389,8 @@ Item {
|
||||
Math.min(connectedButton.width - width,
|
||||
viewport.width - width - 12 - anchorPosition.x))
|
||||
: connectedButton.width - width
|
||||
y: opensAbove
|
||||
? -height - 8
|
||||
: connectedButton.height + 8
|
||||
width: Math.min(360, Math.max(0, Math.min(root.viewportWidth,
|
||||
y: opensAbove ? -height - 8 : connectedButton.height + 8
|
||||
width: Math.min(400, Math.max(0, Math.min(root.viewportWidth,
|
||||
viewport ? viewport.width : root.viewportWidth) - 24))
|
||||
height: Math.min(implicitHeight, availableMenuHeight)
|
||||
margins: 12
|
||||
@@ -257,31 +401,28 @@ Item {
|
||||
if (viewport)
|
||||
anchorPosition = connectedButton.mapToItem(viewport, 0, 0)
|
||||
}
|
||||
|
||||
onAboutToShow: updateAnchor()
|
||||
|
||||
onClosed: {
|
||||
walletMenu.lastClosedMs = Date.now()
|
||||
if (walletStack.depth > 1)
|
||||
walletStack.pop(null, StackView.Immediate)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: walletMenu.opened ? walletMenu.viewport : null
|
||||
|
||||
function onWidthChanged() { Qt.callLater(walletMenu.updateAnchor) }
|
||||
function onHeightChanged() { Qt.callLater(walletMenu.updateAnchor) }
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "#18181b"
|
||||
border.color: "#3f3f46"
|
||||
border.width: 1
|
||||
radius: 8
|
||||
radius: 10
|
||||
}
|
||||
|
||||
contentItem: StackView {
|
||||
id: walletStack
|
||||
objectName: "walletStack"
|
||||
clip: true
|
||||
width: walletMenu.availableWidth
|
||||
height: walletMenu.availableHeight
|
||||
implicitWidth: walletMenu.availableWidth
|
||||
@@ -291,86 +432,214 @@ Item {
|
||||
|
||||
Component {
|
||||
id: walletOverview
|
||||
ScrollView {
|
||||
implicitHeight: Math.min(overviewContent.implicitHeight, 520)
|
||||
contentWidth: availableWidth
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
ColumnLayout {
|
||||
id: overviewContent
|
||||
objectName: "walletOverviewContent"
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
WalletIconButton {
|
||||
objectName: "walletAccountsButton"
|
||||
iconSource: Qt.resolvedUrl("icons/account.svg")
|
||||
accessibleName: qsTr("Accounts")
|
||||
onClicked: walletStack.push(accountList)
|
||||
}
|
||||
|
||||
WalletIconButton {
|
||||
objectName: "walletDisconnectButton"
|
||||
iconSource: Qt.resolvedUrl("icons/power.svg")
|
||||
accessibleName: qsTr("Disconnect")
|
||||
onClicked: {
|
||||
walletMenu.close()
|
||||
if (root.wallet)
|
||||
root.wallet.disconnectWallet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: accountCard.implicitHeight + 24
|
||||
color: "#27272a"
|
||||
radius: 6
|
||||
|
||||
ColumnLayout {
|
||||
id: accountCard
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 1
|
||||
Label {
|
||||
text: root.selectedName || qsTr("Account")
|
||||
color: "#f4f4f5"
|
||||
text: qsTr("AMM Wallet")
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private")
|
||||
text: root.wallet && root.wallet.activeNetwork
|
||||
? root.wallet.activeNetwork : qsTr("Network unavailable")
|
||||
color: "#a1a1aa"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: root.selectedBalance || "-"
|
||||
color: "#f4f4f5"
|
||||
font.bold: true
|
||||
}
|
||||
WalletIconButton {
|
||||
objectName: "walletAccountsButton"
|
||||
iconSource: Qt.resolvedUrl("icons/account.svg")
|
||||
accessibleName: qsTr("Accounts")
|
||||
onClicked: walletStack.push(accountList, StackView.Immediate)
|
||||
}
|
||||
WalletIconButton {
|
||||
objectName: "walletDisconnectButton"
|
||||
iconSource: Qt.resolvedUrl("icons/power.svg")
|
||||
accessibleName: qsTr("Disconnect")
|
||||
onClicked: {
|
||||
walletMenu.close()
|
||||
if (root.wallet)
|
||||
root.wallet.disconnectWallet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: identityCard.implicitHeight + 24
|
||||
color: "#27272a"
|
||||
radius: 8
|
||||
ColumnLayout {
|
||||
id: identityCard
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 7
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
text: root.selectedAddress
|
||||
color: "#a1a1aa"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.primaryName || qsTr("No primary account")
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Primary")
|
||||
color: "#fbbf24"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: root.selectedIsPublic ? qsTr("Public user account")
|
||||
: qsTr("Private account")
|
||||
color: "#a1a1aa"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.selectedDisplayAddress
|
||||
color: "#71717a"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
CopyButton {
|
||||
visible: root.selectedDisplayAddress.length > 0
|
||||
onCopyRequested: root.copyToClipboard(root.selectedDisplayAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CopyButton {
|
||||
visible: root.selectedAddress.length > 0
|
||||
onCopyRequested: root.copyToClipboard(root.selectedAddress)
|
||||
Label {
|
||||
text: qsTr("Assets")
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
visible: root.wallet && root.wallet.assetStatus === "loading"
|
||||
text: qsTr("Loading balances…")
|
||||
color: "#a1a1aa"
|
||||
}
|
||||
Repeater {
|
||||
model: root.walletAssets
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
visible: modelData.section === "assets"
|
||||
implicitHeight: visible ? 62 : 0
|
||||
color: "#27272a"
|
||||
radius: 8
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.shortAddress(modelData.displayDefinitionId
|
||||
|| modelData.definitionId)
|
||||
color: "#71717a"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: modelData.balance
|
||||
color: "#fafafa"
|
||||
font.family: "monospace"
|
||||
font.bold: true
|
||||
}
|
||||
CopyButton {
|
||||
onCopyRequested: root.copyToClipboard(
|
||||
modelData.displayDefinitionId || modelData.definitionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
visible: (!root.walletAssets || root.walletAssets.length === 0)
|
||||
&& (!root.wallet || root.wallet.assetStatus !== "loading")
|
||||
text: root.wallet && root.wallet.assetError
|
||||
? qsTr("Assets unavailable: %1").arg(root.wallet.assetError)
|
||||
: qsTr("No assets yet")
|
||||
color: "#a1a1aa"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Button {
|
||||
objectName: "walletAvailableAssetsButton"
|
||||
Layout.fillWidth: true
|
||||
visible: root.availableAssetCount > 0
|
||||
text: root.availableExpanded ? qsTr("Hide available tokens")
|
||||
: qsTr("Available tokens")
|
||||
flat: true
|
||||
onClicked: root.availableExpanded = !root.availableExpanded
|
||||
}
|
||||
Repeater {
|
||||
model: root.walletAssets
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
visible: root.availableExpanded && modelData.section === "available"
|
||||
implicitHeight: visible ? 58 : 0
|
||||
color: "#202023"
|
||||
radius: 8
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name
|
||||
color: modelData.status === "ready" ? "#d4d4d8" : "#a1a1aa"
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Label {
|
||||
text: root.shortAddress(modelData.displayDefinitionId
|
||||
|| modelData.definitionId)
|
||||
color: "#71717a"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: modelData.status === "ready" ? "0" : qsTr("Unavailable")
|
||||
color: "#71717a"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
CopyButton {
|
||||
onCopyRequested: root.copyToClipboard(
|
||||
modelData.displayDefinitionId || modelData.definitionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,77 +649,173 @@ Item {
|
||||
|
||||
Component {
|
||||
id: accountList
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
spacing: 10
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
WalletIconButton {
|
||||
objectName: "walletAccountsBackButton"
|
||||
iconSource: Qt.resolvedUrl("icons/back.svg")
|
||||
accessibleName: qsTr("Back")
|
||||
onClicked: walletStack.pop()
|
||||
onClicked: walletStack.pop(null, StackView.Immediate)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Accounts")
|
||||
color: "#f4f4f5"
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
visible: walletMenu.availableMenuHeight >= 280
|
||||
text: qsTr("Choose the account used as your wallet identity. Program records stay under Advanced.")
|
||||
color: "#a1a1aa"
|
||||
font.pixelSize: 11
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
ListView {
|
||||
id: accountListView
|
||||
objectName: "walletAccountList"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 0
|
||||
Layout.preferredHeight: Math.min(contentHeight, 260)
|
||||
Layout.minimumHeight: 48
|
||||
Layout.preferredHeight: Math.min(contentHeight, 300)
|
||||
clip: true
|
||||
spacing: 6
|
||||
spacing: 0
|
||||
model: root.accountModel
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
delegate: Item {
|
||||
id: accountWrapper
|
||||
required property int index
|
||||
required property string name
|
||||
required property string alias
|
||||
required property string address
|
||||
required property string displayAddress
|
||||
required property string balance
|
||||
required property bool isPublic
|
||||
required property string kind
|
||||
required property string section
|
||||
required property string programName
|
||||
required property string accountType
|
||||
required property string visibility
|
||||
required property bool canBePrimary
|
||||
required property bool isPrimary
|
||||
|
||||
delegate: AccountDelegate {
|
||||
readonly property bool shown: section === "accounts"
|
||||
|| (root.advancedExpanded && section === "advanced")
|
||||
width: ListView.view.width
|
||||
highlighted: index === root.selectedIndex
|
||||
onClicked: {
|
||||
root.selectedIndex = index
|
||||
walletStack.pop()
|
||||
height: shown ? accountDelegate.implicitHeight + 6 : 0
|
||||
visible: shown
|
||||
|
||||
function clicked() {
|
||||
if (canBePrimary && !isPrimary)
|
||||
root.makePrimary(address)
|
||||
}
|
||||
|
||||
AccountDelegate {
|
||||
id: accountDelegate
|
||||
width: parent.width
|
||||
index: accountWrapper.index
|
||||
name: accountWrapper.name
|
||||
alias: accountWrapper.alias
|
||||
address: accountWrapper.address
|
||||
displayAddress: accountWrapper.displayAddress
|
||||
balance: accountWrapper.balance
|
||||
isPublic: accountWrapper.isPublic
|
||||
kind: accountWrapper.kind
|
||||
section: accountWrapper.section
|
||||
programName: accountWrapper.programName
|
||||
accountType: accountWrapper.accountType
|
||||
visibility: accountWrapper.visibility
|
||||
canBePrimary: accountWrapper.canBePrimary
|
||||
isPrimary: accountWrapper.isPrimary
|
||||
onMakePrimaryRequested: function(address) { root.makePrimary(address) }
|
||||
onRenameRequested: function(address, alias) {
|
||||
renameDialog.accountAddress = address
|
||||
renameField.text = alias
|
||||
renameDialog.open()
|
||||
}
|
||||
onCopyRequested: function(text) { root.copyToClipboard(text) }
|
||||
}
|
||||
onCopyRequested: function(text) { root.copyToClipboard(text) }
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
objectName: "walletAddAccountButton"
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Add account")
|
||||
enabled: !root.busy
|
||||
onClicked: createAccountDialog.open()
|
||||
spacing: 6
|
||||
Button {
|
||||
objectName: "walletAdvancedAccountsButton"
|
||||
Layout.fillWidth: true
|
||||
text: root.advancedExpanded ? qsTr("Hide Advanced") : qsTr("Advanced")
|
||||
flat: true
|
||||
onClicked: root.advancedExpanded = !root.advancedExpanded
|
||||
}
|
||||
Button {
|
||||
objectName: "walletAddAccountButton"
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Add account")
|
||||
enabled: !root.busy
|
||||
onClicked: createAccountDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: renameDialog
|
||||
objectName: "walletRenameDialog"
|
||||
property string accountAddress: ""
|
||||
parent: Overlay.overlay
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(360, parent ? parent.width - 32 : 360)
|
||||
title: qsTr("Rename account")
|
||||
standardButtons: Dialog.Save | Dialog.Cancel
|
||||
TextField {
|
||||
id: renameField
|
||||
objectName: "walletAliasField"
|
||||
width: parent.width
|
||||
maximumLength: 40
|
||||
placeholderText: qsTr("Account name")
|
||||
Accessible.name: qsTr("Account name")
|
||||
}
|
||||
onAccepted: {
|
||||
if (!root.wallet)
|
||||
return
|
||||
try {
|
||||
root.watchResult(root.wallet.setAccountAlias(accountAddress, renameField.text),
|
||||
function(ok) {
|
||||
if (!ok)
|
||||
root.showError(qsTr("Account name could not be saved."))
|
||||
}, function(error) {
|
||||
root.showError(qsTr("Account name could not be saved: %1").arg(error))
|
||||
})
|
||||
} catch (error) {
|
||||
root.showError(qsTr("Account name could not be saved: %1").arg(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CreateWalletDialog {
|
||||
id: createWalletDialog
|
||||
objectName: "createWalletDialog"
|
||||
walletHome: root.wallet ? root.wallet.walletHome || "" : ""
|
||||
busy: root.busy
|
||||
|
||||
onCreateRequested: function(password) {
|
||||
if (!root.wallet || root.busy)
|
||||
return
|
||||
root.postCreationWarning = ""
|
||||
root.createdWalletAwaitingAcknowledgement = false
|
||||
root.busy = true
|
||||
try {
|
||||
root.watchResult(root.wallet.createNewDefault(password), function(mnemonic) {
|
||||
root.busy = false
|
||||
if (mnemonic && mnemonic.length > 0)
|
||||
if (mnemonic && mnemonic.length > 0) {
|
||||
createWalletDialog.mnemonic = mnemonic
|
||||
else
|
||||
root.createdWalletAwaitingAcknowledgement = true
|
||||
root.postCreationWarning = root.walletRefreshWarning(qsTr("Wallet"))
|
||||
} else
|
||||
createWalletDialog.errorText = qsTr("Wallet could not be created.")
|
||||
}, function(error) {
|
||||
root.busy = false
|
||||
@@ -461,30 +826,40 @@ Item {
|
||||
createWalletDialog.errorText = qsTr("Wallet could not be created: %1").arg(error)
|
||||
}
|
||||
}
|
||||
|
||||
onCopyRequested: function(text) { root.copyToClipboard(text) }
|
||||
onClosed: {
|
||||
const warning = root.postCreationWarning.length > 0 ? root.postCreationWarning
|
||||
: root.createdWalletAwaitingAcknowledgement
|
||||
? root.walletRefreshWarning(qsTr("Wallet")) : ""
|
||||
root.postCreationWarning = ""
|
||||
root.createdWalletAwaitingAcknowledgement = false
|
||||
if (warning.length > 0)
|
||||
root.showError(warning)
|
||||
}
|
||||
}
|
||||
|
||||
CreateAccountDialog {
|
||||
id: createAccountDialog
|
||||
objectName: "createAccountDialog"
|
||||
busy: root.busy
|
||||
|
||||
onCreateRequested: function(isPublic) {
|
||||
if (!root.wallet || root.busy)
|
||||
return
|
||||
root.busy = true
|
||||
try {
|
||||
const request = isPublic
|
||||
? root.wallet.createAccountPublic()
|
||||
: root.wallet.createAccountPrivate()
|
||||
const request = isPublic ? root.wallet.createAccountPublic()
|
||||
: root.wallet.createAccountPrivate()
|
||||
root.watchResult(request, function(accountId) {
|
||||
root.busy = false
|
||||
if (accountId && accountId.length > 0) {
|
||||
createAccountDialog.close()
|
||||
} else {
|
||||
Qt.callLater(function() {
|
||||
const warning = root.walletRefreshWarning(qsTr("Account"))
|
||||
if (warning.length > 0)
|
||||
root.showError(warning)
|
||||
})
|
||||
} else
|
||||
root.showError(qsTr("Account could not be created."))
|
||||
}
|
||||
}, function(error) {
|
||||
root.busy = false
|
||||
root.showError(qsTr("Account could not be created: %1").arg(error))
|
||||
|
||||
@@ -7,71 +7,136 @@ ItemDelegate {
|
||||
|
||||
required property int index
|
||||
required property string name
|
||||
required property string alias
|
||||
required property string address
|
||||
required property string displayAddress
|
||||
required property string balance
|
||||
required property bool isPublic
|
||||
required property string kind
|
||||
required property string section
|
||||
required property string programName
|
||||
required property string accountType
|
||||
required property string visibility
|
||||
required property bool canBePrimary
|
||||
required property bool isPrimary
|
||||
|
||||
signal copyRequested(string text)
|
||||
signal makePrimaryRequested(string address)
|
||||
signal renameRequested(string address, string alias)
|
||||
|
||||
leftPadding: 12
|
||||
rightPadding: 8
|
||||
topPadding: 10
|
||||
bottomPadding: 10
|
||||
enabled: root.section !== "hidden"
|
||||
Accessible.name: root.isPrimary
|
||||
? qsTr("%1, primary account").arg(root.name)
|
||||
: root.name
|
||||
|
||||
Accessible.name: qsTr("%1, balance %2").arg(root.name).arg(root.balance || "0")
|
||||
function kindLabel() {
|
||||
if (root.kind === "user")
|
||||
return qsTr("User")
|
||||
if (root.kind === "private")
|
||||
return qsTr("Account")
|
||||
if (root.accountType.length > 0)
|
||||
return root.accountType
|
||||
return root.kind === "unknown" ? qsTr("Unknown") : qsTr("Program")
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: root.highlighted || root.hovered ? "#27272a" : "#18181b"
|
||||
radius: 6
|
||||
border.width: root.activeFocus ? 1 : 0
|
||||
border.color: "#f26a21"
|
||||
color: root.isPrimary || root.hovered ? "#27272a" : "#18181b"
|
||||
radius: 8
|
||||
border.width: root.activeFocus || root.isPrimary ? 1 : 0
|
||||
border.color: root.isPrimary ? "#f59e0b" : "#52525b"
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 6
|
||||
spacing: 7
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
spacing: 7
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.name
|
||||
color: "#f4f4f5"
|
||||
color: "#fafafa"
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: root.isPrimary
|
||||
text: qsTr("Primary")
|
||||
color: "#fbbf24"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.isPublic ? qsTr("Public") : qsTr("Private")
|
||||
text: root.kindLabel()
|
||||
color: "#a1a1aa"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: root.balance.length > 0 ? root.balance : "-"
|
||||
color: "#f4f4f5"
|
||||
font.bold: true
|
||||
text: root.visibility === "private" ? qsTr("Private") : qsTr("Public")
|
||||
color: root.visibility === "private" ? "#c4b5fd" : "#93c5fd"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: root.programName.length > 0
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("%1 program · wallet controlled").arg(root.programName)
|
||||
color: "#a1a1aa"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.address
|
||||
color: "#a1a1aa"
|
||||
text: root.displayAddress
|
||||
color: "#71717a"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
CopyButton {
|
||||
visible: root.address.length > 0
|
||||
onCopyRequested: root.copyRequested(root.address)
|
||||
visible: root.displayAddress.length > 0
|
||||
onCopyRequested: root.copyRequested(root.displayAddress)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Button {
|
||||
text: qsTr("Rename")
|
||||
flat: true
|
||||
onClicked: root.renameRequested(root.address, root.alias)
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
visible: root.canBePrimary && !root.isPrimary
|
||||
text: qsTr("Make primary")
|
||||
flat: true
|
||||
onClicked: root.makePrimaryRequested(root.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.canBePrimary && !root.isPrimary)
|
||||
root.makePrimaryRequested(root.address)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QTimer>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
@@ -73,6 +74,46 @@ WalletCreation failedCreation(WalletFailure failure)
|
||||
creation.snapshot.failure = failure;
|
||||
return creation;
|
||||
}
|
||||
|
||||
WalletAccountRead parsePublicAccount(const QString& accountId, const QString& payload)
|
||||
{
|
||||
WalletAccountRead read;
|
||||
read.accountId = accountId;
|
||||
if (!isHex(accountId, 64))
|
||||
return read;
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(payload.toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
return read;
|
||||
|
||||
const QJsonObject account = document.object();
|
||||
const QString owner = account.value(QStringLiteral("program_owner")).toString();
|
||||
const QString balance = account.value(QStringLiteral("balance")).toString();
|
||||
const QString nonce = account.value(QStringLiteral("nonce")).toString();
|
||||
const QString data = account.value(QStringLiteral("data")).toString();
|
||||
if (!isHex(owner, 64)
|
||||
|| !isHex(balance, 32)
|
||||
|| !isHex(nonce, 32)
|
||||
|| data.size() % 2 != 0
|
||||
|| !isHex(data, data.size())) {
|
||||
return read;
|
||||
}
|
||||
|
||||
read.status = QStringLiteral("ok");
|
||||
read.programOwner = owner;
|
||||
read.balanceHex = balance;
|
||||
read.nonceHex = nonce;
|
||||
read.dataHex = data;
|
||||
return read;
|
||||
}
|
||||
|
||||
void applyPublicRead(WalletAccount& account, const WalletAccountRead& read)
|
||||
{
|
||||
account.readStatus = read.status;
|
||||
account.programOwner = read.programOwner;
|
||||
account.dataHex = read.dataHex;
|
||||
}
|
||||
}
|
||||
|
||||
struct LogosWalletProvider::Impl {
|
||||
@@ -130,6 +171,76 @@ WalletSession LogosWalletProvider::connect(const WalletPaths& paths)
|
||||
return session;
|
||||
}
|
||||
|
||||
void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback callback)
|
||||
{
|
||||
clearSnapshot();
|
||||
const quint64 generation = ++m_generation;
|
||||
if (!m_impl->logos) {
|
||||
QTimer::singleShot(0, [callback = std::move(callback)]() mutable {
|
||||
callback(failedSession(WalletFailure::WalletUnavailable));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto finishOpen = [this, generation, callback = std::move(callback)](
|
||||
bool adopted, WalletFailure failure) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
if (failure != WalletFailure::None) {
|
||||
callback(failedSession(failure));
|
||||
return;
|
||||
}
|
||||
m_connected = true;
|
||||
loadSnapshotAsync(generation,
|
||||
[this, generation, adopted, callback = std::move(callback)](
|
||||
WalletSnapshot snapshot) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
WalletSession session;
|
||||
session.adopted = adopted;
|
||||
session.failure = snapshot.failure;
|
||||
session.snapshot = std::move(snapshot);
|
||||
callback(std::move(session));
|
||||
});
|
||||
};
|
||||
|
||||
auto openStored = [this, generation, paths, finishOpen]() mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
if (!QFileInfo::exists(paths.storage)) {
|
||||
finishOpen(false, WalletFailure::WalletMissing);
|
||||
return;
|
||||
}
|
||||
m_impl->logos->logos_execution_zone.openAsync(
|
||||
paths.config, paths.storage,
|
||||
[this, generation, finishOpen](int result) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
finishOpen(false, result == WALLET_FFI_SUCCESS
|
||||
? WalletFailure::None : WalletFailure::OpenFailed);
|
||||
});
|
||||
};
|
||||
|
||||
m_impl->logos->logos_execution_zone.get_sequencer_addrAsync(
|
||||
[this, generation, finishOpen, openStored](QString address) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
if (!address.isEmpty()) {
|
||||
finishOpen(true, WalletFailure::None);
|
||||
return;
|
||||
}
|
||||
m_impl->logos->logos_execution_zone.list_accountsAsync(
|
||||
[this, generation, finishOpen, openStored](QVariantList accounts) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
if (!accounts.isEmpty())
|
||||
finishOpen(true, WalletFailure::None);
|
||||
else
|
||||
openStored();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths,
|
||||
const QString& password)
|
||||
{
|
||||
@@ -180,6 +291,26 @@ WalletSnapshot LogosWalletProvider::snapshot(bool forceRefresh)
|
||||
return result;
|
||||
}
|
||||
|
||||
void LogosWalletProvider::snapshotAsync(bool forceRefresh, SnapshotCallback callback)
|
||||
{
|
||||
if (m_snapshotReady && !forceRefresh) {
|
||||
const WalletSnapshot snapshot = m_snapshot;
|
||||
QTimer::singleShot(0, [callback = std::move(callback), snapshot]() mutable {
|
||||
callback(snapshot);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!m_connected) {
|
||||
WalletSnapshot snapshot;
|
||||
snapshot.failure = WalletFailure::WalletUnavailable;
|
||||
QTimer::singleShot(0, [callback = std::move(callback), snapshot]() mutable {
|
||||
callback(snapshot);
|
||||
});
|
||||
return;
|
||||
}
|
||||
loadSnapshotAsync(++m_generation, std::move(callback));
|
||||
}
|
||||
|
||||
void LogosWalletProvider::clearSnapshot()
|
||||
{
|
||||
m_snapshot = {};
|
||||
@@ -208,45 +339,57 @@ WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic)
|
||||
|
||||
if (isPublic)
|
||||
creation.publicAccount = readPublicAccount(creation.accountId);
|
||||
|
||||
clearSnapshot();
|
||||
creation.snapshot = snapshot(true);
|
||||
return creation;
|
||||
}
|
||||
|
||||
WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const
|
||||
{
|
||||
WalletAccountRead read;
|
||||
read.accountId = accountId;
|
||||
if (!m_impl->logos || !isHex(accountId, 64))
|
||||
return read;
|
||||
return WalletAccountRead { accountId };
|
||||
return parsePublicAccount(
|
||||
accountId,
|
||||
m_impl->logos->logos_execution_zone.get_account_public(accountId));
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(
|
||||
m_impl->logos->logos_execution_zone.get_account_public(accountId).toUtf8(),
|
||||
&parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
return read;
|
||||
|
||||
const QJsonObject account = document.object();
|
||||
const QString owner = account.value(QStringLiteral("program_owner")).toString();
|
||||
const QString balance = account.value(QStringLiteral("balance")).toString();
|
||||
const QString nonce = account.value(QStringLiteral("nonce")).toString();
|
||||
const QString data = account.value(QStringLiteral("data")).toString();
|
||||
if (!isHex(owner, 64)
|
||||
|| !isHex(balance, 32)
|
||||
|| !isHex(nonce, 32)
|
||||
|| data.size() % 2 != 0
|
||||
|| !isHex(data, data.size())) {
|
||||
return read;
|
||||
void LogosWalletProvider::readPublicAccountsAsync(
|
||||
const QStringList& accountIds,
|
||||
AccountReadsCallback callback)
|
||||
{
|
||||
if (!m_impl->logos || accountIds.isEmpty()) {
|
||||
QTimer::singleShot(0,
|
||||
[callback = std::move(callback)]() mutable { callback({}); });
|
||||
return;
|
||||
}
|
||||
|
||||
read.status = QStringLiteral("ok");
|
||||
read.programOwner = owner;
|
||||
read.balanceHex = balance;
|
||||
read.nonceHex = nonce;
|
||||
read.dataHex = data;
|
||||
return read;
|
||||
struct BatchState {
|
||||
QVector<WalletAccountRead> reads;
|
||||
qsizetype remaining = 0;
|
||||
AccountReadsCallback callback;
|
||||
};
|
||||
const quint64 generation = m_generation;
|
||||
auto state = std::make_shared<BatchState>();
|
||||
state->reads.resize(accountIds.size());
|
||||
state->remaining = accountIds.size();
|
||||
state->callback = std::move(callback);
|
||||
for (qsizetype index = 0; index < accountIds.size(); ++index) {
|
||||
const QString accountId = accountIds.at(index);
|
||||
if (!isHex(accountId, 64)) {
|
||||
state->reads[index] = WalletAccountRead { accountId };
|
||||
if (--state->remaining == 0)
|
||||
state->callback(std::move(state->reads));
|
||||
continue;
|
||||
}
|
||||
m_impl->logos->logos_execution_zone.get_account_publicAsync(
|
||||
accountId,
|
||||
[this, generation, state, index, accountId](QString payload) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
state->reads[index] = parsePublicAccount(accountId, payload);
|
||||
if (--state->remaining == 0)
|
||||
state->callback(std::move(state->reads));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
WalletSubmission LogosWalletProvider::submitPublicTransaction(
|
||||
@@ -314,6 +457,7 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction(
|
||||
|
||||
void LogosWalletProvider::disconnect()
|
||||
{
|
||||
++m_generation;
|
||||
if (m_connected)
|
||||
save();
|
||||
clearSnapshot();
|
||||
@@ -361,20 +505,181 @@ WalletSnapshot LogosWalletProvider::loadSnapshot()
|
||||
if (account.isPublic) {
|
||||
const WalletAccountRead read = readPublicAccount(address);
|
||||
result.publicAccountReads.append(read);
|
||||
applyPublicRead(account, read);
|
||||
account.balance = read.ok()
|
||||
? littleEndianU128ToDecimal(read.balanceHex)
|
||||
: m_impl->logos->logos_execution_zone.get_balance(address, true);
|
||||
} else {
|
||||
account.readStatus = QStringLiteral("private");
|
||||
account.balance = m_impl->logos->logos_execution_zone.get_balance(address, false);
|
||||
}
|
||||
result.accounts.append(account);
|
||||
}
|
||||
|
||||
if (!save())
|
||||
result.failure = WalletFailure::SaveFailed;
|
||||
return result;
|
||||
}
|
||||
|
||||
void LogosWalletProvider::loadSnapshotAsync(quint64 generation, SnapshotCallback callback)
|
||||
{
|
||||
if (!m_impl->logos || generation != m_generation)
|
||||
return;
|
||||
|
||||
m_impl->logos->logos_execution_zone.get_current_block_heightAsync(
|
||||
[this, generation, callback = std::move(callback)](int currentHeight) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
|
||||
auto afterSync = [this, generation, currentHeight,
|
||||
callback = std::move(callback)](int syncResult) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
if (syncResult != WALLET_FFI_SUCCESS) {
|
||||
WalletSnapshot failed;
|
||||
failed.failure = WalletFailure::ReadFailed;
|
||||
callback(std::move(failed));
|
||||
return;
|
||||
}
|
||||
|
||||
m_impl->logos->logos_execution_zone.get_last_synced_blockAsync(
|
||||
[this, generation, currentHeight,
|
||||
callback = std::move(callback)](int lastSynced) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
m_impl->logos->logos_execution_zone.get_sequencer_addrAsync(
|
||||
[this, generation, currentHeight, lastSynced,
|
||||
callback = std::move(callback)](QString address) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
m_impl->logos->logos_execution_zone.list_accountsAsync(
|
||||
[this, generation, currentHeight, lastSynced,
|
||||
address = std::move(address),
|
||||
callback = std::move(callback)](
|
||||
QVariantList entries) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
|
||||
struct SnapshotState {
|
||||
WalletSnapshot snapshot;
|
||||
QVector<WalletAccountRead> publicReads;
|
||||
QVector<bool> publicFlags;
|
||||
qsizetype remaining = 0;
|
||||
SnapshotCallback callback;
|
||||
};
|
||||
auto state = std::make_shared<SnapshotState>();
|
||||
state->snapshot.currentBlockHeight = static_cast<quint64>(
|
||||
qMax(0, currentHeight));
|
||||
state->snapshot.lastSyncedBlock = static_cast<quint64>(
|
||||
qMax(0, lastSynced));
|
||||
state->snapshot.sequencerAddress = std::move(address);
|
||||
state->snapshot.accounts.resize(entries.size());
|
||||
state->publicReads.resize(entries.size());
|
||||
state->publicFlags.resize(entries.size());
|
||||
state->remaining = entries.size();
|
||||
state->callback = std::move(callback);
|
||||
|
||||
for (qsizetype index = 0; index < entries.size(); ++index) {
|
||||
const QVariantMap entry = entries.at(index).toMap();
|
||||
const QString accountId = entry
|
||||
.value(QStringLiteral("account_id")).toString();
|
||||
if (entry.isEmpty() || !isHex(accountId, 64)) {
|
||||
state->snapshot.failure = WalletFailure::ReadFailed;
|
||||
state->callback(std::move(state->snapshot));
|
||||
return;
|
||||
}
|
||||
state->snapshot.accounts[index] = WalletAccount {
|
||||
accountId,
|
||||
{},
|
||||
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 {
|
||||
if (generation != m_generation || --state->remaining > 0)
|
||||
return;
|
||||
for (qsizetype index = 0;
|
||||
index < state->publicReads.size(); ++index) {
|
||||
if (state->publicFlags.at(index))
|
||||
state->snapshot.publicAccountReads.append(
|
||||
state->publicReads.at(index));
|
||||
}
|
||||
m_impl->logos->logos_execution_zone.saveAsync(
|
||||
[this, generation, state](int result) mutable {
|
||||
if (generation != m_generation)
|
||||
return;
|
||||
if (result != WALLET_FFI_SUCCESS)
|
||||
state->snapshot.failure = WalletFailure::SaveFailed;
|
||||
if (state->snapshot.ok()) {
|
||||
m_snapshot = state->snapshot;
|
||||
m_snapshotReady = true;
|
||||
}
|
||||
state->callback(std::move(state->snapshot));
|
||||
});
|
||||
};
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
state->remaining = 1;
|
||||
(*finishOne)();
|
||||
return;
|
||||
}
|
||||
|
||||
for (qsizetype index = 0; index < entries.size(); ++index) {
|
||||
const WalletAccount account = state->snapshot.accounts.at(index);
|
||||
if (!account.isPublic) {
|
||||
m_impl->logos->logos_execution_zone.get_balanceAsync(
|
||||
account.address, false,
|
||||
[state, finishOne, index](QString balance) {
|
||||
state->snapshot.accounts[index].balance =
|
||||
std::move(balance);
|
||||
(*finishOne)();
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
m_impl->logos->logos_execution_zone.get_account_publicAsync(
|
||||
account.address,
|
||||
[this, state, finishOne, index,
|
||||
accountId = account.address](QString payload) {
|
||||
const WalletAccountRead read =
|
||||
parsePublicAccount(accountId, payload);
|
||||
state->publicReads[index] = read;
|
||||
applyPublicRead(
|
||||
state->snapshot.accounts[index], read);
|
||||
if (read.ok()) {
|
||||
state->snapshot.accounts[index].balance =
|
||||
littleEndianU128ToDecimal(read.balanceHex);
|
||||
(*finishOne)();
|
||||
return;
|
||||
}
|
||||
m_impl->logos->logos_execution_zone.get_balanceAsync(
|
||||
accountId, true,
|
||||
[state, finishOne, index](QString balance) {
|
||||
state->snapshot.accounts[index].balance =
|
||||
std::move(balance);
|
||||
(*finishOne)();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (currentHeight > 0) {
|
||||
m_impl->logos->logos_execution_zone.sync_to_blockAsync(
|
||||
currentHeight, std::move(afterSync));
|
||||
} else {
|
||||
afterSync(WALLET_FFI_SUCCESS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool LogosWalletProvider::save() const
|
||||
{
|
||||
return m_impl->logos
|
||||
|
||||
@@ -14,12 +14,16 @@ public:
|
||||
~LogosWalletProvider() override;
|
||||
|
||||
WalletSession connect(const WalletPaths& paths) override;
|
||||
void connectAsync(const WalletPaths& paths, SessionCallback callback) override;
|
||||
WalletCreation createWallet(const WalletPaths& paths,
|
||||
const QString& password) override;
|
||||
WalletSnapshot snapshot(bool forceRefresh = false) override;
|
||||
void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override;
|
||||
void clearSnapshot() override;
|
||||
WalletAccountCreation createAccount(bool isPublic) override;
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override;
|
||||
void readPublicAccountsAsync(const QStringList& accountIds,
|
||||
AccountReadsCallback callback) override;
|
||||
WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) override;
|
||||
void disconnect() override;
|
||||
@@ -27,6 +31,7 @@ public:
|
||||
private:
|
||||
bool sharedWalletIsOpen() const;
|
||||
WalletSnapshot loadSnapshot();
|
||||
void loadSnapshotAsync(quint64 generation, SnapshotCallback callback);
|
||||
bool save() const;
|
||||
|
||||
struct Impl;
|
||||
@@ -34,4 +39,5 @@ private:
|
||||
WalletSnapshot m_snapshot;
|
||||
bool m_snapshotReady = false;
|
||||
bool m_connected = false;
|
||||
quint64 m_generation = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "WalletAccountId.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QVector>
|
||||
|
||||
namespace {
|
||||
constexpr char BASE58_ALPHABET[] =
|
||||
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
bool isHexCharacter(QChar character)
|
||||
{
|
||||
const ushort value = character.unicode();
|
||||
return (value >= '0' && value <= '9')
|
||||
|| (value >= 'a' && value <= 'f')
|
||||
|| (value >= 'A' && value <= 'F');
|
||||
}
|
||||
}
|
||||
|
||||
QString walletAccountIdToBase58(const QString& accountId)
|
||||
{
|
||||
if (accountId.size() != 64)
|
||||
return {};
|
||||
for (const QChar character : accountId) {
|
||||
if (!isHexCharacter(character))
|
||||
return {};
|
||||
}
|
||||
|
||||
const QByteArray bytes = QByteArray::fromHex(accountId.toLatin1());
|
||||
if (bytes.size() != 32)
|
||||
return {};
|
||||
|
||||
qsizetype leadingZeroes = 0;
|
||||
while (leadingZeroes < bytes.size() && bytes.at(leadingZeroes) == 0)
|
||||
++leadingZeroes;
|
||||
|
||||
QVector<unsigned char> digits;
|
||||
digits.reserve(45);
|
||||
for (const char byte : bytes) {
|
||||
int carry = static_cast<unsigned char>(byte);
|
||||
for (unsigned char& digit : digits) {
|
||||
carry += static_cast<int>(digit) * 256;
|
||||
digit = static_cast<unsigned char>(carry % 58);
|
||||
carry /= 58;
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.append(static_cast<unsigned char>(carry % 58));
|
||||
carry /= 58;
|
||||
}
|
||||
}
|
||||
|
||||
QString encoded(leadingZeroes, QLatin1Char('1'));
|
||||
encoded.reserve(leadingZeroes + digits.size());
|
||||
for (auto digit = digits.crbegin(); digit != digits.crend(); ++digit)
|
||||
encoded.append(QLatin1Char(BASE58_ALPHABET[*digit]));
|
||||
return encoded;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
QString walletAccountIdToBase58(const QString& accountId);
|
||||
@@ -1,5 +1,13 @@
|
||||
#include "WalletAccountModel.h"
|
||||
|
||||
#include "WalletAccountId.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
const QString DEFAULT_PROGRAM_OWNER(64, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
WalletAccountModel::WalletAccountModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
@@ -21,10 +29,36 @@ QVariant WalletAccountModel::data(const QModelIndex& index, int role) const
|
||||
return account.name;
|
||||
case AddressRole:
|
||||
return account.address;
|
||||
case DisplayAddressRole:
|
||||
return account.displayAddress;
|
||||
case BalanceRole:
|
||||
return account.balance;
|
||||
case IsPublicRole:
|
||||
return account.isPublic;
|
||||
case KindRole:
|
||||
return account.kind;
|
||||
case SectionRole:
|
||||
return account.section;
|
||||
case ProgramOwnerRole:
|
||||
return account.programOwner;
|
||||
case ReadStatusRole:
|
||||
return account.readStatus;
|
||||
case ProgramNameRole:
|
||||
return account.programName;
|
||||
case AccountTypeRole:
|
||||
return account.accountType;
|
||||
case VisibilityRole:
|
||||
return account.isPublic ? QStringLiteral("public") : QStringLiteral("private");
|
||||
case ControlRole:
|
||||
return QStringLiteral("wallet");
|
||||
case CanBePrimaryRole:
|
||||
return account.canBePrimary;
|
||||
case IsPrimaryRole:
|
||||
return account.isPrimary;
|
||||
case DefinitionIdRole:
|
||||
return account.definitionId;
|
||||
case AliasRole:
|
||||
return account.alias;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -37,25 +71,170 @@ QHash<int, QByteArray> WalletAccountModel::roleNames() const
|
||||
{ AddressRole, "address" },
|
||||
{ BalanceRole, "balance" },
|
||||
{ IsPublicRole, "isPublic" },
|
||||
{ KindRole, "kind" },
|
||||
{ SectionRole, "section" },
|
||||
{ ProgramOwnerRole, "programOwner" },
|
||||
{ ReadStatusRole, "readStatus" },
|
||||
{ ProgramNameRole, "programName" },
|
||||
{ AccountTypeRole, "accountType" },
|
||||
{ VisibilityRole, "visibility" },
|
||||
{ ControlRole, "control" },
|
||||
{ CanBePrimaryRole, "canBePrimary" },
|
||||
{ IsPrimaryRole, "isPrimary" },
|
||||
{ DefinitionIdRole, "definitionId" },
|
||||
{ AliasRole, "alias" },
|
||||
{ DisplayAddressRole, "displayAddress" },
|
||||
};
|
||||
}
|
||||
|
||||
void WalletAccountModel::replaceAccounts(const QVector<WalletAccount>& accounts)
|
||||
void WalletAccountModel::replaceAccounts(const QVector<WalletAccount>& accounts,
|
||||
const QHash<QString, QString>& aliases,
|
||||
const QString& primaryAddress)
|
||||
{
|
||||
beginResetModel();
|
||||
const qsizetype oldCount = m_accounts.size();
|
||||
m_accounts.clear();
|
||||
m_accounts.reserve(accounts.size());
|
||||
for (qsizetype index = 0; index < accounts.size(); ++index) {
|
||||
const WalletAccount& account = accounts.at(index);
|
||||
m_accounts.append({
|
||||
QStringLiteral("Account %1").arg(index + 1),
|
||||
account.address,
|
||||
account.balance,
|
||||
account.isPublic,
|
||||
});
|
||||
for (const WalletAccount& account : accounts) {
|
||||
Entry entry;
|
||||
entry.alias = aliases.value(account.address);
|
||||
entry.address = account.address;
|
||||
entry.displayAddress = walletAccountIdToBase58(account.address);
|
||||
if (entry.displayAddress.isEmpty())
|
||||
entry.displayAddress = account.address;
|
||||
entry.balance = account.balance;
|
||||
entry.isPublic = account.isPublic;
|
||||
entry.programOwner = account.programOwner;
|
||||
entry.readStatus = account.readStatus;
|
||||
if (!account.isPublic) {
|
||||
entry.kind = QStringLiteral("private");
|
||||
entry.canBePrimary = true;
|
||||
} else if (account.readStatus != QStringLiteral("ok")) {
|
||||
entry.kind = QStringLiteral("unknown");
|
||||
} else if (account.programOwner == DEFAULT_PROGRAM_OWNER) {
|
||||
entry.kind = QStringLiteral("user");
|
||||
entry.canBePrimary = true;
|
||||
} else {
|
||||
entry.kind = QStringLiteral("program");
|
||||
}
|
||||
entry.section = sectionFor(entry);
|
||||
entry.isPrimary = account.address == primaryAddress && entry.canBePrimary;
|
||||
updateEntryName(entry);
|
||||
m_accounts.append(std::move(entry));
|
||||
}
|
||||
endResetModel();
|
||||
if (oldCount != m_accounts.size())
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void WalletAccountModel::applyPresentations(
|
||||
const QVector<WalletAccountPresentation>& presentations)
|
||||
{
|
||||
for (const WalletAccountPresentation& presentation : presentations) {
|
||||
const int row = indexOf(presentation.address);
|
||||
if (row < 0)
|
||||
continue;
|
||||
Entry& entry = m_accounts[row];
|
||||
if (!presentation.kind.isEmpty())
|
||||
entry.kind = presentation.kind;
|
||||
entry.programName = presentation.programName;
|
||||
entry.accountType = presentation.accountType;
|
||||
entry.definitionId = presentation.definitionId;
|
||||
entry.semanticName = presentation.semanticName;
|
||||
entry.section = sectionFor(entry, presentation.hiddenFromAccounts);
|
||||
entry.canBePrimary = entry.kind == QStringLiteral("user")
|
||||
|| entry.kind == QStringLiteral("private");
|
||||
if (!entry.canBePrimary)
|
||||
entry.isPrimary = false;
|
||||
updateEntryName(entry);
|
||||
const QModelIndex changed = index(row);
|
||||
emit dataChanged(changed, changed);
|
||||
}
|
||||
}
|
||||
|
||||
void WalletAccountModel::setAlias(const QString& address, const QString& alias)
|
||||
{
|
||||
const int row = indexOf(address);
|
||||
if (row < 0)
|
||||
return;
|
||||
Entry& entry = m_accounts[row];
|
||||
entry.alias = alias;
|
||||
updateEntryName(entry);
|
||||
emit dataChanged(index(row), index(row), { NameRole, AliasRole });
|
||||
}
|
||||
|
||||
void WalletAccountModel::setPrimaryAddress(const QString& address)
|
||||
{
|
||||
for (int row = 0; row < m_accounts.size(); ++row) {
|
||||
Entry& entry = m_accounts[row];
|
||||
const bool next = entry.address == address && entry.canBePrimary;
|
||||
if (entry.isPrimary == next)
|
||||
continue;
|
||||
entry.isPrimary = next;
|
||||
emit dataChanged(index(row), index(row), { IsPrimaryRole });
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletAccountModel::contains(const QString& address) const
|
||||
{
|
||||
return indexOf(address) >= 0;
|
||||
}
|
||||
|
||||
bool WalletAccountModel::canBePrimary(const QString& address) const
|
||||
{
|
||||
const int row = indexOf(address);
|
||||
return row >= 0 && m_accounts.at(row).canBePrimary;
|
||||
}
|
||||
|
||||
QString WalletAccountModel::firstAutomaticPrimary() const
|
||||
{
|
||||
for (const Entry& entry : m_accounts) {
|
||||
if (entry.kind == QStringLiteral("user"))
|
||||
return entry.address;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int WalletAccountModel::indexOf(const QString& address) const
|
||||
{
|
||||
for (int row = 0; row < m_accounts.size(); ++row) {
|
||||
if (m_accounts.at(row).address == address)
|
||||
return row;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString WalletAccountModel::defaultName(const Entry& entry)
|
||||
{
|
||||
if (!entry.accountType.isEmpty()) {
|
||||
QString name = entry.accountType;
|
||||
for (qsizetype index = 1; index < name.size(); ++index) {
|
||||
if (name.at(index).isUpper() && name.at(index - 1).isLower())
|
||||
name.insert(index++, QLatin1Char(' '));
|
||||
}
|
||||
return name;
|
||||
}
|
||||
if (entry.kind == QStringLiteral("user"))
|
||||
return QStringLiteral("User account");
|
||||
if (entry.kind == QStringLiteral("private"))
|
||||
return QStringLiteral("Private account");
|
||||
if (entry.kind == QStringLiteral("unknown"))
|
||||
return QStringLiteral("Unknown account");
|
||||
return QStringLiteral("Program account");
|
||||
}
|
||||
|
||||
QString WalletAccountModel::sectionFor(const Entry& entry, bool hiddenFromAccounts)
|
||||
{
|
||||
if (hiddenFromAccounts || entry.kind == QStringLiteral("token_holding"))
|
||||
return QStringLiteral("hidden");
|
||||
if (entry.kind == QStringLiteral("user") || entry.kind == QStringLiteral("private"))
|
||||
return QStringLiteral("accounts");
|
||||
return QStringLiteral("advanced");
|
||||
}
|
||||
|
||||
void WalletAccountModel::updateEntryName(Entry& entry)
|
||||
{
|
||||
entry.name = !entry.alias.isEmpty()
|
||||
? entry.alias
|
||||
: (!entry.semanticName.isEmpty() ? entry.semanticName : defaultName(entry));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
struct WalletAccountPresentation {
|
||||
QString address;
|
||||
QString kind;
|
||||
QString semanticName;
|
||||
QString programName;
|
||||
QString accountType;
|
||||
QString definitionId;
|
||||
bool hiddenFromAccounts = false;
|
||||
};
|
||||
|
||||
class WalletAccountModel final : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
@@ -15,6 +26,19 @@ public:
|
||||
AddressRole,
|
||||
BalanceRole,
|
||||
IsPublicRole,
|
||||
KindRole,
|
||||
SectionRole,
|
||||
ProgramOwnerRole,
|
||||
ReadStatusRole,
|
||||
ProgramNameRole,
|
||||
AccountTypeRole,
|
||||
VisibilityRole,
|
||||
ControlRole,
|
||||
CanBePrimaryRole,
|
||||
IsPrimaryRole,
|
||||
DefinitionIdRole,
|
||||
AliasRole,
|
||||
DisplayAddressRole,
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
@@ -24,7 +48,16 @@ public:
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void replaceAccounts(const QVector<WalletAccount>& accounts);
|
||||
void replaceAccounts(const QVector<WalletAccount>& accounts,
|
||||
const QHash<QString, QString>& aliases = {},
|
||||
const QString& primaryAddress = {});
|
||||
void applyPresentations(const QVector<WalletAccountPresentation>& presentations);
|
||||
void setAlias(const QString& address, const QString& alias);
|
||||
void setPrimaryAddress(const QString& address);
|
||||
bool contains(const QString& address) const;
|
||||
bool canBePrimary(const QString& address) const;
|
||||
QString firstAutomaticPrimary() const;
|
||||
int indexOf(const QString& address) const;
|
||||
int count() const { return m_accounts.size(); }
|
||||
|
||||
signals:
|
||||
@@ -32,11 +65,27 @@ signals:
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QString alias;
|
||||
QString semanticName;
|
||||
QString name;
|
||||
QString address;
|
||||
QString displayAddress;
|
||||
QString balance;
|
||||
bool isPublic = true;
|
||||
QString kind;
|
||||
QString section;
|
||||
QString programOwner;
|
||||
QString readStatus;
|
||||
QString programName;
|
||||
QString accountType;
|
||||
QString definitionId;
|
||||
bool canBePrimary = false;
|
||||
bool isPrimary = false;
|
||||
};
|
||||
|
||||
static QString defaultName(const Entry& entry);
|
||||
static QString sectionFor(const Entry& entry, bool hiddenFromAccounts = false);
|
||||
void updateEntryName(Entry& entry);
|
||||
|
||||
QVector<Entry> m_accounts;
|
||||
};
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
#include <utility>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
@@ -18,6 +22,10 @@ namespace {
|
||||
const char SETTINGS_ORG[] = "Logos";
|
||||
const char DISCONNECTED_KEY[] = "disconnected";
|
||||
const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
|
||||
const char WALLET_SETTINGS_GROUP[] = "wallets";
|
||||
const char ALIASES_KEY[] = "aliases";
|
||||
const char PRIMARY_ACCOUNT_KEY[] = "primaryAccount";
|
||||
constexpr qsizetype MAX_ALIAS_LENGTH = 40;
|
||||
|
||||
QString toLocalPath(const QString& path)
|
||||
{
|
||||
@@ -25,6 +33,26 @@ QString toLocalPath(const QString& path)
|
||||
return QUrl::fromUserInput(path).toLocalFile();
|
||||
return path;
|
||||
}
|
||||
|
||||
QString configuredSequencer(const QString& path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
|
||||
if (!document.isObject())
|
||||
return {};
|
||||
return document.object().value(QStringLiteral("sequencer_addr")).toString();
|
||||
}
|
||||
|
||||
QString canonicalStoragePath(const QString& path)
|
||||
{
|
||||
const QFileInfo info(path);
|
||||
const QString canonical = info.canonicalFilePath();
|
||||
return canonical.isEmpty()
|
||||
? QDir::cleanPath(info.absoluteFilePath())
|
||||
: canonical;
|
||||
}
|
||||
}
|
||||
|
||||
WalletController::WalletController(WalletProvider& wallet,
|
||||
@@ -38,7 +66,10 @@ WalletController::WalletController(WalletProvider& wallet,
|
||||
m_reachabilityTimer(new QTimer(this))
|
||||
{
|
||||
m_state.walletHome = defaultWalletHome();
|
||||
m_state.configPath = defaultConfigPath();
|
||||
m_state.storagePath = defaultStoragePath();
|
||||
m_state.walletExists = QFileInfo::exists(defaultStoragePath());
|
||||
m_state.sequencerAddress = configuredSequencer(defaultConfigPath());
|
||||
|
||||
m_reachabilityTimer->setInterval(10000);
|
||||
connect(m_reachabilityTimer, &QTimer::timeout,
|
||||
@@ -83,20 +114,60 @@ void WalletController::openOnStartup()
|
||||
|
||||
const QString config = defaultConfigPath();
|
||||
const QString storage = defaultStoragePath();
|
||||
const WalletSession session = m_wallet.connect({ config, storage });
|
||||
if (session.failure == WalletFailure::WalletMissing)
|
||||
return;
|
||||
if (!session.ok()) {
|
||||
qWarning() << "WalletController: wallet connection failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return;
|
||||
beginOpen(config, storage);
|
||||
}
|
||||
|
||||
bool WalletController::beginOpen(const QString& config, const QString& storage)
|
||||
{
|
||||
if (m_state.syncStatus == QStringLiteral("opening")
|
||||
|| m_state.syncStatus == QStringLiteral("syncing")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const quint64 generation = ++m_operationGeneration;
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
|
||||
m_state.isWalletOpen = true;
|
||||
applySnapshot(session.snapshot);
|
||||
m_state.syncStatus = QStringLiteral("opening");
|
||||
m_state.syncError.clear();
|
||||
const QString endpoint = configuredSequencer(config);
|
||||
if (!endpoint.isEmpty())
|
||||
m_state.sequencerAddress = endpoint;
|
||||
emit stateChanged();
|
||||
|
||||
QTimer::singleShot(0, this, [this, generation]() {
|
||||
if (generation == m_operationGeneration
|
||||
&& m_state.syncStatus == QStringLiteral("opening")) {
|
||||
m_state.syncStatus = QStringLiteral("syncing");
|
||||
emit stateChanged();
|
||||
}
|
||||
});
|
||||
m_wallet.connectAsync({ config, storage },
|
||||
[this, generation, config, storage](WalletSession session) {
|
||||
if (generation != m_operationGeneration)
|
||||
return;
|
||||
if (session.failure == WalletFailure::WalletMissing) {
|
||||
m_state.syncStatus = QStringLiteral("closed");
|
||||
m_state.walletExists = false;
|
||||
emit 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();
|
||||
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);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
QString WalletController::createDefaultWallet(const QString& password)
|
||||
@@ -112,7 +183,9 @@ QString WalletController::createWallet(const QString& configPath,
|
||||
const QString storage = toLocalPath(storagePath);
|
||||
const WalletCreation creation = m_wallet.createWallet(
|
||||
{ config, storage }, password);
|
||||
if (creation.mnemonic.isEmpty()) {
|
||||
const bool createdButUnreadable = creation.failure == WalletFailure::ReadFailed;
|
||||
if (creation.mnemonic.isEmpty()
|
||||
|| (!creation.ok() && !createdButUnreadable)) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
@@ -122,14 +195,18 @@ QString WalletController::createWallet(const QString& configPath,
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = true;
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
m_state.isWalletOpen = true;
|
||||
if (!creation.snapshot.ok()) {
|
||||
qWarning() << "WalletController: wallet creation refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
m_state.syncStatus = QStringLiteral("error");
|
||||
m_state.syncError = walletFailureCode(creation.snapshot.failure);
|
||||
emit stateChanged();
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
m_state.isWalletOpen = true;
|
||||
m_state.syncStatus = QStringLiteral("ready");
|
||||
m_state.syncError.clear();
|
||||
applySnapshot(creation.snapshot);
|
||||
return creation.mnemonic;
|
||||
}
|
||||
@@ -140,29 +217,67 @@ bool WalletController::open()
|
||||
? defaultConfigPath() : m_state.configPath;
|
||||
const QString storage = m_state.storagePath.isEmpty()
|
||||
? defaultStoragePath() : m_state.storagePath;
|
||||
const WalletSession session = m_wallet.connect({ config, storage });
|
||||
if (!session.ok()) {
|
||||
qWarning() << "WalletController: wallet open failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = true;
|
||||
m_state.isWalletOpen = true;
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
|
||||
applySnapshot(session.snapshot);
|
||||
return true;
|
||||
return beginOpen(config, storage);
|
||||
}
|
||||
|
||||
void WalletController::disconnect()
|
||||
{
|
||||
++m_operationGeneration;
|
||||
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();
|
||||
emit snapshotChanged();
|
||||
}
|
||||
|
||||
bool WalletController::setAccountAlias(const QString& address, const QString& alias)
|
||||
{
|
||||
if (!m_accountModel->contains(address))
|
||||
return false;
|
||||
const QString normalized = alias.trimmed();
|
||||
if (normalized.size() > MAX_ALIAS_LENGTH)
|
||||
return false;
|
||||
if (normalized.isEmpty())
|
||||
m_aliases.remove(address);
|
||||
else
|
||||
m_aliases.insert(address, normalized);
|
||||
m_accountModel->setAlias(address, normalized);
|
||||
storeAliases(m_aliases);
|
||||
updatePrimaryState(m_state.primaryAccountAddress);
|
||||
emit stateChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletController::setPrimaryAccount(const QString& address)
|
||||
{
|
||||
if (!m_accountModel->canBePrimary(address))
|
||||
return false;
|
||||
m_accountModel->setPrimaryAddress(address);
|
||||
storePrimaryAccount(address);
|
||||
updatePrimaryState(address);
|
||||
emit stateChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletController::applyAccountPresentations(
|
||||
const QVector<WalletAccountPresentation>& presentations)
|
||||
{
|
||||
m_accountModel->applyPresentations(presentations);
|
||||
QString primary = m_state.primaryAccountAddress;
|
||||
if (!m_accountModel->canBePrimary(primary))
|
||||
primary = m_accountModel->firstAutomaticPrimary();
|
||||
m_accountModel->setPrimaryAddress(primary);
|
||||
storePrimaryAccount(primary);
|
||||
updatePrimaryState(primary);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
QString WalletController::createAccount(bool isPublic)
|
||||
@@ -174,23 +289,41 @@ QString WalletController::createAccount(bool isPublic)
|
||||
return {};
|
||||
}
|
||||
if (creation.snapshot.ok()) {
|
||||
m_state.syncStatus = QStringLiteral("ready");
|
||||
m_state.syncError.clear();
|
||||
applySnapshot(creation.snapshot);
|
||||
} else {
|
||||
qWarning() << "WalletController: account refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
m_state.syncStatus = QStringLiteral("error");
|
||||
m_state.syncError = walletFailureCode(creation.snapshot.failure);
|
||||
emit stateChanged();
|
||||
}
|
||||
return creation.accountId;
|
||||
}
|
||||
|
||||
void WalletController::refresh()
|
||||
{
|
||||
const WalletSnapshot next = m_wallet.snapshot(true);
|
||||
if (next.ok()) {
|
||||
applySnapshot(next);
|
||||
} else {
|
||||
qWarning() << "WalletController: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
}
|
||||
if (!m_state.isWalletOpen || m_state.syncStatus == QStringLiteral("syncing"))
|
||||
return;
|
||||
const quint64 generation = ++m_operationGeneration;
|
||||
m_state.syncStatus = QStringLiteral("syncing");
|
||||
m_state.syncError.clear();
|
||||
emit stateChanged();
|
||||
m_wallet.snapshotAsync(true, [this, generation](WalletSnapshot next) {
|
||||
if (generation != m_operationGeneration)
|
||||
return;
|
||||
if (next.ok()) {
|
||||
m_state.syncStatus = QStringLiteral("ready");
|
||||
applySnapshot(next);
|
||||
} else {
|
||||
qWarning() << "WalletController: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
m_state.syncStatus = QStringLiteral("error");
|
||||
m_state.syncError = walletFailureCode(next.failure);
|
||||
emit stateChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QString WalletController::balance(const QString& accountId, bool isPublic)
|
||||
@@ -205,14 +338,85 @@ QString WalletController::balance(const QString& accountId, bool isPublic)
|
||||
|
||||
void WalletController::applySnapshot(const WalletSnapshot& snapshot)
|
||||
{
|
||||
m_accountModel->replaceAccounts(snapshot.accounts);
|
||||
m_snapshot = snapshot;
|
||||
m_aliases = loadAliases();
|
||||
QString primary = loadPrimaryAccount();
|
||||
m_accountModel->replaceAccounts(snapshot.accounts, m_aliases, primary);
|
||||
if (!m_accountModel->canBePrimary(primary))
|
||||
primary = m_accountModel->firstAutomaticPrimary();
|
||||
m_accountModel->setPrimaryAddress(primary);
|
||||
storePrimaryAccount(primary);
|
||||
updatePrimaryState(primary);
|
||||
m_state.lastSyncedBlock = static_cast<int>(snapshot.lastSyncedBlock);
|
||||
m_state.currentBlockHeight = static_cast<int>(snapshot.currentBlockHeight);
|
||||
m_state.sequencerAddress = snapshot.sequencerAddress;
|
||||
if (!snapshot.sequencerAddress.isEmpty())
|
||||
m_state.sequencerAddress = snapshot.sequencerAddress;
|
||||
emit snapshotChanged();
|
||||
emit stateChanged();
|
||||
checkReachability();
|
||||
}
|
||||
|
||||
QString WalletController::walletSettingsGroup() const
|
||||
{
|
||||
const QByteArray hash = QCryptographicHash::hash(
|
||||
canonicalStoragePath(m_state.storagePath).toUtf8(),
|
||||
QCryptographicHash::Sha256).toHex();
|
||||
return QStringLiteral("%1/%2")
|
||||
.arg(QString::fromLatin1(WALLET_SETTINGS_GROUP), QString::fromLatin1(hash));
|
||||
}
|
||||
|
||||
QHash<QString, QString> WalletController::loadAliases() const
|
||||
{
|
||||
QSettings settings(SETTINGS_ORG, m_settingsApplication);
|
||||
settings.beginGroup(walletSettingsGroup());
|
||||
const QVariantMap stored = settings.value(ALIASES_KEY).toMap();
|
||||
QHash<QString, QString> aliases;
|
||||
for (auto iterator = stored.cbegin(); iterator != stored.cend(); ++iterator) {
|
||||
const QString alias = iterator.value().toString().trimmed();
|
||||
if (!alias.isEmpty() && alias.size() <= MAX_ALIAS_LENGTH)
|
||||
aliases.insert(iterator.key(), alias);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
QString WalletController::loadPrimaryAccount() const
|
||||
{
|
||||
QSettings settings(SETTINGS_ORG, m_settingsApplication);
|
||||
settings.beginGroup(walletSettingsGroup());
|
||||
return settings.value(PRIMARY_ACCOUNT_KEY).toString();
|
||||
}
|
||||
|
||||
void WalletController::storeAliases(const QHash<QString, QString>& aliases) const
|
||||
{
|
||||
QVariantMap stored;
|
||||
for (auto iterator = aliases.cbegin(); iterator != aliases.cend(); ++iterator)
|
||||
stored.insert(iterator.key(), iterator.value());
|
||||
QSettings settings(SETTINGS_ORG, m_settingsApplication);
|
||||
settings.beginGroup(walletSettingsGroup());
|
||||
settings.setValue(ALIASES_KEY, stored);
|
||||
}
|
||||
|
||||
void WalletController::storePrimaryAccount(const QString& address) const
|
||||
{
|
||||
QSettings settings(SETTINGS_ORG, m_settingsApplication);
|
||||
settings.beginGroup(walletSettingsGroup());
|
||||
if (address.isEmpty())
|
||||
settings.remove(PRIMARY_ACCOUNT_KEY);
|
||||
else
|
||||
settings.setValue(PRIMARY_ACCOUNT_KEY, address);
|
||||
}
|
||||
|
||||
void WalletController::updatePrimaryState(const QString& address)
|
||||
{
|
||||
m_state.primaryAccountAddress = address;
|
||||
m_state.primaryAccountName.clear();
|
||||
const int row = m_accountModel->indexOf(address);
|
||||
if (row >= 0) {
|
||||
m_state.primaryAccountName = m_accountModel->data(
|
||||
m_accountModel->index(row), WalletAccountModel::NameRole).toString();
|
||||
}
|
||||
}
|
||||
|
||||
void WalletController::checkReachability()
|
||||
{
|
||||
if (!m_state.isWalletOpen || m_state.sequencerAddress.isEmpty())
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QTimer;
|
||||
class WalletAccountModel;
|
||||
struct WalletAccountPresentation;
|
||||
|
||||
struct WalletUiState {
|
||||
bool isWalletOpen = false;
|
||||
@@ -19,6 +22,15 @@ struct WalletUiState {
|
||||
int currentBlockHeight = 0;
|
||||
QString sequencerAddress;
|
||||
bool sequencerReachable = true;
|
||||
QString syncStatus = QStringLiteral("closed");
|
||||
QString syncError;
|
||||
QString primaryAccountAddress;
|
||||
QString primaryAccountName;
|
||||
|
||||
bool canSubmit() const
|
||||
{
|
||||
return isWalletOpen && syncStatus == QStringLiteral("ready");
|
||||
}
|
||||
};
|
||||
|
||||
class WalletController final : public QObject {
|
||||
@@ -33,6 +45,7 @@ public:
|
||||
|
||||
WalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
const WalletUiState& state() const { return m_state; }
|
||||
const WalletSnapshot& snapshot() const { return m_snapshot; }
|
||||
|
||||
void start();
|
||||
QString createAccount(bool isPublic);
|
||||
@@ -44,9 +57,14 @@ public:
|
||||
const QString& password);
|
||||
bool open();
|
||||
void disconnect();
|
||||
bool setAccountAlias(const QString& address, const QString& alias);
|
||||
bool setPrimaryAccount(const QString& address);
|
||||
void applyAccountPresentations(
|
||||
const QVector<WalletAccountPresentation>& presentations);
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
void snapshotChanged();
|
||||
|
||||
private:
|
||||
static QString defaultWalletHome();
|
||||
@@ -54,14 +72,24 @@ private:
|
||||
QString defaultStoragePath() const;
|
||||
|
||||
void openOnStartup();
|
||||
bool beginOpen(const QString& config, const QString& storage);
|
||||
void applySnapshot(const WalletSnapshot& snapshot);
|
||||
void checkReachability();
|
||||
QString walletSettingsGroup() const;
|
||||
QHash<QString, QString> loadAliases() const;
|
||||
QString loadPrimaryAccount() const;
|
||||
void storeAliases(const QHash<QString, QString>& aliases) const;
|
||||
void storePrimaryAccount(const QString& address) const;
|
||||
void updatePrimaryState(const QString& address);
|
||||
|
||||
WalletProvider& m_wallet;
|
||||
QString m_settingsApplication;
|
||||
WalletUiState m_state;
|
||||
WalletSnapshot m_snapshot;
|
||||
QHash<QString, QString> m_aliases;
|
||||
WalletAccountModel* m_accountModel;
|
||||
QNetworkAccessManager* m_network;
|
||||
QTimer* m_reachabilityTimer;
|
||||
bool m_started = false;
|
||||
quint64 m_operationGeneration = 0;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
@@ -38,6 +39,9 @@ struct WalletAccount {
|
||||
QString address;
|
||||
QString balance;
|
||||
bool isPublic = true;
|
||||
QString readStatus;
|
||||
QString programOwner;
|
||||
QString dataHex;
|
||||
};
|
||||
|
||||
struct WalletSnapshot {
|
||||
@@ -92,15 +96,23 @@ struct WalletSubmission {
|
||||
|
||||
class WalletProvider {
|
||||
public:
|
||||
using SessionCallback = std::function<void(WalletSession)>;
|
||||
using SnapshotCallback = std::function<void(WalletSnapshot)>;
|
||||
using AccountReadsCallback = std::function<void(QVector<WalletAccountRead>)>;
|
||||
|
||||
virtual ~WalletProvider() = default;
|
||||
|
||||
virtual WalletSession connect(const WalletPaths& paths) = 0;
|
||||
virtual void connectAsync(const WalletPaths& paths, SessionCallback callback) = 0;
|
||||
virtual WalletCreation createWallet(const WalletPaths& paths,
|
||||
const QString& password) = 0;
|
||||
virtual WalletSnapshot snapshot(bool forceRefresh = false) = 0;
|
||||
virtual void snapshotAsync(bool forceRefresh, SnapshotCallback callback) = 0;
|
||||
virtual void clearSnapshot() = 0;
|
||||
virtual WalletAccountCreation createAccount(bool isPublic) = 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 disconnect() = 0;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "FakeWalletProvider.h"
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "WalletAccountId.h"
|
||||
#include "WalletAccountModel.h"
|
||||
#include "WalletController.h"
|
||||
#include "logos_sdk.h"
|
||||
@@ -17,7 +18,9 @@
|
||||
namespace {
|
||||
const QString ACCOUNT_A(64, QLatin1Char('a'));
|
||||
const QString ACCOUNT_B(64, QLatin1Char('b'));
|
||||
const QString ACCOUNT_C(64, QLatin1Char('d'));
|
||||
const QString PROGRAM_ID(64, QLatin1Char('c'));
|
||||
const QString EOA_OWNER(64, QLatin1Char('0'));
|
||||
|
||||
QString publicAccountJson(const QString& owner = PROGRAM_ID,
|
||||
const QString& balance = QStringLiteral("01000000000000000000000000000000"),
|
||||
@@ -39,6 +42,7 @@ QVariantMap accountEntry(const QString& id, bool isPublic)
|
||||
{ QStringLiteral("is_public"), isPublic },
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LogosWalletProviderTest : public QObject {
|
||||
@@ -47,6 +51,7 @@ class LogosWalletProviderTest : public QObject {
|
||||
private slots:
|
||||
void adoptsOpenWalletAndCachesSnapshots();
|
||||
void opensConfiguredWalletWhenNoSharedSessionExists();
|
||||
void opensAndReadsAsynchronously();
|
||||
void createsAndPersistsWallet();
|
||||
void validatesCompletePublicAccountPayloads();
|
||||
void fallsBackToBalanceWhenPublicReadFails();
|
||||
@@ -55,9 +60,12 @@ private slots:
|
||||
void preservesCreatedAccountWhenSnapshotRefreshFails();
|
||||
void dispatchesExactGenericTransaction();
|
||||
void rejectsInvalidSubmissionResponses();
|
||||
void encodesAccountIdsForDisplay();
|
||||
void exposesStableAccountModelRoles();
|
||||
void persistsHumanizedWalletPreferences();
|
||||
void fakeProviderImplementsConsumerContract();
|
||||
void controllerOwnsUiWalletFlow();
|
||||
void controllerReportsCreationPersistenceAndRefreshFailures();
|
||||
void controllerStopsReachabilityChecksAfterDisconnect();
|
||||
};
|
||||
|
||||
@@ -84,6 +92,10 @@ void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||
QCOMPARE(session.snapshot.accounts.at(0).balance, QStringLiteral("1"));
|
||||
QCOMPARE(session.snapshot.accounts.at(1).balance, QStringLiteral("42"));
|
||||
QCOMPARE(session.snapshot.publicAccountReads.size(), 1);
|
||||
QCOMPARE(session.snapshot.accounts.at(0).readStatus, QStringLiteral("ok"));
|
||||
QCOMPARE(session.snapshot.accounts.at(0).programOwner, PROGRAM_ID);
|
||||
QCOMPARE(session.snapshot.accounts.at(0).dataHex, QStringLiteral("00ff"));
|
||||
QCOMPARE(session.snapshot.accounts.at(1).readStatus, QStringLiteral("private"));
|
||||
QCOMPARE(session.snapshot.currentBlockHeight, quint64(12));
|
||||
QCOMPARE(session.snapshot.lastSyncedBlock, quint64(11));
|
||||
|
||||
@@ -138,6 +150,38 @@ void LogosWalletProviderTest::opensConfiguredWalletWhenNoSharedSessionExists()
|
||||
WalletFailure::WalletMissing);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::opensAndReadsAsynchronously()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
|
||||
modules.logos_execution_zone.publicAccounts.insert(
|
||||
ACCOUNT_A, publicAccountJson(EOA_OWNER));
|
||||
LogosWalletProvider provider(&modules);
|
||||
|
||||
bool connected = false;
|
||||
provider.connectAsync({}, [&connected](WalletSession session) {
|
||||
connected = session.ok() && session.snapshot.accounts.size() == 1;
|
||||
});
|
||||
QVERIFY(connected);
|
||||
|
||||
bool refreshed = false;
|
||||
provider.snapshotAsync(true, [&refreshed](WalletSnapshot snapshot) {
|
||||
refreshed = snapshot.ok() && snapshot.accounts.at(0).programOwner == EOA_OWNER;
|
||||
});
|
||||
QVERIFY(refreshed);
|
||||
|
||||
bool batchRead = false;
|
||||
provider.readPublicAccountsAsync(
|
||||
{ ACCOUNT_A, ACCOUNT_B },
|
||||
[&batchRead](QVector<WalletAccountRead> reads) {
|
||||
batchRead = reads.size() == 2
|
||||
&& reads.at(0).ok()
|
||||
&& !reads.at(1).ok();
|
||||
});
|
||||
QVERIFY(batchRead);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::createsAndPersistsWallet()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
@@ -338,19 +382,92 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
WalletAccountModel model;
|
||||
QSignalSpy countChanged(&model, &WalletAccountModel::countChanged);
|
||||
model.replaceAccounts({
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true },
|
||||
{ ACCOUNT_B, QStringLiteral("20"), false },
|
||||
});
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
{ ACCOUNT_B, QStringLiteral("20"), false, QStringLiteral("private"), {}, {} },
|
||||
{ ACCOUNT_C, QStringLiteral("30"), true, QStringLiteral("ok"), PROGRAM_ID, QStringLiteral("00") },
|
||||
}, { { ACCOUNT_A, QStringLiteral("Trading") } }, ACCOUNT_A);
|
||||
|
||||
QCOMPARE(model.count(), 2);
|
||||
QCOMPARE(model.count(), 3);
|
||||
QCOMPARE(countChanged.count(), 1);
|
||||
QCOMPARE(model.roleNames().value(WalletAccountModel::NameRole), QByteArray("name"));
|
||||
QCOMPARE(model.data(model.index(0), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("Account 1"));
|
||||
QStringLiteral("Trading"));
|
||||
QCOMPARE(model.data(model.index(0), WalletAccountModel::KindRole).toString(),
|
||||
QStringLiteral("user"));
|
||||
QVERIFY(model.data(model.index(0), WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
QVERIFY(model.data(model.index(0), WalletAccountModel::IsPrimaryRole).toBool());
|
||||
QCOMPARE(model.data(model.index(1), WalletAccountModel::AddressRole).toString(), ACCOUNT_B);
|
||||
QCOMPARE(model.roleNames().value(WalletAccountModel::DisplayAddressRole),
|
||||
QByteArray("displayAddress"));
|
||||
QCOMPARE(model.data(model.index(1), WalletAccountModel::DisplayAddressRole).toString(),
|
||||
walletAccountIdToBase58(ACCOUNT_B));
|
||||
QCOMPARE(model.data(model.index(1), WalletAccountModel::BalanceRole).toString(),
|
||||
QStringLiteral("20"));
|
||||
QVERIFY(!model.data(model.index(1), WalletAccountModel::IsPublicRole).toBool());
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::KindRole).toString(),
|
||||
QStringLiteral("program"));
|
||||
QVERIFY(!model.data(model.index(2), WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
|
||||
model.applyPresentations({ {
|
||||
ACCOUNT_C,
|
||||
QStringLiteral("token_holding"),
|
||||
QStringLiteral("TEST holding"),
|
||||
QStringLiteral("Token"),
|
||||
QStringLiteral("TokenHolding"),
|
||||
ACCOUNT_A,
|
||||
true,
|
||||
} });
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::SectionRole).toString(),
|
||||
QStringLiteral("hidden"));
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("TEST holding"));
|
||||
model.setAlias(ACCOUNT_C, QStringLiteral("Reserve"));
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("Reserve"));
|
||||
model.setAlias(ACCOUNT_C, {});
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("TEST holding"));
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::encodesAccountIdsForDisplay()
|
||||
{
|
||||
QCOMPARE(walletAccountIdToBase58(
|
||||
QStringLiteral("00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb")),
|
||||
QStringLiteral("14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La"));
|
||||
QCOMPARE(walletAccountIdToBase58(QString(64, QLatin1Char('0'))),
|
||||
QString(32, QLatin1Char('1')));
|
||||
QVERIFY(walletAccountIdToBase58(QStringLiteral("not-an-account-id")).isEmpty());
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::persistsHumanizedWalletPreferences()
|
||||
{
|
||||
const QString application = QStringLiteral("HumanizedWalletPreferencesTest");
|
||||
QSettings settings(QStringLiteral("Logos"), application);
|
||||
settings.clear();
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.adopted = true;
|
||||
provider.connectResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
{ ACCOUNT_B, QStringLiteral("20"), false, QStringLiteral("private"), {}, {} },
|
||||
{ ACCOUNT_C, QStringLiteral("30"), true, QStringLiteral("ok"), PROGRAM_ID, {} },
|
||||
};
|
||||
|
||||
{
|
||||
WalletController controller(provider, application);
|
||||
QVERIFY(controller.open());
|
||||
QCOMPARE(controller.state().primaryAccountAddress, ACCOUNT_A);
|
||||
QVERIFY(!controller.setPrimaryAccount(ACCOUNT_C));
|
||||
QVERIFY(controller.setAccountAlias(ACCOUNT_B, QStringLiteral(" Private savings ")));
|
||||
QVERIFY(controller.setPrimaryAccount(ACCOUNT_B));
|
||||
QCOMPARE(controller.state().primaryAccountName, QStringLiteral("Private savings"));
|
||||
QVERIFY(!controller.setAccountAlias(ACCOUNT_A, QString(41, QLatin1Char('x'))));
|
||||
}
|
||||
|
||||
WalletController reopened(provider, application);
|
||||
QVERIFY(reopened.open());
|
||||
QCOMPARE(reopened.state().primaryAccountAddress, ACCOUNT_B);
|
||||
QCOMPARE(reopened.state().primaryAccountName, QStringLiteral("Private savings"));
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
|
||||
@@ -419,6 +536,65 @@ void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerReportsCreationPersistenceAndRefreshFailures()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletCreationFailureTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
WalletController controller(provider, settingsApplication);
|
||||
const WalletUiState baseline = controller.state();
|
||||
const int baselineAccountCount = controller.accountModel()->count();
|
||||
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);
|
||||
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);
|
||||
QCOMPARE(snapshotChanged.count(), 1);
|
||||
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletReachabilityTest");
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class FakeExecutionZone {
|
||||
@@ -48,6 +50,13 @@ public:
|
||||
return openResult;
|
||||
}
|
||||
|
||||
void openAsync(const QString& config,
|
||||
const QString& storage,
|
||||
std::function<void(int)> callback)
|
||||
{
|
||||
callback(open(config, storage));
|
||||
}
|
||||
|
||||
QString create_new(const QString& config,
|
||||
const QString& storage,
|
||||
const QString& password)
|
||||
@@ -64,36 +73,69 @@ public:
|
||||
return saveResult;
|
||||
}
|
||||
|
||||
void saveAsync(std::function<void(int)> callback) { callback(save()); }
|
||||
|
||||
QString create_account_public() { return publicAccountId; }
|
||||
QString create_account_private() { return privateAccountId; }
|
||||
|
||||
int get_last_synced_block() const { return lastSyncedBlock; }
|
||||
int get_current_block_height() const { return currentBlockHeight; }
|
||||
void get_last_synced_blockAsync(std::function<void(int)> callback)
|
||||
{
|
||||
callback(get_last_synced_block());
|
||||
}
|
||||
void get_current_block_heightAsync(std::function<void(int)> callback)
|
||||
{
|
||||
callback(get_current_block_height());
|
||||
}
|
||||
|
||||
int sync_to_block(quint64)
|
||||
{
|
||||
++syncCalls;
|
||||
return syncResult;
|
||||
}
|
||||
void sync_to_blockAsync(int blockId, std::function<void(int)> callback)
|
||||
{
|
||||
callback(sync_to_block(static_cast<quint64>(blockId)));
|
||||
}
|
||||
|
||||
QString get_sequencer_addr() const { return sequencerAddress; }
|
||||
void get_sequencer_addrAsync(std::function<void(QString)> callback)
|
||||
{
|
||||
callback(get_sequencer_addr());
|
||||
}
|
||||
|
||||
QVariantList list_accounts()
|
||||
{
|
||||
++listCalls;
|
||||
return accounts;
|
||||
}
|
||||
void list_accountsAsync(std::function<void(QVariantList)> callback)
|
||||
{
|
||||
callback(list_accounts());
|
||||
}
|
||||
|
||||
QString get_account_public(const QString& accountId)
|
||||
{
|
||||
++publicReadCalls;
|
||||
return publicAccounts.value(accountId);
|
||||
}
|
||||
void get_account_publicAsync(const QString& accountId,
|
||||
std::function<void(QString)> callback)
|
||||
{
|
||||
callback(get_account_public(accountId));
|
||||
}
|
||||
|
||||
QString get_balance(const QString& accountId, bool) const
|
||||
{
|
||||
return balances.value(accountId);
|
||||
}
|
||||
void get_balanceAsync(const QString& accountId,
|
||||
bool isPublic,
|
||||
std::function<void(QString)> callback)
|
||||
{
|
||||
callback(get_balance(accountId, isPublic));
|
||||
}
|
||||
|
||||
QString send_generic_public_transaction(
|
||||
const QStringList& accountIds,
|
||||
|
||||
@@ -13,32 +13,62 @@ Item {
|
||||
QtObject {
|
||||
property bool isWalletOpen: false
|
||||
property bool walletExists: true
|
||||
property bool completeOpenImmediately: true
|
||||
property bool createWalletFails: false
|
||||
property bool createWalletRefreshFails: false
|
||||
property bool accountRefreshFails: false
|
||||
property string walletHome: "/wallet"
|
||||
property string walletSyncStatus: "closed"
|
||||
property string walletSyncError: ""
|
||||
property int openCalls: 0
|
||||
property int createCalls: 0
|
||||
property int publicAccountCalls: 0
|
||||
property int privateAccountCalls: 0
|
||||
property int disconnectCalls: 0
|
||||
property int primaryAccountCalls: 0
|
||||
property int aliasCalls: 0
|
||||
property string primaryAccountAddress: ""
|
||||
property string primaryAccountName: ""
|
||||
property string activeNetwork: "testnet"
|
||||
property string networkStatus: "ready"
|
||||
property string assetStatus: "ready"
|
||||
property string assetError: ""
|
||||
property var assets: []
|
||||
|
||||
function openExisting() {
|
||||
openCalls++
|
||||
isWalletOpen = true
|
||||
if (completeOpenImmediately) {
|
||||
walletSyncStatus = "ready"
|
||||
isWalletOpen = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function createNewDefault(_password) {
|
||||
createCalls++
|
||||
if (createWalletFails)
|
||||
return ""
|
||||
isWalletOpen = true
|
||||
walletSyncStatus = createWalletRefreshFails ? "error" : "ready"
|
||||
walletSyncError = createWalletRefreshFails ? "read_failed" : ""
|
||||
return "alpha beta gamma"
|
||||
}
|
||||
|
||||
function createAccountPublic() {
|
||||
publicAccountCalls++
|
||||
if (accountRefreshFails) {
|
||||
walletSyncStatus = "error"
|
||||
walletSyncError = "read_failed"
|
||||
}
|
||||
return "a".repeat(64)
|
||||
}
|
||||
|
||||
function createAccountPrivate() {
|
||||
privateAccountCalls++
|
||||
if (accountRefreshFails) {
|
||||
walletSyncStatus = "error"
|
||||
walletSyncError = "read_failed"
|
||||
}
|
||||
return "b".repeat(64)
|
||||
}
|
||||
|
||||
@@ -46,6 +76,17 @@ Item {
|
||||
disconnectCalls++
|
||||
isWalletOpen = false
|
||||
}
|
||||
|
||||
function setPrimaryAccount(address) {
|
||||
primaryAccountCalls++
|
||||
primaryAccountAddress = address
|
||||
return true
|
||||
}
|
||||
|
||||
function setAccountAlias(_address, _alias) {
|
||||
aliasCalls++
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +156,7 @@ Item {
|
||||
const model = createTemporaryObject(modelComponent, root)
|
||||
verify(model, "Account model exists")
|
||||
for (const account of accounts || [])
|
||||
model.append(account)
|
||||
model.append(accountData(account))
|
||||
const control = createTemporaryObject(controlComponent, root, {
|
||||
wallet: backend,
|
||||
accountModel: model
|
||||
@@ -124,6 +165,24 @@ Item {
|
||||
return { backend, model, control }
|
||||
}
|
||||
|
||||
function accountData(account) {
|
||||
return {
|
||||
name: account.name || "Account",
|
||||
alias: account.alias || "",
|
||||
address: account.address || "",
|
||||
displayAddress: account.displayAddress || account.address || "",
|
||||
balance: account.balance || "0",
|
||||
isPublic: account.isPublic === true,
|
||||
kind: account.kind || (account.isPublic === false ? "private" : "user"),
|
||||
section: account.section || "accounts",
|
||||
programName: account.programName || "",
|
||||
accountType: account.accountType || "",
|
||||
visibility: account.visibility || (account.isPublic === false ? "private" : "public"),
|
||||
canBePrimary: account.canBePrimary === undefined ? true : account.canBePrimary,
|
||||
isPrimary: account.isPrimary === true
|
||||
}
|
||||
}
|
||||
|
||||
function test_opensExistingWallet() {
|
||||
const fixture = createControl({ walletExists: true }, [])
|
||||
const connectButton = findChild(fixture.control, "walletConnectButton")
|
||||
@@ -133,6 +192,81 @@ 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)
|
||||
compare(fixture.backend.openCalls, 1)
|
||||
verify(fixture.control.busy)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
function test_requiresSeedBackupAcknowledgement() {
|
||||
const fixture = createControl({ walletExists: false }, [])
|
||||
mouseClick(findChild(fixture.control, "walletConnectButton"))
|
||||
@@ -165,6 +299,42 @@ Item {
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_showsWalletCreationFailure() {
|
||||
const fixture = createControl({ walletExists: false, createWalletFails: true }, [])
|
||||
mouseClick(findChild(fixture.control, "walletConnectButton"))
|
||||
const dialog = findChild(fixture.control, "createWalletDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
findChild(dialog, "walletPasswordField").text = "secret"
|
||||
findChild(dialog, "walletConfirmPasswordField").text = "secret"
|
||||
findChild(dialog, "createWalletButton").clicked()
|
||||
compare(fixture.backend.createCalls, 1)
|
||||
compare(dialog.mnemonic, "")
|
||||
compare(dialog.errorText, "Wallet could not be created.")
|
||||
verify(dialog.opened)
|
||||
}
|
||||
|
||||
function test_warnsWhenCreatedWalletCannotRefresh() {
|
||||
const fixture = createControl({
|
||||
walletExists: false,
|
||||
createWalletRefreshFails: true
|
||||
}, [])
|
||||
mouseClick(findChild(fixture.control, "walletConnectButton"))
|
||||
const dialog = findChild(fixture.control, "createWalletDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
findChild(dialog, "walletPasswordField").text = "secret"
|
||||
findChild(dialog, "walletConfirmPasswordField").text = "secret"
|
||||
findChild(dialog, "createWalletButton").clicked()
|
||||
tryCompare(dialog, "mnemonic", "alpha beta gamma")
|
||||
const message = findChild(fixture.control, "walletMessageDialog")
|
||||
verify(!message.opened)
|
||||
mouseClick(findChild(dialog, "walletBackupAcknowledgement"))
|
||||
mouseClick(findChild(dialog, "walletContinueButton"))
|
||||
|
||||
tryCompare(message, "opened", true)
|
||||
compare(message.message,
|
||||
"Wallet was created, but could not be refreshed. Reconnect the wallet to refresh it.")
|
||||
}
|
||||
|
||||
function test_clampsSelectionAndDisconnectsLocally() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true },
|
||||
@@ -173,12 +343,12 @@ Item {
|
||||
fixture.control.selectedIndex = 1
|
||||
compare(fixture.control.selectedAddress, "b".repeat(64))
|
||||
fixture.model.clear()
|
||||
tryCompare(fixture.control, "selectedIndex", 0)
|
||||
tryCompare(fixture.control, "selectedIndex", -1)
|
||||
compare(fixture.control.selectedAddress, "")
|
||||
|
||||
fixture.model.append({
|
||||
fixture.model.append(accountData({
|
||||
name: "One", address: "a".repeat(64), balance: "10", isPublic: true
|
||||
})
|
||||
}))
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
const disconnectButton = findChild(fixture.control, "walletDisconnectButton")
|
||||
tryVerify(function() { return disconnectButton.visible })
|
||||
@@ -226,8 +396,20 @@ Item {
|
||||
|
||||
function test_selectsAccount() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true },
|
||||
{ name: "Two", address: "b".repeat(64), balance: "20", isPublic: false }
|
||||
{
|
||||
name: "One",
|
||||
address: "a".repeat(64),
|
||||
displayAddress: "base58-one",
|
||||
balance: "10",
|
||||
isPublic: true
|
||||
},
|
||||
{
|
||||
name: "Two",
|
||||
address: "b".repeat(64),
|
||||
displayAddress: "base58-two",
|
||||
balance: "20",
|
||||
isPublic: false
|
||||
}
|
||||
])
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
const accountsButton = findChild(fixture.control, "walletAccountsButton")
|
||||
@@ -240,7 +422,95 @@ Item {
|
||||
const secondAccount = accountList.itemAtIndex(1)
|
||||
secondAccount.clicked()
|
||||
tryCompare(fixture.control, "selectedIndex", 1)
|
||||
compare(fixture.backend.primaryAccountAddress, "b".repeat(64))
|
||||
compare(fixture.control.selectedAddress, "b".repeat(64))
|
||||
compare(fixture.control.selectedDisplayAddress, "base58-two")
|
||||
}
|
||||
|
||||
function test_accountNavigationKeepsOverviewInsidePopup() {
|
||||
const assets = []
|
||||
for (let index = 0; index < 10; ++index) {
|
||||
assets.push({
|
||||
name: "Token " + index,
|
||||
balance: "100",
|
||||
definitionId: "c".repeat(64),
|
||||
displayDefinitionId: "base58-token-" + index,
|
||||
status: "ready",
|
||||
section: "assets"
|
||||
})
|
||||
}
|
||||
const fixture = createControl({ isWalletOpen: true, assets: assets }, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
|
||||
])
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
const stack = findChild(fixture.control, "walletStack")
|
||||
verify(stack, "Wallet stack exists")
|
||||
verify(stack.clip, "Wallet pages are clipped to the popup")
|
||||
mouseClick(findChild(fixture.control, "walletAccountsButton"))
|
||||
tryCompare(stack, "busy", false)
|
||||
compare(stack.depth, 2)
|
||||
|
||||
mouseClick(findChild(fixture.control, "walletAccountsBackButton"))
|
||||
tryCompare(stack, "busy", false)
|
||||
compare(stack.depth, 1)
|
||||
compare(stack.currentItem.x, 0)
|
||||
const overviewContent = findChild(fixture.control, "walletOverviewContent")
|
||||
verify(overviewContent, "Wallet overview content exists")
|
||||
compare(overviewContent.mapToItem(stack, 0, 0).x, 0)
|
||||
}
|
||||
|
||||
function test_programRecordCannotBecomePrimary() {
|
||||
const userAddress = "a".repeat(64)
|
||||
const programAddress = "c".repeat(64)
|
||||
const fixture = createControl({
|
||||
isWalletOpen: true,
|
||||
primaryAccountAddress: userAddress,
|
||||
primaryAccountName: "Trading"
|
||||
}, [
|
||||
{
|
||||
name: "Trading",
|
||||
address: userAddress,
|
||||
balance: "10",
|
||||
isPublic: true,
|
||||
kind: "user",
|
||||
isPrimary: true
|
||||
},
|
||||
{
|
||||
name: "Token definition",
|
||||
address: programAddress,
|
||||
balance: "0",
|
||||
isPublic: true,
|
||||
kind: "token_definition",
|
||||
section: "advanced",
|
||||
programName: "Token",
|
||||
accountType: "TokenDefinition",
|
||||
canBePrimary: false
|
||||
}
|
||||
])
|
||||
compare(fixture.control.selectedAddress, userAddress)
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
mouseClick(findChild(fixture.control, "walletAccountsButton"))
|
||||
mouseClick(findChild(fixture.control, "walletAdvancedAccountsButton"))
|
||||
const list = findChild(fixture.control, "walletAccountList")
|
||||
tryVerify(function() { return list.itemAtIndex(1) !== null })
|
||||
list.itemAtIndex(1).clicked()
|
||||
compare(fixture.backend.primaryAccountCalls, 0)
|
||||
compare(fixture.control.selectedAddress, userAddress)
|
||||
}
|
||||
|
||||
function test_onlyProgramRecordsLeavesPrimaryEmpty() {
|
||||
const fixture = createControl({ isWalletOpen: true }, [{
|
||||
name: "Token definition",
|
||||
address: "c".repeat(64),
|
||||
balance: "0",
|
||||
isPublic: true,
|
||||
kind: "token_definition",
|
||||
section: "advanced",
|
||||
canBePrimary: false
|
||||
}])
|
||||
compare(fixture.control.selectedIndex, -1)
|
||||
compare(fixture.control.selectedAddress, "")
|
||||
compare(fixture.control.primaryName, "")
|
||||
}
|
||||
|
||||
function test_createsAccount() {
|
||||
@@ -262,6 +532,47 @@ Item {
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_createsPrivateAccount() {
|
||||
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 addButton = findChild(fixture.control, "walletAddAccountButton")
|
||||
tryVerify(function() { return addButton.visible })
|
||||
addButton.clicked()
|
||||
const dialog = findChild(fixture.control, "createAccountDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
mouseClick(findChild(dialog, "privateAccountSwitch"))
|
||||
findChild(dialog, "createAccountButton").clicked()
|
||||
compare(fixture.backend.privateAccountCalls, 1)
|
||||
tryCompare(dialog, "opened", false)
|
||||
}
|
||||
|
||||
function test_warnsWhenCreatedAccountCannotRefresh() {
|
||||
const fixture = createControl({
|
||||
isWalletOpen: true,
|
||||
walletSyncStatus: "ready",
|
||||
accountRefreshFails: true
|
||||
}, [
|
||||
{ name: "One", address: "a".repeat(64), balance: "10", isPublic: true }
|
||||
])
|
||||
mouseClick(findChild(fixture.control, "walletAccountButton"))
|
||||
mouseClick(findChild(fixture.control, "walletAccountsButton"))
|
||||
const addButton = findChild(fixture.control, "walletAddAccountButton")
|
||||
tryVerify(function() { return addButton.visible })
|
||||
addButton.clicked()
|
||||
const dialog = findChild(fixture.control, "createAccountDialog")
|
||||
tryCompare(dialog, "opened", true)
|
||||
findChild(dialog, "createAccountButton").clicked()
|
||||
tryCompare(dialog, "opened", false)
|
||||
|
||||
const message = findChild(fixture.control, "walletMessageDialog")
|
||||
tryCompare(message, "opened", true)
|
||||
compare(message.message,
|
||||
"Account was created, but could not be refreshed. Reconnect the wallet to refresh it.")
|
||||
}
|
||||
|
||||
function test_compactLayoutHasStableWidth() {
|
||||
const fixture = createControl({ isWalletOpen: false }, [])
|
||||
fixture.control.viewportWidth = 480
|
||||
@@ -300,12 +611,12 @@ Item {
|
||||
const model = createTemporaryObject(modelComponent, root)
|
||||
verify(backend && model, "Wallet fixture exists")
|
||||
for (let index = 0; index < 10; ++index) {
|
||||
model.append({
|
||||
model.append(accountData({
|
||||
name: "Account " + index,
|
||||
address: String(index).repeat(64),
|
||||
balance: String(index),
|
||||
isPublic: true
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const window = createTemporaryObject(compactWindowComponent, root)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class FakeWalletProvider final : public WalletProvider {
|
||||
@@ -9,6 +11,7 @@ public:
|
||||
WalletSnapshot snapshotResult;
|
||||
WalletAccountCreation createAccountResult;
|
||||
WalletAccountRead readResult;
|
||||
QVector<WalletAccountRead> readResults;
|
||||
WalletSubmission submissionResult;
|
||||
|
||||
int connectCalls = 0;
|
||||
@@ -31,6 +34,13 @@ public:
|
||||
return connectResult;
|
||||
}
|
||||
|
||||
void connectAsync(const WalletPaths& paths, SessionCallback callback) override
|
||||
{
|
||||
++connectCalls;
|
||||
lastPaths = paths;
|
||||
callback(connectResult);
|
||||
}
|
||||
|
||||
WalletCreation createWallet(const WalletPaths& paths,
|
||||
const QString&) override
|
||||
{
|
||||
@@ -46,6 +56,13 @@ public:
|
||||
return snapshotResult;
|
||||
}
|
||||
|
||||
void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override
|
||||
{
|
||||
++snapshotCalls;
|
||||
lastForceRefresh = forceRefresh;
|
||||
callback(snapshotResult);
|
||||
}
|
||||
|
||||
void clearSnapshot() override { ++clearCalls; }
|
||||
|
||||
WalletAccountCreation createAccount(bool isPublic) override
|
||||
@@ -63,6 +80,21 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
void readPublicAccountsAsync(const QStringList& accountIds,
|
||||
AccountReadsCallback callback) override
|
||||
{
|
||||
QVector<WalletAccountRead> results = readResults;
|
||||
if (results.isEmpty()) {
|
||||
results.reserve(accountIds.size());
|
||||
for (const QString& accountId : accountIds) {
|
||||
WalletAccountRead result = readResult;
|
||||
result.accountId = accountId;
|
||||
results.append(std::move(result));
|
||||
}
|
||||
}
|
||||
callback(std::move(results));
|
||||
}
|
||||
|
||||
WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) override
|
||||
{
|
||||
|
||||
Generated
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1779041105,
|
||||
"narHash": "sha256-nnGD2f8OlAZT2i5OfwikJsw+ifWfiA4d6A8BWlgOXV0=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "10e6e3cb966f7cfcc789fe5eee7a85f3188ce08b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"ref": "v0.23.4",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1782841183,
|
||||
"narHash": "sha256-Ndt/5R7UN4rBdhFR1lxHZZZ42cD6vGlnuxC2VvvsKE4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c9bfd86ed684d27e63b0ff9ebb18699f84f27a3b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
description = "LEZ program client libraries";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
|
||||
crane.url = "github:ipetkov/crane/v0.23.4";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, crane, ... }:
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
in {
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
craneLib = crane.mkLib pkgs;
|
||||
src = craneLib.cleanCargoSource ./.;
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
pname = "wallet-idl-decoder";
|
||||
version = "0.1.0";
|
||||
strictDeps = true;
|
||||
cargoExtraArgs = "-p wallet-idl-decoder";
|
||||
};
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
decoder = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
doCheck = false;
|
||||
postInstall = ''
|
||||
install -Dm644 ${./tools/wallet-idl-decoder/include/wallet_idl_decoder.h} \
|
||||
$out/include/wallet_idl_decoder.h
|
||||
'';
|
||||
});
|
||||
in {
|
||||
default = decoder;
|
||||
wallet_idl_decoder = decoder;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "wallet-idl-decoder"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
name = "wallet_idl_decoder"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
base58 = "0.2"
|
||||
hex = "0.4"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
spel-framework-core = { git = "https://github.com/logos-co/spel.git", tag = "v0.6.0" }
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef WALLET_IDL_DECODER_H
|
||||
#define WALLET_IDL_DECODER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
char *wallet_idl_decode_accounts(const char *request_json);
|
||||
void wallet_idl_decoder_free(char *value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,278 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ffi::{CStr, CString},
|
||||
os::raw::c_char,
|
||||
panic::{catch_unwind, AssertUnwindSafe},
|
||||
};
|
||||
|
||||
use base58::FromBase58;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use spel_framework_core::{decode::decode_account_data_try_all, idl::SpelIdl};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DecodeRequest {
|
||||
idl: SpelIdl,
|
||||
accounts: Vec<AccountInput>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AccountInput {
|
||||
id: String,
|
||||
data_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DecodeResponse {
|
||||
status: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<&'static str>,
|
||||
accounts: Vec<AccountOutput>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AccountOutput {
|
||||
id: String,
|
||||
status: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
type_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
value: Option<Value>,
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
account_ids: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
fn decode_request(request: DecodeRequest) -> DecodeResponse {
|
||||
let accounts = request
|
||||
.accounts
|
||||
.into_iter()
|
||||
.map(|account| decode_account(account, &request.idl))
|
||||
.collect();
|
||||
DecodeResponse {
|
||||
status: "ok",
|
||||
error: None,
|
||||
accounts,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_account(account: AccountInput, idl: &SpelIdl) -> AccountOutput {
|
||||
let Ok(data) = hex::decode(&account.data_hex) else {
|
||||
return AccountOutput {
|
||||
id: account.id,
|
||||
status: "invalid_data",
|
||||
type_name: None,
|
||||
value: None,
|
||||
account_ids: BTreeMap::new(),
|
||||
};
|
||||
};
|
||||
let Some((type_name, value)) = decode_account_data_try_all(&data, idl) else {
|
||||
return AccountOutput {
|
||||
id: account.id,
|
||||
status: "unknown_type",
|
||||
type_name: None,
|
||||
value: None,
|
||||
account_ids: BTreeMap::new(),
|
||||
};
|
||||
};
|
||||
let mut account_ids = BTreeMap::new();
|
||||
collect_account_ids(&value, &mut account_ids);
|
||||
AccountOutput {
|
||||
id: account.id,
|
||||
status: "decoded",
|
||||
type_name: Some(type_name),
|
||||
value: Some(value),
|
||||
account_ids,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_account_ids(value: &Value, output: &mut BTreeMap<String, String>) {
|
||||
match value {
|
||||
Value::String(encoded) => {
|
||||
let Some(base58) = encoded.strip_prefix("Public/") else {
|
||||
return;
|
||||
};
|
||||
if let Ok(bytes) = base58.from_base58() {
|
||||
if bytes.len() == 32 {
|
||||
output.insert(encoded.clone(), hex::encode(bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for nested in values {
|
||||
collect_account_ids(nested, output);
|
||||
}
|
||||
}
|
||||
Value::Object(values) => {
|
||||
for nested in values.values() {
|
||||
collect_account_ids(nested, output);
|
||||
}
|
||||
}
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(error: &'static str) -> DecodeResponse {
|
||||
DecodeResponse {
|
||||
status: "error",
|
||||
error: Some(error),
|
||||
accounts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn response_pointer(response: &DecodeResponse) -> *mut c_char {
|
||||
let json = serde_json::to_string(response).unwrap_or_else(|_| {
|
||||
String::from(r#"{"status":"error","error":"serialization_failed","accounts":[]}"#)
|
||||
});
|
||||
CString::new(json).map_or(std::ptr::null_mut(), CString::into_raw)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
unsafe_code,
|
||||
reason = "C ABI input requires reading a caller-owned C string"
|
||||
)]
|
||||
fn decode_pointer(request_json: *const c_char) -> DecodeResponse {
|
||||
if request_json.is_null() {
|
||||
return error_response("null_request");
|
||||
}
|
||||
let bytes = unsafe {
|
||||
// SAFETY: Caller owns a non-null NUL-terminated C string for this call.
|
||||
CStr::from_ptr(request_json)
|
||||
};
|
||||
let Ok(json) = bytes.to_str() else {
|
||||
return error_response("invalid_utf8");
|
||||
};
|
||||
match serde_json::from_str::<DecodeRequest>(json) {
|
||||
Ok(request) => decode_request(request),
|
||||
Err(_) => error_response("invalid_request"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a JSON batch request using its embedded SPEL IDL.
|
||||
///
|
||||
/// Returns a library-owned JSON C string. Release it with
|
||||
/// [`wallet_idl_decoder_free`].
|
||||
#[no_mangle]
|
||||
#[expect(unsafe_code, reason = "C ABI requires a stable exported symbol")]
|
||||
pub extern "C" fn wallet_idl_decode_accounts(request_json: *const c_char) -> *mut c_char {
|
||||
let response = catch_unwind(AssertUnwindSafe(|| decode_pointer(request_json)))
|
||||
.unwrap_or_else(|_| error_response("panic"));
|
||||
response_pointer(&response)
|
||||
}
|
||||
|
||||
/// Frees a response allocated by [`wallet_idl_decode_accounts`].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `value` must be null or a pointer returned by this library that has not
|
||||
/// already been freed.
|
||||
#[no_mangle]
|
||||
#[expect(unsafe_code, reason = "C ABI deallocator reconstructs its CString")]
|
||||
pub unsafe extern "C" fn wallet_idl_decoder_free(value: *mut c_char) {
|
||||
if !value.is_null() {
|
||||
unsafe {
|
||||
// SAFETY: Pointer must come from CString::into_raw in this library and be freed once.
|
||||
drop(CString::from_raw(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn token_idl() -> SpelIdl {
|
||||
match serde_json::from_str(include_str!("../../../artifacts/token-idl.json")) {
|
||||
Ok(idl) => idl,
|
||||
Err(error) => panic!("committed token IDL should parse: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_fungible_definition() {
|
||||
let request = DecodeRequest {
|
||||
idl: token_idl(),
|
||||
accounts: vec![AccountInput {
|
||||
id: "definition".to_owned(),
|
||||
data_hex: concat!(
|
||||
"00", // Fungible variant
|
||||
"04000000",
|
||||
"54455354", // TEST
|
||||
"0a000000000000000000000000000000", // supply 10
|
||||
"00", // metadata_id None
|
||||
"00" // authority None
|
||||
)
|
||||
.to_owned(),
|
||||
}],
|
||||
};
|
||||
let response = decode_request(request);
|
||||
let Some(account) = response.accounts.first() else {
|
||||
panic!("decoder should return one account");
|
||||
};
|
||||
assert_eq!(account.status, "decoded");
|
||||
assert_eq!(account.type_name.as_deref(), Some("TokenDefinition"));
|
||||
assert_eq!(
|
||||
account
|
||||
.value
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("Fungible"))
|
||||
.and_then(|value| value.get("name"))
|
||||
.and_then(Value::as_str),
|
||||
Some("TEST")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_decoded_public_ids_to_hex() {
|
||||
let request = DecodeRequest {
|
||||
idl: token_idl(),
|
||||
accounts: vec![AccountInput {
|
||||
id: "holding".to_owned(),
|
||||
data_hex: format!("00{}19000000000000000000000000000000", "01".repeat(32)),
|
||||
}],
|
||||
};
|
||||
let response = decode_request(request);
|
||||
let Some(account) = response.accounts.first() else {
|
||||
panic!("decoder should return one account");
|
||||
};
|
||||
let expected = "01".repeat(32);
|
||||
assert_eq!(account.status, "decoded");
|
||||
assert_eq!(account.type_name.as_deref(), Some("TokenHolding"));
|
||||
assert_eq!(
|
||||
account.account_ids.values().next().map(String::as_str),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_hex_per_account() {
|
||||
let response = decode_request(DecodeRequest {
|
||||
idl: token_idl(),
|
||||
accounts: vec![AccountInput {
|
||||
id: "broken".to_owned(),
|
||||
data_hex: "xyz".to_owned(),
|
||||
}],
|
||||
});
|
||||
assert_eq!(response.status, "ok");
|
||||
assert_eq!(
|
||||
response.accounts.first().map(|account| account.status),
|
||||
Some("invalid_data")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(unsafe_code, reason = "test verifies the exported C allocator pair")]
|
||||
fn ffi_allocates_json_and_accepts_its_pointer_on_free() {
|
||||
let response = wallet_idl_decode_accounts(std::ptr::null());
|
||||
assert!(!response.is_null());
|
||||
let json = match unsafe { CStr::from_ptr(response) }.to_str() {
|
||||
Ok(json) => json,
|
||||
Err(error) => panic!("response should be UTF-8 JSON: {error}"),
|
||||
};
|
||||
assert!(json.contains("null_request"));
|
||||
unsafe { wallet_idl_decoder_free(response) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user