Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d469a91
feat: add exodus style bip39 to monero legacy seed
konstantinullrich Mar 27, 2025
28b4ef0
feat: restore monero wallet from bip39 and add test
konstantinullrich Mar 28, 2025
705bf76
bug: fix wrong naming in CI
konstantinullrich Mar 28, 2025
9ee0cd7
feat: add monero bip39 UI flow
konstantinullrich Mar 28, 2025
b22bfca
Merge branch 'refs/heads/main' into CW-723-Add-Monero-support-to-the-…
konstantinullrich Mar 28, 2025
d891f6d
fix: monero.dart generation
konstantinullrich Mar 28, 2025
3f109c9
fix: skip monero_wallet_service tests till CI is fixed
konstantinullrich Mar 28, 2025
993143a
ci: copy monero_libwallet2_api_c.so to /usr/lib for testing
MrCyjaneK Mar 28, 2025
3b39da5
Merge branch 'main' into CW-723-Add-Monero-support-to-the-Shared-Seed…
konstantinullrich Mar 29, 2025
eea3558
fix: monero wallet creation credentials default to bip39 if mnemonic …
konstantinullrich Mar 30, 2025
6515383
Merge branch 'main' into CW-723-Add-Monero-support-to-the-Shared-Seed…
konstantinullrich Mar 30, 2025
63de3bd
fix: do not skip monero wallets services test
konstantinullrich Mar 30, 2025
ebc152a
fix: Include non bip39 monero wallets on Wallet Group
konstantinullrich Mar 31, 2025
56d1db2
fix: null pointer stemming from missing language selector if seed is …
konstantinullrich Mar 31, 2025
41c2211
fix: Fixes to Bip39 Creation and restore
konstantinullrich Apr 1, 2025
281e19c
fix: Fixes to Bip39 restore
konstantinullrich Apr 1, 2025
775b09f
Merge branch 'main' into CW-723-Add-Monero-support-to-the-Shared-Seed…
konstantinullrich Apr 8, 2025
22b56f3
feat (cw_monero): Store monero wallet after bip39 creation
konstantinullrich Apr 8, 2025
ed77a78
feat (cw_monero): remove prints from monero_wallet_service_test.dart
konstantinullrich Apr 8, 2025
8a6bef7
Merge branch 'main' into CW-723-Add-Monero-support-to-the-Shared-Seed…
konstantinullrich Apr 9, 2025
c3b6448
fix: exception during seed language autodetect
konstantinullrich Apr 9, 2025
0aa1ba8
feat (cw_monero): Add support for passphrases on bip39 seeds
konstantinullrich Apr 9, 2025
74feb91
feat (cw_monero): Add support for passphrases on bip39 seeds
konstantinullrich Apr 9, 2025
11c070e
fix: seed language selection for recovering bip39 wallets
konstantinullrich Apr 9, 2025
a419ba4
style: improve readability of isLegacySeedOnly in wallet_keys_view_mo…
konstantinullrich Apr 9, 2025
7ba6f85
feat: hide monero seed type selector from advanced settings when crea…
konstantinullrich Apr 9, 2025
44a079a
fix(cw_monero): use named arguments for bip39_seed tests
konstantinullrich Apr 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr_test_build_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ jobs:
xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
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
- name: Test [cw_monero]
timeout-minutes: 2
run: cd cw_monero && flutter test
- name: Stop screen recording, encrypt and upload
if: always()
run: |
Expand Down
3 changes: 2 additions & 1 deletion cw_monero/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ android/.externalNativeBuild/
android/.cxx/

macos/cw_monero.podspec
macos/External/
macos/External/
*monero_libwallet2_api_c.*
5 changes: 5 additions & 0 deletions cw_monero/lib/api/wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ String getSeed() {
}
return cakepolyseed;
}

final bip39 = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed.bip39");

if(bip39.isNotEmpty) return bip39;

final legacy = getSeedLegacy(null);
return legacy;
}
Expand Down
58 changes: 58 additions & 0 deletions cw_monero/lib/bip39_seed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:typed_data';

import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:polyseed/polyseed.dart';

bool isBip39Seed(String mnemonic) => bip39.validateMnemonic(mnemonic);

String getBip39Seed() => bip39.generateMnemonic();

