Skip to content

Commit f58a5fb

Browse files
CW-723-Add-Monero-support-to-the-Shared-Seed-feature-in-Cake (#2131)
* feat: add exodus style bip39 to monero legacy seed * feat: restore monero wallet from bip39 and add test * bug: fix wrong naming in CI * feat: add monero bip39 UI flow * fix: monero.dart generation * fix: skip monero_wallet_service tests till CI is fixed * ci: copy monero_libwallet2_api_c.so to /usr/lib for testing ci: reduce timeout for cw_monero tests * fix: monero wallet creation credentials default to bip39 if mnemonic are set * fix: do not skip monero wallets services test * fix: Include non bip39 monero wallets on Wallet Group * fix: null pointer stemming from missing language selector if seed is selected * fix: Fixes to Bip39 Creation and restore - Do not restore from 0 for fresh bip39 wallet - disallow restoring bip39 wallet without date or height * fix: Fixes to Bip39 restore - Refresh height is now getting set correctly - Add new create monero wallet tests - Add seed-language English for Bip39 Monero wallets - Fix seed-type naming * feat (cw_monero): Store monero wallet after bip39 creation * feat (cw_monero): remove prints from monero_wallet_service_test.dart * fix: exception during seed language autodetect * feat (cw_monero): Add support for passphrases on bip39 seeds * feat (cw_monero): Add support for passphrases on bip39 seeds * fix: seed language selection for recovering bip39 wallets * style: improve readability of isLegacySeedOnly in wallet_keys_view_model.dart * feat: hide monero seed type selector from advanced settings when creating a child wallet * fix(cw_monero): use named arguments for bip39_seed tests --------- Co-authored-by: cyan <cyjan@mrcyjanek.net>
1 parent 4942072 commit f58a5fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+702
-283
lines changed

.github/workflows/pr_test_build_linux.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ jobs:
283283
xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &
284284
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
285285
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart
286+
- name: Test [cw_monero]
287+
timeout-minutes: 2
288+
run: cd cw_monero && flutter test
286289
- name: Stop screen recording, encrypt and upload
287290
if: always()
288291
run: |

cw_monero/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ android/.externalNativeBuild/
1111
android/.cxx/
1212

1313
macos/cw_monero.podspec
14-
macos/External/
14+
macos/External/
15+
*monero_libwallet2_api_c.*

cw_monero/lib/api/wallet.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ String getSeed() {
6262
}
6363
return cakepolyseed;
6464
}
65+
66+
final bip39 = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed.bip39");
67+
68+
if(bip39.isNotEmpty) return bip39;
69+
6570
final legacy = getSeedLegacy(null);
6671
return legacy;
6772
}

cw_monero/lib/bip39_seed.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'dart:typed_data';
2+
3+
import 'package:bip32/bip32.dart' as bip32;
4+
import 'package:bip39/bip39.dart' as bip39;
5+
import 'package:polyseed/polyseed.dart';
6+
7+
bool isBip39Seed(String mnemonic) => bip39.validateMnemonic(mnemonic);
8+
9+
String getBip39Seed() => bip39.generateMnemonic();
10+
11+
String getLegacySeedFromBip39(String mnemonic,
12+
{int accountIndex = 0, String passphrase = ""}) {
13+
final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase);
14+
15+
final bip32KeyPair =
16+
bip32.BIP32.fromSeed(seed).derivePath("m/44'/128'/$accountIndex'/0/0");
17+
18+
final spendKey = _reduceECKey(bip32KeyPair.privateKey!);
19+
20+
return LegacySeedLang.getByEnglishName("English")
21+
.encodePhrase(spendKey.toHexString());
22+
}
23+
24+
const _ed25519CurveOrder =
25+
"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED";
26+
27+
Uint8List _reduceECKey(Uint8List buffer) {
28+
final curveOrder = BigInt.parse(_ed25519CurveOrder, radix: 16);
29+
final bigNumber = _readBytes(buffer);
30+
31+
var result = bigNumber % curveOrder;
32+
33+
final resultBuffer = Uint8List(32);
34+
for (var i = 0; i < 32; i++) {
35+
resultBuffer[i] = (result & BigInt.from(0xff)).toInt();
36+
result = result >> 8;
37+
}
38+
39+
return resultBuffer;
40+
}
41+
42+
/// Read BigInt from a little-endian Uint8List
43+
/// From https://github.com/dart-lang/sdk/issues/32803#issuecomment-387405784
44+
BigInt _readBytes(Uint8List bytes) {
45+
BigInt read(int start, int end) {
46+
if (end - start <= 4) {
47+
var result = 0;
48+
for (int i = end - 1; i >= start; i--) {
49+
result = result * 256 + bytes[i];
50+
}
51+
return BigInt.from(result);
52+
}
53+
final mid = start + ((end - start) >> 1);
54+
return read(start, mid) +
55+
read(mid, end) * (BigInt.one << ((mid - start) * 8));
56+
}
57+
58+
return read(0, bytes.length);
59+
}

