From b76033ffcabc61edcbae6b93ac602db7b6535b4a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Nov 2023 09:26:10 -0600 Subject: [PATCH] Simplify lib api and clean up duplicate code --- .../plugin_integration_test.dart | 32 +---- example/lib/main.dart | 111 +++--------------- lib/extensions.dart | 23 ++++ lib/flutter_libsparkmobile.dart | 88 ++++++++++---- test/flutter_libsparkmobile_test.dart | 38 ++---- 5 files changed, 118 insertions(+), 174 deletions(-) create mode 100644 lib/extensions.dart diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index eae5981..d9fbfb2 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -1,6 +1,3 @@ -import 'dart:ffi' as ffi; -import 'dart:io'; - import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:flutter_libsparkmobile_example/main.dart'; @@ -13,24 +10,23 @@ void main() { // Load coinlib for crypto operations. coinlib.loadCoinlib(); - // Initialize the plugin. - final FlutterLibsparkmobile plugin = FlutterLibsparkmobile(_loadLibrary()); - final SparkAddressGenerator addressGenerator = SparkAddressGenerator(plugin); - testWidgets('mnemonic to address test', (WidgetTester tester) async { // Define the mnemonic. const mnemonic = 'jazz settle broccoli dove hurt deny leisure coffee ivory calm pact chicken flag spot nature gym afford cotton dinosaur young private flash core approve'; + const index = 1; + // Construct derivePath string. - const derivePath = "m/44'/136'/0'/6/1"; + const derivePath = "m/44'/136'/0'/$kSparkChain/$index"; // Generate key data from the mnemonic. final keyDataHex = - await addressGenerator.generateKeyData(mnemonic, derivePath); + await SparkAddressGenerator.generateKeyData(mnemonic, derivePath); // Derive the address from the key data. - final address = await addressGenerator.getAddress(keyDataHex, 1, 0, false); + final address = + await SparkAddressGenerator.getAddress(keyDataHex, index, 0, false); // Define the expected address. const expectedAddress = @@ -40,19 +36,3 @@ void main() { expect(address, expectedAddress); }); } - -/// Load the native library. -ffi.DynamicLibrary _loadLibrary() { - if (Platform.isLinux) { - return ffi.DynamicLibrary.open('libsparkmobile.so'); - } else if (Platform.isAndroid) { - return ffi.DynamicLibrary.open('libsparkmobile.so'); - } else if (Platform.isIOS) { - return ffi.DynamicLibrary.open('libsparkmobile.dylib'); - } else if (Platform.isMacOS) { - return ffi.DynamicLibrary.open('libsparkmobile.dylib'); - } else if (Platform.isWindows) { - return ffi.DynamicLibrary.open('sparkmobile.dll'); - } - throw UnsupportedError('This platform is not supported'); -} diff --git a/example/lib/main.dart b/example/lib/main.dart index 6470143..5d4e4ec 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,21 +1,17 @@ import 'dart:async'; -import 'dart:ffi'; -import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_libsparkmobile/extensions.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; -class SparkAddressGenerator { - final FlutterLibsparkmobile _flutterLibsparkmobilePlugin; - - SparkAddressGenerator(this._flutterLibsparkmobilePlugin); - +abstract class SparkAddressGenerator { /// Generate key data from a mnemonic. - Future generateKeyData(String mnemonic, String derivePath) async { + static Future generateKeyData( + String mnemonic, String derivePath) async { final seed = bip39.mnemonicToSeed(mnemonic, passphrase: ''); final root = coinlib.HDPrivateKey.fromSeed(seed); @@ -26,17 +22,20 @@ class SparkAddressGenerator { } /// Derive an address from the keyData (mnemonic). - Future getAddress( + static Future getAddress( String keyDataHex, int index, int diversifier, bool isTestnet) async { - // Convert the hex string to a list of bytes and pad to 32 bytes. - final List keyData = keyDataHex.toBytes(); - - return await _flutterLibsparkmobilePlugin.getAddress( - keyData, index, diversifier, isTestnet); + return await LibSpark.getAddress( + privateKey: keyDataHex.toBytes(), + index: index, + diversifier: diversifier, + isTestNet: isTestnet, + ); } } -void main() { +void main() async { + await coinlib.loadCoinlib(); + runApp(const MyApp()); } @@ -48,11 +47,6 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - late final SparkAddressGenerator _addressGenerator; - - String _platformVersion = 'Unknown'; - final FlutterLibsparkmobile _flutterLibsparkmobilePlugin; - final mnemonicController = TextEditingController( text: 'jazz settle broccoli dove hurt deny leisure coffee ivory calm pact chicken flag spot nature gym afford cotton dinosaur young private flash core approve'); @@ -80,68 +74,20 @@ class _MyAppState extends State { ]; // 128 bits for 12 words, 256 bits for 24 words. int currentStrength = 256; // 24 words by default. - _MyAppState() - : _flutterLibsparkmobilePlugin = FlutterLibsparkmobile(_loadLibrary()); - - static DynamicLibrary _loadLibrary() { - if (Platform.isLinux) { - return DynamicLibrary.open('libsparkmobile.so'); - } else if (Platform.isAndroid) { - // return DynamicLibrary.open('libsparkmobile.so'); - } else if (Platform.isIOS) { - // return DynamicLibrary.open('libsparkmobile.dylib'); - } else if (Platform.isMacOS) { - // return DynamicLibrary.open('libsparkmobile.dylib'); - } else if (Platform.isWindows) { - // return DynamicLibrary.open('sparkmobile.dll'); - } - throw UnsupportedError('This platform is not supported'); - } - @override void initState() { super.initState(); - // Load coinlib. - coinlib.loadCoinlib(); - - _addressGenerator = SparkAddressGenerator(_flutterLibsparkmobilePlugin); - - initPlatformState(); - SchedulerBinding.instance .addPostFrameCallback((_) => generateKeyDataAndGetAddress()); } - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. - try { - platformVersion = - await _flutterLibsparkmobilePlugin.getPlatformVersion() ?? - 'Unknown platform version'; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); - } - Future generateKeyData() async { // Construct derivePath string. final derivePath = "m/${purposeController.text}'/${coinTypeController.text}'/${accountController.text}'/${chainController.text}/${indexController.text}"; - final keyData = await _addressGenerator.generateKeyData( + final keyData = await SparkAddressGenerator.generateKeyData( mnemonicController.text, derivePath); setState(() { keyDataController.text = keyData; @@ -149,7 +95,7 @@ class _MyAppState extends State { } Future getAddress() async { - final address = await _addressGenerator.getAddress( + final address = await SparkAddressGenerator.getAddress( keyDataController.text, int.parse(indexController.text), int.parse(diversifierController.text), @@ -170,7 +116,7 @@ class _MyAppState extends State { // Construct derivePath string. final String derivePath = "m/$purpose'/$coinType'/$account'/$chain/$index"; - final keyData = await _addressGenerator.generateKeyData( + final keyData = await SparkAddressGenerator.generateKeyData( mnemonicController.text, derivePath); setState(() { @@ -324,26 +270,3 @@ class _MyAppState extends State { ); } } - -/// Convert a hex string to a list of bytes, padded to 32 bytes if necessary. -extension on String { - List toBytes() { - // Pad the string to 64 characters with zeros if it's shorter. - String hexString = padLeft(64, '0'); - - List bytes = []; - for (int i = 0; i < hexString.length; i += 2) { - var byteString = hexString.substring(i, i + 2); - var byteValue = int.parse(byteString, radix: 16); - bytes.add(byteValue); - } - return bytes; - } -} - -/// Convert a Uint8List to a hex string. -extension on Uint8List { - String toHexString() { - return map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); - } -} diff --git a/lib/extensions.dart b/lib/extensions.dart new file mode 100644 index 0000000..7314cbd --- /dev/null +++ b/lib/extensions.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; + +extension Uint8ListExt on Uint8List { + String toHexString() { + return map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + } +} + +/// Convert a hex string to a list of bytes, padded to 32 bytes if necessary. +extension StringExt on String { + Uint8List toBytes() { + // Pad the string to 64 characters with zeros if it's shorter. + String hexString = padLeft(64, '0'); + + List bytes = []; + for (int i = 0; i < hexString.length; i += 2) { + var byteString = hexString.substring(i, i + 2); + var byteValue = int.parse(byteString, radix: 16); + bytes.add(byteValue); + } + return Uint8List.fromList(bytes); + } +} diff --git a/lib/flutter_libsparkmobile.dart b/lib/flutter_libsparkmobile.dart index ad6ea9b..adaa9e3 100644 --- a/lib/flutter_libsparkmobile.dart +++ b/lib/flutter_libsparkmobile.dart @@ -1,37 +1,86 @@ import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; import 'package:ffi/ffi.dart'; +import 'package:flutter_libsparkmobile/extensions.dart'; import 'flutter_libsparkmobile_bindings.dart'; -import 'flutter_libsparkmobile_platform_interface.dart'; -class FlutterLibsparkmobile { - final SparkMobileBindings _bindings; +const kSparkChain = 6; +const kSparkBaseDerivationPath = "m/44'/136'/0'/$kSparkChain/"; - FlutterLibsparkmobile(DynamicLibrary dynamicLibrary) - : _bindings = SparkMobileBindings(dynamicLibrary); +abstract final class LibSpark { + static SparkMobileBindings? _bindings; - Future getPlatformVersion() { - return FlutterLibsparkmobilePlatform.instance.getPlatformVersion(); + static void _checkLoaded() { + _bindings ??= SparkMobileBindings(_loadLibrary()); + } + + static DynamicLibrary _loadLibrary() { + // hack in prefix for test env + String testPrefix = ""; + if (Platform.environment.containsKey('FLUTTER_TEST')) { + if (Platform.isLinux) { + testPrefix = 'scripts/linux/build/'; + } else if (Platform.isMacOS) { + testPrefix = 'scripts/macos/build/'; + } else if (Platform.isWindows) { + testPrefix = 'scripts/windows/build/'; + } else { + throw UnsupportedError('This platform is not supported'); + } + } + + if (Platform.isLinux) { + return DynamicLibrary.open('${testPrefix}libsparkmobile.so'); + } else if (Platform.isAndroid) { + // return DynamicLibrary.open('${testPrefix}libsparkmobile.so'); + } else if (Platform.isIOS) { + // return DynamicLibrary.open('${testPrefix}libsparkmobile.dylib'); + } else if (Platform.isMacOS) { + // return DynamicLibrary.open('${testPrefix}libsparkmobile.dylib'); + } else if (Platform.isWindows) { + // return DynamicLibrary.open('${testPrefix}sparkmobile.dll'); + } + throw UnsupportedError('This platform is not supported'); } // SparkMobileBindings methods: /// Derive an address from the keyData (mnemonic). - Future getAddress( - List keyData, int index, int diversifier, bool isTestNet) async { - // Validate that the keyData is 32 bytes. - if (keyData.length != 32) { - throw 'Key data must be 32 bytes.'; + static Future getAddress({ + required Uint8List privateKey, + required int index, + required int diversifier, + bool isTestNet = false, + }) async { + _checkLoaded(); + + if (index < 0) { + throw Exception("Index must not be negative."); + } + + if (diversifier < 0) { + throw Exception("Diversifier must not be negative."); + } + + if (privateKey.length != 32) { + throw Exception( + "Invalid private key length: ${privateKey.length}. Must be 32 bytes.", + ); } // Allocate memory for the hex string on the native heap. - final keyDataHex = keyData.toHexString(); - final keyDataPointer = keyDataHex.toNativeUtf8().cast(); + final keyDataPointer = privateKey.toHexString().toNativeUtf8().cast(); // Call the native method with the pointer. - final addressPointer = _bindings.getAddress( - keyDataPointer, index, diversifier, isTestNet ? 1 : 0); + final addressPointer = _bindings!.getAddress( + keyDataPointer, + index, + diversifier, + isTestNet ? 1 : 0, + ); // Convert the Pointer to a Dart String. final addressString = addressPointer.cast().toDartString(); @@ -43,10 +92,3 @@ class FlutterLibsparkmobile { return addressString; } } - -/// Convert List keyData to a hex string. -extension on List { - String toHexString() { - return map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); - } -} diff --git a/test/flutter_libsparkmobile_test.dart b/test/flutter_libsparkmobile_test.dart index 7a5e92e..c4cbcb3 100644 --- a/test/flutter_libsparkmobile_test.dart +++ b/test/flutter_libsparkmobile_test.dart @@ -1,13 +1,8 @@ -import 'dart:ffi' as ffi; -import 'dart:io'; - +import 'package:flutter_libsparkmobile/extensions.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - // Initialize the plugin. - final FlutterLibsparkmobile plugin = FlutterLibsparkmobile(_loadLibrary()); - test('mnemonic to address test', () async { // Generate key data from the mnemonic. // @@ -24,7 +19,12 @@ void main() { 'cb02b05c71a69080b083484f1cdf407677fac00ced6438df16925e2a29b4eebf'; // Derive the address from the key data. - final address = await plugin.getAddress(keyDataHex.toBytes(), 1, 0, false); + final address = await LibSpark.getAddress( + privateKey: keyDataHex.toBytes(), + index: 1, + diversifier: 0, + isTestNet: false, + ); // Define the expected address. const expectedAddress = @@ -34,27 +34,3 @@ void main() { expect(address, expectedAddress); }); } - -/// Load the native library. -ffi.DynamicLibrary _loadLibrary() { - if (Platform.isLinux) { - return ffi.DynamicLibrary.open('scripts/linux/build/libsparkmobile.so'); - } else if (Platform.isMacOS) { - return ffi.DynamicLibrary.open('scripts/macos/build/libsparkmobile.dylib'); - } else if (Platform.isWindows) { - return ffi.DynamicLibrary.open('scripts/windows/build/sparkmobile.dll'); - } - throw UnsupportedError('This platform is not supported'); -} - -extension on String { - List toBytes() { - List bytes = []; - for (int i = 0; i < length; i += 2) { - var byteString = substring(i, i + 2); - var byteValue = int.parse(byteString, radix: 16); - bytes.add(byteValue); - } - return bytes; - } -}