diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 379c47d702..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"; @@ -14,6 +21,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"); } @@ -52,7 +63,8 @@ class Monero extends CryptonoteCurrency { } switch (network) { case CryptoCurrencyNetwork.main: - return csMonero.validateAddress(address, 0); + case CryptoCurrencyNetwork.stage: + return csMonero.validateAddress(address, moneroNetworkType(network)); default: throw Exception("Unsupported network: $network"); } @@ -78,6 +90,25 @@ class Monero extends CryptonoteCurrency { isPrimary: isPrimary, ); + case CryptoCurrencyNetwork.stage: + // Public stagenet RPC; the wallet treats the remote daemon as + // untrusted. + 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 +145,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/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/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 8d02b130c0..cc4595c533 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..2be1575597 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); + required int network, + }) => 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, + required int network, }) => 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, + required int network, 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, + required int network, 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..80e68b5ff0 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/coins/monero.dart'; import '../../crypto_currency/intermediate/cryptonote_currency.dart'; import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; @@ -110,6 +111,8 @@ abstract class LibMoneroWallet final lib_monero_compat.WalletType compatType; + int getNetworkType() => moneroNetworkType(cryptoCurrency.network); + lib_monero_compat.SyncStatus? get syncStatus => _syncStatus; lib_monero_compat.SyncStatus? _syncStatus; int _syncedCount = 0; @@ -138,6 +141,7 @@ abstract class LibMoneroWallet Future loadWallet({ required String path, required String password, + required int network, }); Future getCreatedWallet({ @@ -145,6 +149,7 @@ abstract class LibMoneroWallet required String password, required int wordCount, required String seedOffset, + required int network, }); Future getRestoredWallet({ @@ -152,6 +157,7 @@ abstract class LibMoneroWallet required String password, required String mnemonic, required String seedOffset, + required int network, int height = 0, }); @@ -160,6 +166,7 @@ abstract class LibMoneroWallet required String password, required String address, required String privateViewKey, + required int network, int height = 0, }); @@ -209,7 +216,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 +340,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 +370,7 @@ abstract class LibMoneroWallet password: password, wordCount: wordCount, seedOffset: "", // default for non restored wallets for now + network: getNetworkType(), ); await info.updateRestoreHeight( @@ -438,6 +454,7 @@ abstract class LibMoneroWallet mnemonic: mnemonic, height: height, seedOffset: seedOffset, + network: getNetworkType(), ); if (this.wallet != null) { @@ -1575,6 +1592,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..36adf8412e 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, + required int network, }); Future getAddress( @@ -38,6 +39,7 @@ abstract class CsMoneroInterface { required String password, required int wordCount, required String seedOffset, + required int network, }); Future getRestoredWallet({ @@ -46,6 +48,7 @@ abstract class CsMoneroInterface { required String password, required String mnemonic, required String seedOffset, + required int network, int height = 0, }); @@ -55,6 +58,7 @@ abstract class CsMoneroInterface { required String password, required String address, required String privateViewKey, + required int network, int height = 0, }); diff --git a/scripts/app_config/configure_stack_duo.sh b/scripts/app_config/configure_stack_duo.sh index 7d6bae012d..83c3c83092 100755 --- a/scripts/app_config/configure_stack_duo.sh +++ b/scripts/app_config/configure_stack_duo.sh @@ -80,6 +80,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 bf3d6c6621..2e3d85d0c0 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -135,6 +135,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}) 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); + }); +}