Skip to content

Commit

Permalink
feat(cat-voices): Add CIP-39 seed phrase utility (#852)
Browse files Browse the repository at this point in the history
* feat: bip39

* fix: constructor

* refactor: single class

* fix: docs

* test: initial test

* fix: typo

* refactor: seed phrase class

* test: seed phrase words

* feat: full words

* fix: cspell

* refactor: move to models

* refactor: rename test folder

* test: add exceeding cases

* refactor: test array

* fix: linter for a file
  • Loading branch information
apskhem committed Sep 23, 2024
1 parent 50348cd commit 177e2ee
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// cspell: words wordlists WORDLIST
// ignore_for_file: implementation_imports

import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bip39/src/wordlists/english.dart';
import 'package:convert/convert.dart';

/// Represents a seed phrase consisting of a mnemonic and provides methods for
/// generating and deriving cryptographic data from the mnemonic.
///
/// The `SeedPhrase` class allows creation of a seed phrase either randomly,
/// from a given mnemonic, or from entropy data. It supports converting between
/// different formats, including Uint8List and hex strings.
class SeedPhrase {
/// The mnemonic phrase
final String mnemonic;

/// Generates a new seed phrase with a random mnemonic.
///
/// Throws an [ArgumentError] if the word count is invalid.
///
/// [wordCount]: The number of words in the mnemonic.
/// The default word count is 12, but can specify 12, 15, 18, 21, or 24 words.
/// with a higher word count providing greater entropy and security.
SeedPhrase({int wordCount = 12})
: this.fromMnemonic(
bip39.generateMnemonic(
strength: (wordCount * 32) ~/ 3,
),
);

/// Creates a SeedPhrase from an existing [Uint8List] entropy.
///
/// [encodedData]: The entropy data as a Uint8List.
SeedPhrase.fromUint8ListEntropy(Uint8List encodedData)
: this.fromHexEntropy(hex.encode(encodedData));

/// Creates a SeedPhrase from an existing hex-encoded entropy.
///
/// [encodedData]: The entropy data as a hex string.
SeedPhrase.fromHexEntropy(String encodedData)
: this.fromMnemonic(bip39.entropyToMnemonic(encodedData));

/// Creates a SeedPhrase from an existing [mnemonic].
///
/// Throws an [ArgumentError] if the mnemonic is invalid.
///
/// [mnemonic]: The mnemonic to derive the seed from.
SeedPhrase.fromMnemonic(this.mnemonic)
: assert(bip39.validateMnemonic(mnemonic), 'Invalid mnemonic phrase');

/// The seed derived from the mnemonic as a Uint8List.
Uint8List get uint8ListSeed => bip39.mnemonicToSeed(mnemonic);

/// The seed derived from the mnemonic as a hex-encoded string.
String get hexSeed => bip39.mnemonicToSeedHex(mnemonic);

/// The entropy derived from the mnemonic as a Uint8List.
Uint8List get uint8ListEntropy => Uint8List.fromList(hex.decode(hexEntropy));

/// The entropy derived from the mnemonic as a hex-encoded string.
String get hexEntropy => bip39.mnemonicToEntropy(mnemonic);

/// The mnemonic phrase as a list of individual words.
List<String> get mnemonicWords => mnemonic.split(' ');

/// The full list of BIP-39 mnemonic words in English.
static List<String> get wordList => WORDLIST;
}
2 changes: 2 additions & 0 deletions catalyst_voices/packages/catalyst_voices_models/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ environment:
sdk: ">=3.5.0 <4.0.0"

dependencies:
bip39: ^1.0.6
catalyst_cardano_serialization: ^0.4.0
convert: ^3.1.1
equatable: ^2.0.5
flutter_quill: ^10.5.13
meta: ^1.10.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:bip39/bip39.dart' as bip39;
import 'package:catalyst_voices_models/src/seed_phrase.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group(SeedPhrase, () {
test('should generate a new SeedPhrase with random mnemonic', () {
final seedPhrase = SeedPhrase();
expect(seedPhrase.mnemonic, isNotEmpty);
expect(seedPhrase.uint8ListSeed, isNotEmpty);
expect(seedPhrase.hexSeed, isNotEmpty);
expect(seedPhrase.mnemonicWords.length, 12);
});

test('should generate a seed phrase with 12, 15, 18, 21, and 24 words', () {
for (final wordCount in [12, 15, 18, 21, 24]) {
final seedPhrase = SeedPhrase(wordCount: wordCount);
expect(seedPhrase.mnemonicWords.length, wordCount);
expect(bip39.validateMnemonic(seedPhrase.mnemonic), isTrue);
}
});

test('should throw an error for an invalid word count', () {
expect(() => SeedPhrase(wordCount: 9), throwsA(isA<ArgumentError>()));
expect(() => SeedPhrase(wordCount: 13), throwsA(isA<AssertionError>()));
expect(() => SeedPhrase(wordCount: 27), throwsA(isA<ArgumentError>()));
});

test('should create SeedPhrase from a valid mnemonic', () {
final validMnemonic = bip39.generateMnemonic();
final seedPhrase = SeedPhrase.fromMnemonic(validMnemonic);
expect(seedPhrase.mnemonic, validMnemonic);
expect(seedPhrase.hexSeed, bip39.mnemonicToSeedHex(validMnemonic));
});

test('should create SeedPhrase from hex-encoded entropy', () {
final entropy = bip39.mnemonicToEntropy(bip39.generateMnemonic());
final seedPhrase = SeedPhrase.fromHexEntropy(entropy);

expect(seedPhrase.mnemonic, isNotEmpty);
expect(seedPhrase.hexEntropy, entropy);
});

test('should throw an error for invalid mnemonic', () {
const invalidMnemonic = 'invalid mnemonic phrase';
expect(
() => SeedPhrase.fromMnemonic(invalidMnemonic),
throwsA(
isA<AssertionError>().having(
(e) => e.message,
'message',
contains('Invalid mnemonic phrase'),
),
),
);
});

test('should contain consistent mnemonic and seed in generated SeedPhrase',
() {
final seedPhrase = SeedPhrase();
final mnemonic = seedPhrase.mnemonic;
final seed = seedPhrase.hexSeed;

expect(bip39.mnemonicToSeedHex(mnemonic), seed);
});

test('should split mnemonic into a list of words', () {
final mnemonic = bip39.generateMnemonic();
final seedPhrase = SeedPhrase.fromMnemonic(mnemonic);
final expectedWords = mnemonic.split(' ');
expect(seedPhrase.mnemonicWords, expectedWords);
});
});
}
1 change: 1 addition & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ command:
flutter: ">=3.24.1"
dependencies:
asn1lib: ^1.5.3
bip39: ^1.0.6
bloc_concurrency: ^0.2.2
collection: ^1.18.0
cryptography: ^2.7.0
Expand Down

0 comments on commit 177e2ee

Please sign in to comment.