mirror of
https://github.com/MAGICGrants/flutter_libsparkmobile.git
synced 2026-01-09 21:17:56 -05:00
273 lines
9.2 KiB
Dart
273 lines
9.2 KiB
Dart
import 'dart:async';
|
|
|
|
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';
|
|
|
|
abstract class SparkAddressGenerator {
|
|
/// Generate key data from a mnemonic.
|
|
static Future<String> generateKeyData(
|
|
String mnemonic, String derivePath) async {
|
|
final seed = bip39.mnemonicToSeed(mnemonic, passphrase: '');
|
|
final root = coinlib.HDPrivateKey.fromSeed(seed);
|
|
|
|
// TODO validate derivePath.
|
|
final keys = root.derivePath(derivePath);
|
|
|
|
return keys.privateKey.data.toHexString();
|
|
}
|
|
|
|
/// Derive an address from the keyData (mnemonic).
|
|
static Future<String> getAddress(
|
|
String keyDataHex, int index, int diversifier, bool isTestnet) async {
|
|
return await LibSpark.getAddress(
|
|
privateKey: keyDataHex.toBytes(),
|
|
index: index,
|
|
diversifier: diversifier,
|
|
isTestNet: isTestnet,
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() async {
|
|
await coinlib.loadCoinlib();
|
|
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
class MyApp extends StatefulWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
State<MyApp> createState() => _MyAppState();
|
|
}
|
|
|
|
class _MyAppState extends State<MyApp> {
|
|
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');
|
|
|
|
final keyDataController = TextEditingController();
|
|
final diversifierController =
|
|
TextEditingController(text: '0'); // See Spark flow document.
|
|
bool isTestnet = false; // Default to mainnet.
|
|
|
|
final purposeController = TextEditingController(text: '44'); // BIP44.
|
|
final coinTypeController = TextEditingController(text: '136'); // Mainnet.
|
|
// 136 is mainnet, 1 is testnet.
|
|
final accountController = TextEditingController(text: '0'); // Receiving.
|
|
final chainController =
|
|
TextEditingController(text: '6'); // BIP44_SPARK_INDEX.
|
|
// BIP_44_SPARK_INDEX is 6.
|
|
final indexController = TextEditingController(text: '1');
|
|
|
|
final addressController = TextEditingController();
|
|
|
|
// Define mnemonic strengths.
|
|
final List<int> mnemonicStrengths = [
|
|
128,
|
|
256
|
|
]; // 128 bits for 12 words, 256 bits for 24 words.
|
|
int currentStrength = 256; // 24 words by default.
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
SchedulerBinding.instance
|
|
.addPostFrameCallback((_) => generateKeyDataAndGetAddress());
|
|
}
|
|
|
|
Future<void> generateKeyData() async {
|
|
// Construct derivePath string.
|
|
final derivePath =
|
|
"m/${purposeController.text}'/${coinTypeController.text}'/${accountController.text}'/${chainController.text}/${indexController.text}";
|
|
|
|
final keyData = await SparkAddressGenerator.generateKeyData(
|
|
mnemonicController.text, derivePath);
|
|
setState(() {
|
|
keyDataController.text = keyData;
|
|
});
|
|
}
|
|
|
|
Future<void> getAddress() async {
|
|
final address = await SparkAddressGenerator.getAddress(
|
|
keyDataController.text,
|
|
int.parse(indexController.text),
|
|
int.parse(diversifierController.text),
|
|
isTestnet,
|
|
);
|
|
setState(() {
|
|
addressController.text = address;
|
|
});
|
|
}
|
|
|
|
Future<void> generateKeyDataAndGetAddress() async {
|
|
final purpose = int.parse(purposeController.text);
|
|
final coinType = int.parse(coinTypeController.text);
|
|
final account = int.parse(accountController.text);
|
|
final chain = int.parse(chainController.text);
|
|
final index = int.parse(indexController.text);
|
|
|
|
// Construct derivePath string.
|
|
final String derivePath = "m/$purpose'/$coinType'/$account'/$chain/$index";
|
|
|
|
final keyData = await SparkAddressGenerator.generateKeyData(
|
|
mnemonicController.text, derivePath);
|
|
|
|
setState(() {
|
|
keyDataController.text = keyData;
|
|
});
|
|
|
|
await getAddress();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Spark Mobile Example App'),
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
DropdownButton<int>(
|
|
value: currentStrength,
|
|
items: mnemonicStrengths.map((int value) {
|
|
return DropdownMenuItem<int>(
|
|
value: value,
|
|
child: Text('${value == 128 ? 12 : 24} words'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (int? newValue) {
|
|
setState(() {
|
|
currentStrength = newValue!;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 8), // Spacing between inputs
|
|
ElevatedButton(
|
|
onPressed: () => setState(() {
|
|
mnemonicController.text =
|
|
bip39.generateMnemonic(strength: currentStrength);
|
|
|
|
generateKeyDataAndGetAddress();
|
|
}),
|
|
child: const Text('Generate Mnemonic'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
controller: mnemonicController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Mnemonic Recovery Phrase'),
|
|
),
|
|
const SizedBox(height: 20),
|
|
ElevatedButton(
|
|
onPressed: generateKeyData,
|
|
child: const Text('Generate Key Data'),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
// keyData takes the majority of the space
|
|
flex: 4,
|
|
child: TextField(
|
|
controller: keyDataController,
|
|
decoration: const InputDecoration(labelText: 'Key Data'),
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 64,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.only(bottom: 22),
|
|
child: TextField(
|
|
controller: diversifierController,
|
|
decoration:
|
|
const InputDecoration(labelText: 'Diversifier'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.only(bottom: 22),
|
|
child: Row(
|
|
children: [
|
|
Checkbox(
|
|
value: isTestnet,
|
|
onChanged: (bool? newValue) {
|
|
setState(() {
|
|
isTestnet = newValue ?? true;
|
|
if (isTestnet) {
|
|
coinTypeController.text = '1';
|
|
} else {
|
|
coinTypeController.text = '136';
|
|
}
|
|
});
|
|
},
|
|
),
|
|
const Text('Testnet'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
children: [
|
|
_buildNumberInput(purposeController, 'Purpose'),
|
|
const SizedBox(width: 8),
|
|
_buildNumberInput(coinTypeController, 'Coin Type'),
|
|
const SizedBox(width: 8),
|
|
_buildNumberInput(accountController, 'Account'),
|
|
const SizedBox(width: 8),
|
|
_buildNumberInput(chainController, 'Chain'),
|
|
const SizedBox(width: 8),
|
|
_buildNumberInput(indexController, 'Index'),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
ElevatedButton(
|
|
onPressed: getAddress,
|
|
child: const Text('Get Address'),
|
|
),
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
controller: addressController,
|
|
decoration: const InputDecoration(labelText: 'Spark Address'),
|
|
readOnly: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNumberInput(TextEditingController controller, String label) {
|
|
return Expanded(
|
|
child: TextField(
|
|
controller: controller,
|
|
decoration: InputDecoration(labelText: label),
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
),
|
|
);
|
|
}
|
|
}
|