cw_monero/lib/monero_wallet_service.dart

Lines changed: 126 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:ffi';
22
import 'dart:io';
33

4+
import 'package:collection/collection.dart';
45
import 'package:cw_core/get_height_by_date.dart';
56
import 'package:cw_core/monero_wallet_utils.dart';
67
import 'package:cw_core/pathForWallet.dart';
@@ -14,29 +15,38 @@ import 'package:cw_core/wallet_type.dart';
1415
import 'package:cw_monero/api/account_list.dart';
1516
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
1617
import 'package:cw_monero/api/wallet_manager.dart';
18+
import 'package:cw_monero/bip39_seed.dart';
1719
import 'package:cw_monero/ledger.dart';
1820
import 'package:cw_monero/monero_wallet.dart';
19-
import 'package:collection/collection.dart';
2021
import 'package:hive/hive.dart';
2122
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
2223
import 'package:monero/monero.dart' as monero;
2324
import 'package:polyseed/polyseed.dart';
2425

26+
enum MoneroSeedType { polyseed, legacy, bip39 }
27+
2528
class MoneroNewWalletCredentials extends WalletCredentials {
2629
MoneroNewWalletCredentials(
27-
{required String name, required this.language, required this.isPolyseed, String? password, this.passphrase})
30+
{required String name,
31+
required this.language,
32+
required this.seedType,
33+
String? password,
34+
this.passphrase,
35+
this.mnemonic})
2836
: super(name: name, password: password);
2937

3038
final String language;
31-
final bool isPolyseed;
39+
final MoneroSeedType seedType;
3240
final String? passphrase;
41+
final String? mnemonic;
3342
}
3443

3544
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
36-
MoneroRestoreWalletFromHardwareCredentials({required String name,
37-
required this.ledgerConnection,
38-
int height = 0,
39-
String? password})
45+
MoneroRestoreWalletFromHardwareCredentials(
46+
{required String name,
47+
required this.ledgerConnection,
48+
int height = 0,
49+
String? password})
4050
: super(name: name, password: password, height: height);
4151
LedgerConnection ledgerConnection;
4252
}
@@ -60,13 +70,14 @@ class MoneroWalletLoadingException implements Exception {
6070
}
6171

6272
class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
63-
MoneroRestoreWalletFromKeysCredentials({required String name,
64-
required String password,
65-
required this.language,
66-
required this.address,
67-
required this.viewKey,
68-
required this.spendKey,
69-
int height = 0})
73+
MoneroRestoreWalletFromKeysCredentials(
74+
{required String name,
75+
required String password,
76+
required this.language,
77+
required this.address,
78+
required this.viewKey,
79+
required this.spendKey,
80+
int height = 0})
7081
: super(name: name, password: password, height: height);
7182

7283
final String language;
@@ -97,27 +108,42 @@ class MoneroWalletService extends WalletService<
97108
@override
98109
WalletType getType() => WalletType.monero;
99110

100-
@override
101-
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async {
111+
@override
112+
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials,
113+
{bool? isTestnet}) async {
102114
try {
103115
final path = await pathForWallet(name: credentials.name, type: getType());
104116

105-
if (credentials.isPolyseed) {
117+
if (credentials.seedType == MoneroSeedType.bip39) {
118+
return _restoreFromBip39(
119+
path: path,
120+
password: credentials.password!,
121+
mnemonic: credentials.mnemonic ?? getBip39Seed(),
122+
passphrase: credentials.passphrase,
123+
walletInfo: credentials.walletInfo!,
124+
);
125+
}
126+
127+
if (credentials.seedType == MoneroSeedType.polyseed) {
106128
final polyseed = Polyseed.create();
107129
final lang = PolyseedLang.getByEnglishName(credentials.language);
108130

109-
if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!);
131+
if (credentials.passphrase != null)
132+
polyseed.crypt(credentials.passphrase!);
110133

111-
final heightOverride =
112-
getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2)));
134+
final heightOverride = getMoneroHeigthByDate(
135+
date: DateTime.now().subtract(Duration(days: 2)));
113136

114-
return _restoreFromPolyseed(
115-
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
137+
return _restoreFromPolyseed(path, credentials.password!, polyseed,
138+
credentials.walletInfo!, lang,
116139
overrideHeight: heightOverride, passphrase: credentials.passphrase);
117140
}
118141

119142
await monero_wallet_manager.createWallet(
120-
path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??"");
143+
path: path,
144+
password: credentials.password!,
145+
language: credentials.language,
146+
passphrase: credentials.passphrase ?? "");
121147
final wallet = MoneroWallet(
122148
walletInfo: credentials.walletInfo!,
123149
unspentCoinsInfo: unspentCoinsInfoSource,
@@ -145,7 +171,8 @@ class MoneroWalletService extends WalletService<
145171
}
146172

