mirror of
https://github.com/MAGICGrants/flutter_libsparkmobile.git
synced 2026-01-08 20:47:56 -05:00
Simplify lib api and clean up duplicate code
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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<String> generateKeyData(String mnemonic, String derivePath) async {
|
||||
static Future<String> 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<String> getAddress(
|
||||
static Future<String> 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<int> 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<MyApp> {
|
||||
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<MyApp> {
|
||||
]; // 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<void> 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<void> 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<MyApp> {
|
||||
}
|
||||
|
||||
Future<void> 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<MyApp> {
|
||||
// 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<MyApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a hex string to a list of bytes, padded to 32 bytes if necessary.
|
||||
extension on String {
|
||||
List<int> toBytes() {
|
||||
// Pad the string to 64 characters with zeros if it's shorter.
|
||||
String hexString = padLeft(64, '0');
|
||||
|
||||
List<int> 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();
|
||||
}
|
||||
}
|
||||
|
||||
23
lib/extensions.dart
Normal file
23
lib/extensions.dart
Normal file
@@ -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<int> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String?> 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<String> getAddress(
|
||||
List<int> 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<String> 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<Char>();
|
||||
final keyDataPointer = privateKey.toHexString().toNativeUtf8().cast<Char>();
|
||||
|
||||
// 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<Char> to a Dart String.
|
||||
final addressString = addressPointer.cast<Utf8>().toDartString();
|
||||
@@ -43,10 +92,3 @@ class FlutterLibsparkmobile {
|
||||
return addressString;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert List<int> keyData to a hex string.
|
||||
extension on List<int> {
|
||||
String toHexString() {
|
||||
return map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> toBytes() {
|
||||
List<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user