From 7b4738040e9d9cbb4b260b3d4934160c5647557a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 18 Aug 2025 17:29:59 -0500 Subject: [PATCH 1/3] feat(monero): add Monero stagenet to Stack Wallet and Stack Duo --- lib/wallets/crypto_currency/coins/monero.dart | 26 ++++++++++++++ .../crypto_currency/crypto_currency.dart | 4 ++- lib/wallets/wallet/impl/monero_wallet.dart | 14 +++++++- .../intermediate/lib_monero_wallet.dart | 34 +++++++++++++++++-- .../interfaces/cs_monero_interface.dart | 4 +++ scripts/app_config/configure_stack_duo.sh | 1 + scripts/app_config/configure_stack_wallet.sh | 1 + 7 files changed, 80 insertions(+), 4 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 379c47d702..fd45dde740 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -14,6 +14,10 @@ class Monero extends CryptonoteCurrency { _id = _idMain; _name = "Monero"; _ticker = "XMR"; + case CryptoCurrencyNetwork.stage: + _id = "${_idMain}Stagenet"; + _name = "sMonero"; + _ticker = "sXMR"; default: throw Exception("Unsupported network: $network"); } @@ -53,6 +57,8 @@ class Monero extends CryptonoteCurrency { switch (network) { case CryptoCurrencyNetwork.main: return csMonero.validateAddress(address, 0); + case CryptoCurrencyNetwork.stage: + return csMonero.validateAddress(address, 2); default: throw Exception("Unsupported network: $network"); } @@ -78,6 +84,24 @@ class Monero extends CryptonoteCurrency { isPrimary: isPrimary, ); + case CryptoCurrencyNetwork.stage: + // Public third-party clearnet stagenet node; not auto-trusted. + return NodeModel( + host: "http://node3.monerodevs.org", + port: 38089, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: false, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + trusted: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: isPrimary, + ); + default: throw UnimplementedError(); } @@ -114,6 +138,8 @@ class Monero extends CryptonoteCurrency { switch (network) { case CryptoCurrencyNetwork.main: return Uri.parse("https://xmrchain.net/tx/$txid"); + case CryptoCurrencyNetwork.stage: + return Uri.parse("https://stagenet.xmrchain.net/tx/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 8d02b130c0..491def32dd 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -35,7 +35,9 @@ enum CryptoCurrencyNetwork { test4; bool get isTestNet => - this == CryptoCurrencyNetwork.test || this == CryptoCurrencyNetwork.test4; + this == CryptoCurrencyNetwork.test || + this == CryptoCurrencyNetwork.test4 || + this == CryptoCurrencyNetwork.stage; } abstract class CryptoCurrency { diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index 935d5ad3aa..ab011e7767 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -41,7 +41,13 @@ class MoneroWallet extends LibMoneroWallet { Future loadWallet({ required String path, required String password, - }) => csMonero.loadWallet(walletId, path: path, password: password); + int network = 0, + }) => csMonero.loadWallet( + walletId, + path: path, + password: password, + network: network, + ); @override Future getCreatedWallet({ @@ -49,11 +55,13 @@ class MoneroWallet extends LibMoneroWallet { required String password, required int wordCount, required String seedOffset, + int network = 0, }) => csMonero.getCreatedWallet( path: path, password: password, wordCount: wordCount, seedOffset: seedOffset, + network: network, ); @override @@ -62,6 +70,7 @@ class MoneroWallet extends LibMoneroWallet { required String password, required String mnemonic, required String seedOffset, + int network = 0, int height = 0, }) => csMonero.getRestoredWallet( path: path, @@ -69,6 +78,7 @@ class MoneroWallet extends LibMoneroWallet { mnemonic: mnemonic, height: height, seedOffset: seedOffset, + network: network, walletId: walletId, ); @@ -78,6 +88,7 @@ class MoneroWallet extends LibMoneroWallet { required String password, required String address, required String privateViewKey, + int network = 0, int height = 0, }) => csMonero.getRestoredFromViewKeyWallet( walletId: walletId, @@ -85,6 +96,7 @@ class MoneroWallet extends LibMoneroWallet { password: password, address: address, privateViewKey: privateViewKey, + network: network, height: height, ); diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 6c0c49884e..78f73c0135 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -37,6 +37,7 @@ import '../../../utilities/stack_file_system.dart'; import '../../../wl_gen/interfaces/cs_monero_interface.dart'; import '../../../wl_gen/interfaces/cs_salvium_interface.dart' show WrappedWallet; +import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/intermediate/cryptonote_currency.dart'; import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; @@ -110,6 +111,20 @@ abstract class LibMoneroWallet final lib_monero_compat.WalletType compatType; + /// Maps CryptoCurrencyNetwork to monero_c network type integer. + int getNetworkType() { + switch (cryptoCurrency.network) { + case CryptoCurrencyNetwork.main: + return 0; + case CryptoCurrencyNetwork.test: + return 1; + case CryptoCurrencyNetwork.stage: + return 2; + default: + throw Exception("Unsupported network: ${cryptoCurrency.network}"); + } + } + lib_monero_compat.SyncStatus? get syncStatus => _syncStatus; lib_monero_compat.SyncStatus? _syncStatus; int _syncedCount = 0; @@ -138,6 +153,7 @@ abstract class LibMoneroWallet Future loadWallet({ required String path, required String password, + int network = 0, }); Future getCreatedWallet({ @@ -145,6 +161,7 @@ abstract class LibMoneroWallet required String password, required int wordCount, required String seedOffset, + int network = 0, }); Future getRestoredWallet({ @@ -152,6 +169,7 @@ abstract class LibMoneroWallet required String password, required String mnemonic, required String seedOffset, + int network = 0, int height = 0, }); @@ -160,6 +178,7 @@ abstract class LibMoneroWallet required String password, required String address, required String privateViewKey, + int network = 0, int height = 0, }); @@ -209,7 +228,11 @@ abstract class LibMoneroWallet throw Exception("Password not found $e, $s"); } - wallet = await loadWallet(path: path, password: password); + wallet = await loadWallet( + path: path, + password: password, + network: getNetworkType(), + ); _setListener(); @@ -329,7 +352,11 @@ abstract class LibMoneroWallet } catch (e, s) { throw Exception("Password not found $e, $s"); } - wallet = await loadWallet(path: path, password: password); + wallet = await loadWallet( + path: path, + password: password, + network: getNetworkType(), + ); return ( await csMonero.getAddress(wallet!), await csMonero.getPrivateViewKey(wallet!), @@ -355,6 +382,7 @@ abstract class LibMoneroWallet password: password, wordCount: wordCount, seedOffset: "", // default for non restored wallets for now + network: getNetworkType(), ); await info.updateRestoreHeight( @@ -438,6 +466,7 @@ abstract class LibMoneroWallet mnemonic: mnemonic, height: height, seedOffset: seedOffset, + network: getNetworkType(), ); if (this.wallet != null) { @@ -1575,6 +1604,7 @@ abstract class LibMoneroWallet address: data.address, privateViewKey: data.privateViewKey, height: height, + network: getNetworkType(), ); if (this.wallet == null) { diff --git a/lib/wl_gen/interfaces/cs_monero_interface.dart b/lib/wl_gen/interfaces/cs_monero_interface.dart index f9f30d5c83..3223faee54 100644 --- a/lib/wl_gen/interfaces/cs_monero_interface.dart +++ b/lib/wl_gen/interfaces/cs_monero_interface.dart @@ -25,6 +25,7 @@ abstract class CsMoneroInterface { String walletId, { required String path, required String password, + int network = 0, }); Future getAddress( @@ -38,6 +39,7 @@ abstract class CsMoneroInterface { required String password, required int wordCount, required String seedOffset, + int network = 0, }); Future getRestoredWallet({ @@ -46,6 +48,7 @@ abstract class CsMoneroInterface { required String password, required String mnemonic, required String seedOffset, + int network = 0, int height = 0, }); @@ -55,6 +58,7 @@ abstract class CsMoneroInterface { required String password, required String address, required String privateViewKey, + int network = 0, int height = 0, }); diff --git a/scripts/app_config/configure_stack_duo.sh b/scripts/app_config/configure_stack_duo.sh index 502e1a056d..c7a7921233 100755 --- a/scripts/app_config/configure_stack_duo.sh +++ b/scripts/app_config/configure_stack_duo.sh @@ -83,6 +83,7 @@ final List _supportedCoins = List.unmodifiable([ Bitcoin(CryptoCurrencyNetwork.test4), BitcoinFrost(CryptoCurrencyNetwork.test), BitcoinFrost(CryptoCurrencyNetwork.test4), + Monero(CryptoCurrencyNetwork.stage), ]); final ({String from, String fromFuzzyNet, String to, String toFuzzyNet}) diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index 03d9562692..7e1c989583 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -138,6 +138,7 @@ final List _supportedCoins = List.unmodifiable([ Salvium(CryptoCurrencyNetwork.test), Stellar(CryptoCurrencyNetwork.test), Xelis(CryptoCurrencyNetwork.test), + Monero(CryptoCurrencyNetwork.stage), ]); final ({String from, String fromFuzzyNet, String to, String toFuzzyNet}) From 04c71972be27909d2f34e5e12f1d9dd935458bd3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 28 May 2026 11:49:42 -0500 Subject: [PATCH 2/3] chore: dart format --- lib/wallets/crypto_currency/crypto_currency.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 491def32dd..cc4595c533 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -35,7 +35,7 @@ enum CryptoCurrencyNetwork { test4; bool get isTestNet => - this == CryptoCurrencyNetwork.test || + this == CryptoCurrencyNetwork.test || this == CryptoCurrencyNetwork.test4 || this == CryptoCurrencyNetwork.stage; } From acee477a86d80d84659dc676eb2284c278c65202 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 22 Aug 2026 06:43:02 -0500 Subject: [PATCH 3/3] fix(monero): harden stagenet support --- lib/wallets/crypto_currency/coins/monero.dart | 13 +++- lib/wallets/crypto_currency/coins/xelis.dart | 2 +- lib/wallets/wallet/impl/monero_wallet.dart | 8 +-- .../intermediate/lib_monero_wallet.dart | 24 ++----- .../interfaces/cs_monero_interface.dart | 8 +-- .../crypto_currency/monero_stagenet_test.dart | 63 +++++++++++++++++++ 6 files changed, 88 insertions(+), 30 deletions(-) create mode 100644 test/wallets/crypto_currency/monero_stagenet_test.dart diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index fd45dde740..5218a25dd6 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -5,6 +5,13 @@ import '../../../wl_gen/interfaces/cs_monero_interface.dart'; import '../crypto_currency.dart'; import '../intermediate/cryptonote_currency.dart'; +int moneroNetworkType(CryptoCurrencyNetwork network) => switch (network) { + CryptoCurrencyNetwork.main => 0, + CryptoCurrencyNetwork.test => 1, + CryptoCurrencyNetwork.stage => 2, + _ => throw ArgumentError.value(network, "network", "Unsupported network"), +}; + class Monero extends CryptonoteCurrency { Monero(super.network) { _idMain = "monero"; @@ -56,9 +63,8 @@ class Monero extends CryptonoteCurrency { } switch (network) { case CryptoCurrencyNetwork.main: - return csMonero.validateAddress(address, 0); case CryptoCurrencyNetwork.stage: - return csMonero.validateAddress(address, 2); + return csMonero.validateAddress(address, moneroNetworkType(network)); default: throw Exception("Unsupported network: $network"); } @@ -85,7 +91,8 @@ class Monero extends CryptonoteCurrency { ); case CryptoCurrencyNetwork.stage: - // Public third-party clearnet stagenet node; not auto-trusted. + // Public stagenet RPC; the wallet treats the remote daemon as + // untrusted. return NodeModel( host: "http://node3.monerodevs.org", port: 38089, diff --git a/lib/wallets/crypto_currency/coins/xelis.dart b/lib/wallets/crypto_currency/coins/xelis.dart index d022082021..2d946bca62 100644 --- a/lib/wallets/crypto_currency/coins/xelis.dart +++ b/lib/wallets/crypto_currency/coins/xelis.dart @@ -83,7 +83,7 @@ class Xelis extends ElectrumCurrency { isPrimary: isPrimary, ); - case CryptoCurrencyNetwork.test: + case CryptoCurrencyNetwork.stage: return NodeModel( host: "stagenet-node.xelis.io", port: 443, diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index ab011e7767..2be1575597 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -41,7 +41,7 @@ class MoneroWallet extends LibMoneroWallet { Future loadWallet({ required String path, required String password, - int network = 0, + required int network, }) => csMonero.loadWallet( walletId, path: path, @@ -55,7 +55,7 @@ class MoneroWallet extends LibMoneroWallet { required String password, required int wordCount, required String seedOffset, - int network = 0, + required int network, }) => csMonero.getCreatedWallet( path: path, password: password, @@ -70,7 +70,7 @@ class MoneroWallet extends LibMoneroWallet { required String password, required String mnemonic, required String seedOffset, - int network = 0, + required int network, int height = 0, }) => csMonero.getRestoredWallet( path: path, @@ -88,7 +88,7 @@ class MoneroWallet extends LibMoneroWallet { required String password, required String address, required String privateViewKey, - int network = 0, + required int network, int height = 0, }) => csMonero.getRestoredFromViewKeyWallet( walletId: walletId, diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 78f73c0135..80e68b5ff0 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -37,7 +37,7 @@ import '../../../utilities/stack_file_system.dart'; import '../../../wl_gen/interfaces/cs_monero_interface.dart'; import '../../../wl_gen/interfaces/cs_salvium_interface.dart' show WrappedWallet; -import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/coins/monero.dart'; import '../../crypto_currency/intermediate/cryptonote_currency.dart'; import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; @@ -111,19 +111,7 @@ abstract class LibMoneroWallet final lib_monero_compat.WalletType compatType; - /// Maps CryptoCurrencyNetwork to monero_c network type integer. - int getNetworkType() { - switch (cryptoCurrency.network) { - case CryptoCurrencyNetwork.main: - return 0; - case CryptoCurrencyNetwork.test: - return 1; - case CryptoCurrencyNetwork.stage: - return 2; - default: - throw Exception("Unsupported network: ${cryptoCurrency.network}"); - } - } + int getNetworkType() => moneroNetworkType(cryptoCurrency.network); lib_monero_compat.SyncStatus? get syncStatus => _syncStatus; lib_monero_compat.SyncStatus? _syncStatus; @@ -153,7 +141,7 @@ abstract class LibMoneroWallet Future loadWallet({ required String path, required String password, - int network = 0, + required int network, }); Future getCreatedWallet({ @@ -161,7 +149,7 @@ abstract class LibMoneroWallet required String password, required int wordCount, required String seedOffset, - int network = 0, + required int network, }); Future getRestoredWallet({ @@ -169,7 +157,7 @@ abstract class LibMoneroWallet required String password, required String mnemonic, required String seedOffset, - int network = 0, + required int network, int height = 0, }); @@ -178,7 +166,7 @@ abstract class LibMoneroWallet required String password, required String address, required String privateViewKey, - int network = 0, + required int network, int height = 0, }); diff --git a/lib/wl_gen/interfaces/cs_monero_interface.dart b/lib/wl_gen/interfaces/cs_monero_interface.dart index 3223faee54..36adf8412e 100644 --- a/lib/wl_gen/interfaces/cs_monero_interface.dart +++ b/lib/wl_gen/interfaces/cs_monero_interface.dart @@ -25,7 +25,7 @@ abstract class CsMoneroInterface { String walletId, { required String path, required String password, - int network = 0, + required int network, }); Future getAddress( @@ -39,7 +39,7 @@ abstract class CsMoneroInterface { required String password, required int wordCount, required String seedOffset, - int network = 0, + required int network, }); Future getRestoredWallet({ @@ -48,7 +48,7 @@ abstract class CsMoneroInterface { required String password, required String mnemonic, required String seedOffset, - int network = 0, + required int network, int height = 0, }); @@ -58,7 +58,7 @@ abstract class CsMoneroInterface { required String password, required String address, required String privateViewKey, - int network = 0, + required int network, int height = 0, }); diff --git a/test/wallets/crypto_currency/monero_stagenet_test.dart b/test/wallets/crypto_currency/monero_stagenet_test.dart new file mode 100644 index 0000000000..0280e11874 --- /dev/null +++ b/test/wallets/crypto_currency/monero_stagenet_test.dart @@ -0,0 +1,63 @@ +import "package:flutter_test/flutter_test.dart"; +import "package:stackwallet/app_config.dart"; +import "package:stackwallet/wallets/crypto_currency/crypto_currency.dart"; + +void main() { + group("Monero stagenet", () { + final coin = Monero(CryptoCurrencyNetwork.stage); + + test("uses a distinct test-network identity", () { + expect(coin.identifier, "moneroStagenet"); + expect(coin.mainNetId, "monero"); + expect(coin.prettyName, "sMonero"); + expect(coin.ticker, "sXMR"); + expect(coin.network.isTestNet, isTrue); + expect( + AppConfig.getCryptoCurrencyFor(coin.identifier)?.network, + CryptoCurrencyNetwork.stage, + ); + }); + + test("uses the stagenet native network type", () { + expect(moneroNetworkType(CryptoCurrencyNetwork.main), 0); + expect(moneroNetworkType(CryptoCurrencyNetwork.test), 1); + expect(moneroNetworkType(CryptoCurrencyNetwork.stage), 2); + expect( + () => moneroNetworkType(CryptoCurrencyNetwork.test4), + throwsArgumentError, + ); + }); + + test("ships an enabled untrusted stagenet node", () { + final node = coin.defaultNode(isPrimary: true); + + expect(node.host, "http://node3.monerodevs.org"); + expect(node.port, 38089); + expect(node.useSSL, isFalse); + expect(node.enabled, isTrue); + expect(node.trusted, isFalse); + expect(node.torEnabled, isTrue); + expect(node.clearnetEnabled, isTrue); + expect(node.coinName, coin.identifier); + expect(node.isPrimary, isTrue); + }); + + test("uses the stagenet block explorer", () { + expect( + coin.defaultBlockExplorer("abc").toString(), + "https://stagenet.xmrchain.net/tx/abc", + ); + }); + }); + + test("Xelis stagenet has a selectable default node", () { + final coin = Xelis(CryptoCurrencyNetwork.stage); + final node = coin.defaultNode(isPrimary: false); + + expect(coin.network.isTestNet, isTrue); + expect(node.host, "stagenet-node.xelis.io"); + expect(node.port, 443); + expect(node.useSSL, isTrue); + expect(node.coinName, coin.identifier); + }); +}