String getLegacySeedFromBip39(String mnemonic, [int accountIndex = 0]) {
final seed = bip39.mnemonicToSeed(mnemonic);

final bip32KeyPair =
bip32.BIP32.fromSeed(seed).derivePath("m/44'/128'/$accountIndex'/0/0");

final spendKey = _reduceECKey(bip32KeyPair.privateKey!);

return LegacySeedLang.getByEnglishName("English")
.encodePhrase(spendKey.toHexString());
}

const _ed25519CurveOrder =
"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED";

Uint8List _reduceECKey(Uint8List buffer) {
final curveOrder = BigInt.parse(_ed25519CurveOrder, radix: 16);
final bigNumber = _readBytes(buffer);

var result = bigNumber % curveOrder;

final resultBuffer = Uint8List(32);
for (var i = 0; i < 32; i++) {
resultBuffer[i] = (result & BigInt.from(0xff)).toInt();
result = result >> 8;
}

return resultBuffer;
}

/// Read BigInt from a little-endian Uint8List
/// From https://github.com/dart-lang/sdk/issues/32803#issuecomment-387405784
BigInt _readBytes(Uint8List bytes) {
BigInt read(int start, int end) {
if (end - start <= 4) {
var result = 0;
for (int i = end - 1; i >= start; i--) {
result = result * 256 + bytes[i];
}
return BigInt.from(result);
}
final mid = start + ((end - start) >> 1);
return read(start, mid) +
read(mid, end) * (BigInt.one << ((mid - start) * 8));
}

return read(0, bytes.length);
}
158 changes: 122 additions & 36 deletions cw_monero/lib/monero_wallet_service.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:ffi';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/monero_wallet_utils.dart';
import 'package:cw_core/pathForWallet.dart';
Expand All @@ -14,29 +15,38 @@ import 'package:cw_core/wallet_type.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
import 'package:cw_monero/api/wallet_manager.dart';
import 'package:cw_monero/bip39_seed.dart';
import 'package:cw_monero/ledger.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:collection/collection.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:monero/monero.dart' as monero;
import 'package:polyseed/polyseed.dart';

enum MoneroSeedType { polyseed, legacy, bip39 }

class MoneroNewWalletCredentials extends WalletCredentials {
MoneroNewWalletCredentials(
{required String name, required this.language, required this.isPolyseed, String? password, this.passphrase})
{required String name,
required this.language,
required this.seedType,
String? password,
this.passphrase,
this.mnemonic})
: super(name: name, password: password);

final String language;
final bool isPolyseed;
final MoneroSeedType seedType;
final String? passphrase;
final String? mnemonic;
}

class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
MoneroRestoreWalletFromHardwareCredentials({required String name,
required this.ledgerConnection,
int height = 0,
String? password})
MoneroRestoreWalletFromHardwareCredentials(
{required String name,
required this.ledgerConnection,
int height = 0,
String? password})
: super(name: name, password: password, height: height);
LedgerConnection ledgerConnection;
}
Expand All @@ -60,13 +70,14 @@ class MoneroWalletLoadingException implements Exception {
}

class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
MoneroRestoreWalletFromKeysCredentials({required String name,
required String password,
required this.language,
required this.address,
required this.viewKey,
required this.spendKey,
int height = 0})
MoneroRestoreWalletFromKeysCredentials(
{required String name,
required String password,
required this.language,
required this.address,
required this.viewKey,
required this.spendKey,
int height = 0})
: super(name: name, password: password, height: height);

final String language;
Expand Down Expand Up @@ -97,27 +108,41 @@ class MoneroWalletService extends WalletService<
@override
WalletType getType() => WalletType.monero;

@override
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async {
@override
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials,
{bool? isTestnet}) async {
try {
final path = await pathForWallet(name: credentials.name, type: getType());

if (credentials.isPolyseed) {
if (credentials.seedType == MoneroSeedType.bip39) {
return _restoreFromBip39(
path: path,
password: credentials.password!,
mnemonic: credentials.mnemonic ?? getBip39Seed(),
walletInfo: credentials.walletInfo!,
);
}

if (credentials.seedType == MoneroSeedType.polyseed) {
final polyseed = Polyseed.create();
final lang = PolyseedLang.getByEnglishName(credentials.language);

if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!);
if (credentials.passphrase != null)
polyseed.crypt(credentials.passphrase!);

final heightOverride =
getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2)));
final heightOverride = getMoneroHeigthByDate(
date: DateTime.now().subtract(Duration(days: 2)));

