|
| 1 | +import 'dart:convert'; |
| 2 | +import 'dart:io'; |
| 3 | + |
| 4 | +import 'package:dio/dio.dart'; |
| 5 | +import 'package:flutter/foundation.dart'; |
| 6 | +import 'package:flutter_test/flutter_test.dart'; |
| 7 | +import 'package:integration_test/integration_test.dart'; |
| 8 | +import 'package:ldk_node/ldk_node.dart' as ldk; |
| 9 | +import 'package:ldk_node/src/generated/api/builder.dart' as builder; |
| 10 | +import 'package:ldk_node/src/generated/frb_generated.dart'; |
| 11 | +import 'package:path_provider/path_provider.dart'; |
| 12 | + |
| 13 | +void main() { |
| 14 | + String esploraUrl = Platform.isAndroid |
| 15 | + ? 'http://10.0.2.2:30000' |
| 16 | + : 'http://127.0.0.1:30000'; |
| 17 | + final regTestClient = BtcClient(""); |
| 18 | + final esploraConfig = ldk.EsploraSyncConfig( |
| 19 | + backgroundSyncConfig: ldk.BackgroundSyncConfig( |
| 20 | + onchainWalletSyncIntervalSecs: BigInt.from(60), |
| 21 | + lightningWalletSyncIntervalSecs: BigInt.from(60), |
| 22 | + feeRateCacheUpdateIntervalSecs: BigInt.from(600))); |
| 23 | + |
| 24 | + Future<ldk.Config> initLdkConfig( |
| 25 | + String path, ldk.SocketAddress address) async { |
| 26 | + final directory = await getApplicationDocumentsDirectory(); |
| 27 | + final nodePath = "${directory.path}/ldk_cache/integration/regtest/$path"; |
| 28 | + final config = ldk.Config( |
| 29 | + probingLiquidityLimitMultiplier: BigInt.from(3), |
| 30 | + trustedPeers0Conf: [], |
| 31 | + storageDirPath: nodePath, |
| 32 | + network: ldk.Network.regtest, |
| 33 | + listeningAddresses: [address]); |
| 34 | + return config; |
| 35 | + } |
| 36 | + |
| 37 | + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); |
| 38 | + |
| 39 | + // Track if FRB has been initialized |
| 40 | + bool frbInitialized = false; |
| 41 | + |
| 42 | + Future<void> ensureFrbInitialized() async { |
| 43 | + if (!frbInitialized) { |
| 44 | + await core.init(); |
| 45 | + frbInitialized = true; |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + group('new_apis_integration', () { |
| 50 | + setUp(() async {}); |
| 51 | + |
| 52 | + testWidgets('mnemonic_word_count_test', (WidgetTester tester) async { |
| 53 | + // Initialize flutter_rust_bridge |
| 54 | + await ensureFrbInitialized(); |
| 55 | + |
| 56 | + debugPrint("Testing Mnemonic.generateWithWordCount()..."); |
| 57 | + |
| 58 | + // Test 12-word mnemonic using the generated FfiMnemonic API |
| 59 | + final mnemonic12 = await builder.FfiMnemonic.generateWithWordCount(wordCount: 12); |
| 60 | + final words12 = mnemonic12.seedPhrase.split(' '); |
| 61 | + debugPrint("Generated 12-word mnemonic: ${mnemonic12.seedPhrase}"); |
| 62 | + expect(words12.length, equals(12)); |
| 63 | + |
| 64 | + // Test 24-word mnemonic |
| 65 | + final mnemonic24 = await builder.FfiMnemonic.generateWithWordCount(wordCount: 24); |
| 66 | + final words24 = mnemonic24.seedPhrase.split(' '); |
| 67 | + debugPrint("Generated 24-word mnemonic: ${mnemonic24.seedPhrase}"); |
| 68 | + expect(words24.length, equals(24)); |
| 69 | + |
| 70 | + debugPrint("Mnemonic word count test completed successfully!"); |
| 71 | + }); |
| 72 | + |
| 73 | + testWidgets('custom_preimage_api_test', (WidgetTester tester) async { |
| 74 | + await ensureFrbInitialized(); |
| 75 | + |
| 76 | + debugPrint("Testing PaymentPreimage creation and sendWithPreimageUnsafe API availability..."); |
| 77 | + |
| 78 | + // Test PaymentPreimage convenience extension |
| 79 | + final customPreimage = ldk.PaymentPreimageExtensions.fromBytes([ |
| 80 | + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
| 81 | + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 |
| 82 | + ]); |
| 83 | + |
| 84 | + debugPrint("PaymentPreimage created successfully"); |
| 85 | + expect(customPreimage.data.inner.length, equals(32)); |
| 86 | + |
| 87 | + // Create a node to verify spontaneousPayment().sendWithPreimageUnsafe exists |
| 88 | + final aliceConfig = await initLdkConfig( |
| 89 | + 'alice_preimage_api', const ldk.SocketAddress.hostname(addr: "0.0.0.0", port: 3017)); |
| 90 | + final aliceBuilder = ldk.Builder.fromConfig(config: aliceConfig) |
| 91 | + .setEntropyBip39Mnemonic( |
| 92 | + mnemonic: ldk.Mnemonic( |
| 93 | + seedPhrase: |
| 94 | + "replace force spring cruise nothing select glass erupt medal raise consider pull")) |
| 95 | + .setChainSourceEsplora( |
| 96 | + esploraServerUrl: esploraUrl, syncConfig: esploraConfig); |
| 97 | + final aliceNode = await aliceBuilder.build(); |
| 98 | + await aliceNode.start(); |
| 99 | + |
| 100 | + // Verify the spontaneousPayment handler and sendWithPreimageUnsafe method exist |
| 101 | + final spontaneousHandler = await aliceNode.spontaneousPayment(); |
| 102 | + debugPrint("SpontaneousPayment handler obtained successfully"); |
| 103 | + |
| 104 | + // Test that the method exists by checking it's callable (will fail due to no route but that's ok) |
| 105 | + final testNodeId = await aliceNode.nodeId(); // Use own node ID for test |
| 106 | + try { |
| 107 | + await spontaneousHandler.sendWithPreimageUnsafe( |
| 108 | + nodeId: testNodeId, |
| 109 | + preimage: customPreimage, |
| 110 | + amountMsat: BigInt.from(1000), |
| 111 | + ); |
| 112 | + } catch (e) { |
| 113 | + // Expected to fail - no route to self, but this proves the API exists |
| 114 | + debugPrint("sendWithPreimageUnsafe() API exists - got expected error: $e"); |
| 115 | + } |
| 116 | + |
| 117 | + debugPrint("Custom preimage API test completed successfully!"); |
| 118 | + await aliceNode.stop(); |
| 119 | + }); |
| 120 | + }); |
| 121 | +} |
| 122 | + |
| 123 | +class BtcClient { |
| 124 | + String rpcUser = "admin1"; |
| 125 | + String rpcPassword = "123"; |
| 126 | + int rpcPort = 18443; |
| 127 | + |
| 128 | + Dio? _dioClient; |
| 129 | + late Map<String, String> _headers; |
| 130 | + late String _url; |
| 131 | + final String wallet; |
| 132 | + |
| 133 | + String getConnectionString(String host, int port, String wallet) { |
| 134 | + return 'http://$host:$port/wallet/$wallet'; |
| 135 | + } |
| 136 | + |
| 137 | + BtcClient(this.wallet) { |
| 138 | + _headers = { |
| 139 | + 'Content-Type': 'application/json', |
| 140 | + 'authorization': |
| 141 | + 'Basic ${base64.encode(utf8.encode("$rpcUser:$rpcPassword"))}' |
| 142 | + }; |
| 143 | + _url = getConnectionString( |
| 144 | + Platform.isAndroid ? "10.0.2.2" : "0.0.0.0", rpcPort, wallet); |
| 145 | + _dioClient = Dio(); |
| 146 | + } |
| 147 | + |
| 148 | + Future<void> loadWallet() async { |
| 149 | + try { |
| 150 | + var params = [wallet]; |
| 151 | + await call("loadwallet", params); |
| 152 | + } on Exception catch (e) { |
| 153 | + if (e.toString().contains("-4")) { |
| 154 | + debugPrint(" $wallet already loaded!"); |
| 155 | + } else if (e.toString().contains("-18")) { |
| 156 | + debugPrint("$wallet doesn't exist!"); |
| 157 | + var params = [wallet]; |
| 158 | + await call("createwallet", params); |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + Future<List<dynamic>> generate(int nblocks, String address) async { |
| 164 | + var params = [ |
| 165 | + nblocks, |
| 166 | + address, |
| 167 | + ]; |
| 168 | + final res = await call("generatetoaddress", params); |
| 169 | + return res; |
| 170 | + } |
| 171 | + |
| 172 | + Future<String> sendToAddress(String address, int amount) async { |
| 173 | + var params = [address, amount]; |
| 174 | + final res = await call("sendtoaddress", params); |
| 175 | + return res; |
| 176 | + } |
| 177 | + |
| 178 | + Future<int> getBlockCount() async { |
| 179 | + var params = []; |
| 180 | + final res = await call("getblockcount", params); |
| 181 | + return res; |
| 182 | + } |
| 183 | + |
| 184 | + Future<dynamic> call(var methodName, [var params]) async { |
| 185 | + var body = { |
| 186 | + 'jsonrpc': '2.0', |
| 187 | + 'method': methodName, |
| 188 | + 'params': params ?? [], |
| 189 | + 'id': '1' |
| 190 | + }; |
| 191 | + |
| 192 | + try { |
| 193 | + var response = await _dioClient!.post( |
| 194 | + _url, |
| 195 | + data: body, |
| 196 | + options: Options( |
| 197 | + headers: _headers, |
| 198 | + ), |
| 199 | + ); |
| 200 | + if (response.statusCode == HttpStatus.ok) { |
| 201 | + var body = response.data as Map<String, dynamic>; |
| 202 | + if (body.containsKey('error') && body["error"] != null) { |
| 203 | + var error = body['error']; |
| 204 | + |
| 205 | + if (error["message"] is Map<String, dynamic>) { |
| 206 | + error = error['message']; |
| 207 | + } |
| 208 | + |
| 209 | + throw Exception( |
| 210 | + "errorCode: ${error['code']},errorMsg: ${error['message']}", |
| 211 | + ); |
| 212 | + } |
| 213 | + return body['result']; |
| 214 | + } |
| 215 | + } on DioException catch (e) { |
| 216 | + if (e.type == DioExceptionType.badResponse) { |
| 217 | + var errorResponseBody = e.response!.data; |
| 218 | + |
| 219 | + switch (e.response!.statusCode) { |
| 220 | + case 401: |
| 221 | + throw Exception( |
| 222 | + " code: 401, message: Unauthorized", |
| 223 | + ); |
| 224 | + case 403: |
| 225 | + throw Exception( |
| 226 | + "code: 403,message: Forbidden", |
| 227 | + ); |
| 228 | + case 404: |
| 229 | + if (errorResponseBody['error'] != null) { |
| 230 | + var error = errorResponseBody['error']; |
| 231 | + throw Exception( |
| 232 | + "errorCode: ${error['code']},errorMsg: ${error['message']}", |
| 233 | + ); |
| 234 | + } |
| 235 | + throw Exception( |
| 236 | + "code: 500, message: Internal Server Error", |
| 237 | + ); |
| 238 | + default: |
| 239 | + if (errorResponseBody['error'] != null) { |
| 240 | + var error = errorResponseBody['error']; |
| 241 | + throw Exception( |
| 242 | + "errorCode: ${error['code']},errorMsg: ${error['message']}", |
| 243 | + ); |
| 244 | + } |
| 245 | + throw Exception( |
| 246 | + "code: 500, message: 'Internal Server Error'", |
| 247 | + ); |
| 248 | + } |
| 249 | + } else if (e.type == DioExceptionType.connectionError) { |
| 250 | + throw Exception( |
| 251 | + "code: 500,message: e.message ?? 'Connection Error'", |
| 252 | + ); |
| 253 | + } |
| 254 | + throw Exception( |
| 255 | + "code: 500, message: e.message ?? 'Unknown Error'", |
| 256 | + ); |
| 257 | + } |
| 258 | + } |
| 259 | +} |
0 commit comments