1+ /*
2+ * This file is part of Stack Wallet.
3+ *
4+ * Copyright (c) 2025 Cypher Stack
5+ * All Rights Reserved.
6+ * The code is distributed under GPLv3 license, see LICENSE file for details.
7+ * Generated by Cypher Stack on 2025-08-14
8+ *
9+ */
10+
11+ import 'dart:async' ;
12+ import 'dart:io' ;
13+ import 'dart:math' ;
14+ import 'dart:typed_data' ;
15+ import 'package:flutter/material.dart' ;
16+ import 'package:logger/logger.dart' ;
17+ import 'package:flutter_mwebd/flutter_mwebd.dart' ;
18+ import 'package:mweb_client/mweb_client.dart' ;
19+ import '../../../utilities/logger.dart' ;
20+ import '../../../utilities/stack_file_system.dart' ;
21+ import '../test_suite_interface.dart' ;
22+ import '../testing_models.dart' ;
23+
24+ class LitecoinMwebIntegrationTestSuite implements TestSuiteInterface {
25+ final StreamController <TestSuiteStatus > _statusController =
26+ StreamController <TestSuiteStatus >.broadcast ();
27+ TestSuiteStatus _status = TestSuiteStatus .waiting;
28+
29+ @override
30+ String get displayName => "Litecoin MWEB Integration" ;
31+
32+ @override
33+ Widget get icon => const Icon (Icons .currency_bitcoin, size: 32 );
34+
35+ @override
36+ TestSuiteStatus get status => _status;
37+
38+ @override
39+ Stream <TestSuiteStatus > get statusStream => _statusController.stream;
40+
41+ @override
42+ Future <TestResult > runTests () async {
43+ final stopwatch = Stopwatch ()..start ();
44+
45+ try {
46+ _updateStatus (TestSuiteStatus .running);
47+
48+ Logging .instance.log (Level .info, "Starting Litecoin MWEB integration test suite..." );
49+
50+ await _testMwebdServerCreation ();
51+ // await _testMwebdServerStatus();
52+ // await _testMwebClientConnection();
53+
54+ stopwatch.stop ();
55+ _updateStatus (TestSuiteStatus .passed);
56+
57+ return TestResult (
58+ success: true ,
59+ message: "👍👍 All Litecoin MWEB integration tests passed successfully" ,
60+ executionTime: stopwatch.elapsed,
61+ );
62+
63+ } catch (e, stackTrace) {
64+ stopwatch.stop ();
65+ _updateStatus (TestSuiteStatus .failed);
66+
67+ Logging .instance.log (Level .error,
68+ "Litecoin MWEB integration test suite failed: $e \n $stackTrace "
69+ );
70+
71+ return TestResult (
72+ success: false ,
73+ message: "Litecoin MWEB integration tests failed: $e " ,
74+ executionTime: stopwatch.elapsed,
75+ );
76+ }
77+ }
78+
79+ Future <void > _testMwebdServerCreation () async {
80+ Logging .instance.log (Level .info, "Testing MWEB server creation..." );
81+
82+ MwebdServer ? server;
83+ try {
84+ // Get a random unused port for testing.
85+ final port = await _getRandomUnusedPort ();
86+ if (port == null ) {
87+ throw Exception ("Could not find an unused port for mwebd test" );
88+ }
89+
90+ // Get test data directory.
91+ final dir = await StackFileSystem .applicationMwebdDirectory ("testnet" );
92+
93+ // Create MwebdServer instance - this tests FFI integration.
94+ server = MwebdServer (
95+ chain: "testnet" ,
96+ dataDir: dir.path,
97+ peer: "litecoin.stackwallet.com:19335" , // testnet peer
98+ proxy: "" , // no proxy for test
99+ serverPort: port,
100+ );
101+
102+ // Test server creation - this exercises the FFI bindings.
103+ await server.createServer ();
104+ await server.stopServer ();
105+
106+ Logging .instance.log (Level .info,
107+ "👍 MWEB server creation test passed"
108+ );
109+
110+ } catch (e) {
111+ throw Exception ("MWEB server creation test failed: $e " );
112+ } finally {
113+ // Cleanup: stop server if it was created.
114+ if (server != null ) {
115+ try {
116+ await server.stopServer ();
117+ } catch (e) {
118+ Logging .instance.log (Level .warning, "Failed to stop test server: $e " );
119+ }
120+ }
121+ }
122+ }
123+
124+ Future <void > _testMwebdServerStatus () async {
125+ Logging .instance.log (Level .info, "Testing MWEB server status..." );
126+
127+ MwebdServer ? server;
128+ try {
129+ // Get a random unused port for testing.
130+ final port = await _getRandomUnusedPort ();
131+ if (port == null ) {
132+ throw Exception ("Could not find an unused port for mwebd test" );
133+ }
134+
135+ // Get test data directory.
136+ final dir = await StackFileSystem .applicationMwebdDirectory ("testnet" );
137+
138+ // Create and start MwebdServer.
139+ server = MwebdServer (
140+ chain: "testnet" ,
141+ dataDir: dir.path,
142+ peer: "litecoin.stackwallet.com:19335" , // Testnet peer.
143+ proxy: "" , // No proxy for test.
144+ serverPort: port,
145+ );
146+
147+ await server.createServer ();
148+ await server.startServer ();
149+
150+ // Test getting server status - this tests FFI status calls.
151+ final status = await server.getStatus ();
152+
153+ // Verify we got a status response.
154+ if (status.blockHeaderHeight < 0 ) {
155+ throw Exception ("Invalid block header height in status: ${status .blockHeaderHeight }" );
156+ }
157+
158+ // Status should have reasonable values (not necessarily synced for test).
159+ if (status.mwebHeaderHeight < 0 ) {
160+ throw Exception ("Invalid MWEB header height in status: ${status .mwebHeaderHeight }" );
161+ }
162+
163+ Logging .instance.log (Level .info,
164+ "👍 MWEB server status test passed (blockHeight: ${status .blockHeaderHeight }, mwebHeight: ${status .mwebHeaderHeight })"
165+ );
166+
167+ } catch (e) {
168+ throw Exception ("MWEB server status test failed: $e " );
169+ } finally {
170+ // Cleanup: stop server if it was created.
171+ if (server != null ) {
172+ try {
173+ await server.stopServer ();
174+ } catch (e) {
175+ Logging .instance.log (Level .warning, "Failed to stop test server: $e " );
176+ }
177+ }
178+ }
179+ }
180+
181+ Future <void > _testMwebClientConnection () async {
182+ Logging .instance.log (Level .info, "Testing MWEB client connection..." );
183+
184+ MwebdServer ? server;
185+ MwebClient ? client;
186+ try {
187+ // Get a random unused port for testing.
188+ final port = await _getRandomUnusedPort ();
189+ if (port == null ) {
190+ throw Exception ("Could not find an unused port for mwebd test" );
191+ }
192+
193+ // Get test data directory.
194+ final dir = await StackFileSystem .applicationMwebdDirectory ("testnet" );
195+
196+ // Create and start MwebdServer.
197+ server = MwebdServer (
198+ chain: "testnet" ,
199+ dataDir: dir.path,
200+ peer: "litecoin.stackwallet.com:19335" , // testnet peer.
201+ proxy: "" , // no proxy for test.
202+ serverPort: port,
203+ );
204+
205+ await server.createServer ();
206+ await server.startServer ();
207+
208+ // Create MwebClient to connect to the server.
209+ client = MwebClient .fromHost ("127.0.0.1" , port);
210+
211+ // Test basic client operations.
212+ // Generate dummy scan and spend secrets for testing.
213+ final testScanSecret = Uint8List .fromList (
214+ List .generate (32 , (index) => index + 1 )
215+ );
216+ final testSpendPub = Uint8List .fromList (
217+ List .generate (33 , (index) => index + 1 )
218+ );
219+
220+ // Test address generation - this tests the client FFI integration.
221+ final mwebAddress = await client.address (
222+ testScanSecret,
223+ testSpendPub,
224+ 0 , // index
225+ );
226+
227+ // Verify we got a valid MWEB address.
228+ if (mwebAddress.isEmpty) {
229+ throw Exception ("Generated MWEB address is empty" );
230+ }
231+
232+ // MWEB addresses should start with "ltcmweb" for testnet.
233+ if (! mwebAddress.startsWith ("ltcmweb" )) {
234+ throw Exception ("Generated MWEB address does not have expected prefix: $mwebAddress " );
235+ }
236+
237+ Logging .instance.log (Level .info,
238+ "👍 MWEB client connection test passed: $mwebAddress "
239+ );
240+
241+ } catch (e) {
242+ throw Exception ("MWEB client connection test failed: $e " );
243+ } finally {
244+ // Cleanup.
245+ if (client != null ) {
246+ try {
247+ await client.cleanup ();
248+ } catch (e) {
249+ Logging .instance.log (Level .warning, "Failed to cleanup test client: $e " );
250+ }
251+ }
252+ if (server != null ) {
253+ try {
254+ await server.stopServer ();
255+ } catch (e) {
256+ Logging .instance.log (Level .warning, "Failed to stop test server: $e " );
257+ }
258+ }
259+ }
260+ }
261+
262+ void _updateStatus (TestSuiteStatus newStatus) {
263+ _status = newStatus;
264+ _statusController.add (newStatus);
265+ }
266+
267+ @override
268+ Future <void > cleanup () async {
269+ await _statusController.close ();
270+ }
271+ }
272+
273+ // Helper function to get a random unused port.
274+ Future <int ?> _getRandomUnusedPort ({Set <int > excluded = const {}}) async {
275+ const int minPort = 1024 ;
276+ const int maxPort = 65535 ;
277+ const int maxAttempts = 100 ;
278+
279+ final random = Random .secure ();
280+
281+ for (int i = 0 ; i < maxAttempts; i++ ) {
282+ final int potentialPort = minPort + random.nextInt (maxPort - minPort + 1 );
283+
284+ if (excluded.contains (potentialPort)) {
285+ continue ;
286+ }
287+
288+ try {
289+ final serverSocket = await ServerSocket .bind (
290+ InternetAddress .anyIPv4,
291+ potentialPort,
292+ );
293+ await serverSocket.close ();
294+ return potentialPort;
295+ } catch (_) {
296+ excluded.add (potentialPort);
297+ continue ;
298+ }
299+ }
300+
301+ return null ;
302+ }
0 commit comments