147173
@override
148-
Future<MoneroWallet> openWallet(String name, String password, {OpenWalletTry openWalletTry = OpenWalletTry.initial}) async {
174+
Future<MoneroWallet> openWallet(String name, String password,
175+
{OpenWalletTry openWalletTry = OpenWalletTry.initial}) async {
149176
try {
150177
final path = await pathForWallet(name: name, type: getType());
151178

@@ -303,8 +330,28 @@ class MoneroWalletService extends WalletService<
303330
rethrow;
304331
}
305332

333+
try {
334+
if (isBip39Seed(credentials.mnemonic)) {
335+
final path =
336+
await pathForWallet(name: credentials.name, type: getType());
337+
338+
return _restoreFromBip39(
339+
path: path,
340+
password: credentials.password!,
341+
mnemonic: credentials.mnemonic,
342+
walletInfo: credentials.walletInfo!,
343+
overrideHeight: credentials.height!,
344+
passphrase: credentials.passphrase,
345+
);
346+
}
347+
} catch (e) {
348+
printV("Bip39 restore failed: $e");
349+
rethrow;
350+
}
351+
306352
try {
307353
final path = await pathForWallet(name: credentials.name, type: getType());
354+
308355
monero_wallet_manager.restoreFromSeed(
309356
path: path,
310357
password: credentials.password!,
@@ -325,6 +372,50 @@ class MoneroWalletService extends WalletService<
325372
}
326373
}
327374

375+
Future<MoneroWallet> _restoreFromBip39({
376+
required String path,
377+
required String password,
378+
required String mnemonic,
379+
required WalletInfo walletInfo,
380+
String? passphrase,
381+
int? overrideHeight,
382+
}) async {
383+
walletInfo.derivationInfo = DerivationInfo(
384+
derivationType: DerivationType.bip39,
385+
derivationPath: "m/44'/128'/0'/0/0",
386+
);
387+
388+
final legacyMnemonic =
389+
getLegacySeedFromBip39(mnemonic, passphrase: passphrase ?? "");
390+
final height =
391+
overrideHeight ?? getMoneroHeigthByDate(date: DateTime.now());
392+
393+
walletInfo.isRecovery = true;
394+
walletInfo.restoreHeight = height;
395+
396+
monero_wallet_manager.restoreFromSeed(
397+
path: path,
398+
password: password,
399+
passphrase: '',
400+
seed: legacyMnemonic,
401+
restoreHeight: height,
402+
);
403+
404+
monero.Wallet_setCacheAttribute(wptr!,
405+
key: "cakewallet.seed.bip39", value: mnemonic);
406+
407+
monero.Wallet_store(wptr!);
408+
409+
final wallet = MoneroWallet(
410+
walletInfo: walletInfo,
411+
unspentCoinsInfo: unspentCoinsInfoSource,
412+
password: password,
413+
);
414+
await wallet.init();
415+
416+
return wallet;
417+
}
418+
328419
Future<MoneroWallet> restoreFromPolyseed(
329420
MoneroRestoreWalletFromSeedCredentials credentials) async {
330421
try {
@@ -344,23 +435,21 @@ class MoneroWalletService extends WalletService<
344435
}
345436
}
346437

347-
Future<MoneroWallet> _restoreFromPolyseed(
348-
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
438+
Future<MoneroWallet> _restoreFromPolyseed(String path, String password,
439+
Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
349440
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO,
350441
int? overrideHeight,
351442
String? passphrase}) async {
352-
353-
if (polyseed.isEncrypted == false &&
354-
(passphrase??'') != "") {
443+
if (polyseed.isEncrypted == false && (passphrase ?? '') != "") {
355444
// Fallback to the different passphrase offset method, when a passphrase
356445
// was provided but the polyseed is not encrypted.
357446
monero_wallet_manager.restoreWalletFromPolyseedWithOffset(
358-
path: path,
359-
password: password,
360-
seed: polyseed.encode(lang, coin),
361-
seedOffset: passphrase??'',
362-
language: "English");
363-
447+
path: path,
448+
password: password,
449+
seed: polyseed.encode(lang, coin),
450+
seedOffset: passphrase ?? '',
451+
language: "English");
452+
364453
final wallet = MoneroWallet(
365454
walletInfo: walletInfo,
366455
unspentCoinsInfo: unspentCoinsInfoSource,
@@ -437,7 +526,8 @@ class MoneroWalletService extends WalletService<
437526

438527
if (walletFilesExist(path)) await repairOldAndroidWallet(name);
439528

440-
await monero_wallet_manager.openWalletAsync({'path': path, 'password': password});
529+
await monero_wallet_manager
530+
.openWalletAsync({'path': path, 'password': password});
441531
final walletInfo = walletInfoSource.values
442532
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
443533
final wallet = MoneroWallet(

0 commit comments

Comments
 (0)