return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
return _restoreFromPolyseed(path, credentials.password!, polyseed,
credentials.walletInfo!, lang,
overrideHeight: heightOverride, passphrase: credentials.passphrase);
}

await monero_wallet_manager.createWallet(
path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??"");
path: path,
password: credentials.password!,
language: credentials.language,
passphrase: credentials.passphrase ?? "");
final wallet = MoneroWallet(
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
Expand Down Expand Up @@ -145,7 +170,8 @@ class MoneroWalletService extends WalletService<
}

@override
Future<MoneroWallet> openWallet(String name, String password, {OpenWalletTry openWalletTry = OpenWalletTry.initial}) async {
Future<MoneroWallet> openWallet(String name, String password,
{OpenWalletTry openWalletTry = OpenWalletTry.initial}) async {
try {
final path = await pathForWallet(name: name, type: getType());

Expand Down Expand Up @@ -303,8 +329,27 @@ class MoneroWalletService extends WalletService<
rethrow;
}

try {
if (isBip39Seed(credentials.mnemonic)) {
final path =
await pathForWallet(name: credentials.name, type: getType());

return _restoreFromBip39(
path: path,
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
overrideHeight: credentials.height!,
);
}
} catch (e) {
printV("Bip39 restore failed: $e");
rethrow;
}

try {
final path = await pathForWallet(name: credentials.name, type: getType());

monero_wallet_manager.restoreFromSeed(
path: path,
password: credentials.password!,
Expand All @@ -325,6 +370,48 @@ class MoneroWalletService extends WalletService<
}
}

Future<MoneroWallet> _restoreFromBip39({
required String path,
required String password,
required String mnemonic,
required WalletInfo walletInfo,
int? overrideHeight
}) async {
walletInfo.derivationInfo = DerivationInfo(
derivationType: DerivationType.bip39,
derivationPath: "m/44'/128'/0'/0/0",
);

final legacyMnemonic = getLegacySeedFromBip39(mnemonic);
final height = overrideHeight ??
getMoneroHeigthByDate(date: DateTime.now());

walletInfo.isRecovery = true;
walletInfo.restoreHeight = height;

monero_wallet_manager.restoreFromSeed(
path: path,
password: password,
passphrase: '',
seed: legacyMnemonic,
restoreHeight: height,
);

monero.Wallet_setCacheAttribute(wptr!,
key: "cakewallet.seed.bip39", value: mnemonic);

monero.Wallet_store(wptr!);

final wallet = MoneroWallet(
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
password: password,
);
await wallet.init();

return wallet;
}

Future<MoneroWallet> restoreFromPolyseed(
MoneroRestoreWalletFromSeedCredentials credentials) async {
try {
Expand All @@ -344,23 +431,21 @@ class MoneroWalletService extends WalletService<
}
}

Future<MoneroWallet> _restoreFromPolyseed(
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
Future<MoneroWallet> _restoreFromPolyseed(String path, String password,
Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO,
int? overrideHeight,
String? passphrase}) async {

if (polyseed.isEncrypted == false &&
(passphrase??'') != "") {
if (polyseed.isEncrypted == false && (passphrase ?? '') != "") {
// Fallback to the different passphrase offset method, when a passphrase
// was provided but the polyseed is not encrypted.
monero_wallet_manager.restoreWalletFromPolyseedWithOffset(
path: path,
password: password,
seed: polyseed.encode(lang, coin),
seedOffset: passphrase??'',
language: "English");
path: path,
password: password,
seed: polyseed.encode(lang, coin),
seedOffset: passphrase ?? '',
language: "English");

final wallet = MoneroWallet(
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
Expand Down Expand Up @@ -437,7 +522,8 @@ class MoneroWalletService extends WalletService<

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

await monero_wallet_manager.openWalletAsync({'path': path, 'password': password});
await monero_wallet_manager
.openWalletAsync({'path': path, 'password': password});
final walletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = MoneroWallet(
Expand Down
Loading
Loading