Simplify lib api and clean up duplicate code

This commit is contained in:
julian
2023-11-28 09:26:10 -06:00
parent 1ae8d15c3c
commit a3ece512bc
5 changed files with 118 additions and 174 deletions

View File

@@ -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');
}

View File

@@ -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
View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}