Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
134ffae
feat: add key-based wallet restore interface and data model
sneurlax Feb 21, 2026
1cc2bbf
feat: add key-based recovery path for Monero wallets
sneurlax Feb 21, 2026
dd9f73e
feat: add URI restore option to Monero wallet restore UI
sneurlax Feb 21, 2026
3fa83a6
feat: add blockheight to rescan confirm dialog and wallet recovery
sneurlax Mar 5, 2026
0f12521
feat: add date and block height picker to rescan blockchain dialog
sneurlax Mar 5, 2026
4f108bf
feat: restrict rescan height picker to cryptonote and mimblewimble coins
sneurlax Mar 5, 2026
bc101af
refactor: extract date/height picker into StartHeightPicker widget
sneurlax Mar 5, 2026
65d0321
feat: add URI restore to restore options, using StartHeightPickerCont…
sneurlax Mar 5, 2026
09bd34a
feat: add generic WalletUriData class and wallet URI parser
sneurlax Feb 21, 2026
33b1bd9
fix: guard against short addresses
sneurlax Feb 22, 2026
e94ea75
feat: use height param
sneurlax Feb 22, 2026
59c5f9c
fix: fix non-view-only keys-only wallet keys dialog
sneurlax Feb 22, 2026
aa72cce
refactor: use StartHeightPickerController for URI restore
sneurlax Mar 5, 2026
c66cea2
Merge branch 'feat/monero-uri' into feat/rescan-height
sneurlax Mar 6, 2026
246b6f3
fix: deduplicate _parseWalletUri; use dash-normalizing version
sneurlax Mar 6, 2026
054bb05
feat: add initialMnemonic param to RestoreWalletView
sneurlax Mar 6, 2026
758ac2e
feat: add initial params and keys-restore to RestoreViewOnlyWalletView
sneurlax Mar 6, 2026
b4697d5
refactor: replace inline _doUriRestore with navigation to existing views
sneurlax Mar 6, 2026
b3d9f91
fix: use selected date for Salvium block-height lookup
sneurlax May 27, 2026
230b247
chore: dart format
sneurlax May 28, 2026
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget {
required this.walletName,
required this.coin,
required this.restoreBlockHeight,
this.initialAddress,
this.initialViewKey,
this.initialSpendKey,
this.clipboard = const ClipboardWrapper(),
});

Expand All @@ -56,6 +59,9 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget {
final String walletName;
final CryptoCurrency coin;
final int restoreBlockHeight;
final String? initialAddress;
final String? initialViewKey;
final String? initialSpendKey;
final ClipboardInterface clipboard;

@override
Expand Down Expand Up @@ -102,21 +108,23 @@ class _RestoreViewOnlyWalletViewState
}

Future<void> _attemptRestore() async {
final Map<String, dynamic> otherDataJson = {
WalletInfoKeys.isViewOnlyKey: true,
};
final bool isKeysRestore = widget.initialSpendKey != null;

ViewOnlyWalletType viewOnlyWalletType = _walletType;
if (widget.coin is Bip39HDCurrency) {
// already set above
} else if (widget.coin is CryptonoteCurrency) {
viewOnlyWalletType = ViewOnlyWalletType.cryptonote;
final Map<String, dynamic> otherDataJson;
if (isKeysRestore) {
otherDataJson = {WalletInfoKeys.isRestoredFromKeysKey: true};
} else {
throw Exception(
"Unsupported view only wallet currency type found: ${widget.coin.runtimeType}",
);
otherDataJson = {WalletInfoKeys.isViewOnlyKey: true};

if (widget.coin is! Bip39HDCurrency &&
widget.coin is! CryptonoteCurrency) {
throw Exception(
"Unsupported view only wallet currency type found:"
" ${widget.coin.runtimeType}",
);
}
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] = _walletType.index;
}
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] = _walletType.index;

if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.enable();

Expand All @@ -126,7 +134,11 @@ class _RestoreViewOnlyWalletViewState
name: widget.walletName,
restoreHeight: widget.restoreBlockHeight,
otherDataJsonString: jsonEncode(otherDataJson),
overrideAddressType: viewOnlyWalletType == .spark ? .spark : null,
overrideAddressType: isKeysRestore
? null
: _walletType == .spark
? .spark
: null,
);

bool isRestoring = true;
Expand All @@ -153,53 +165,6 @@ class _RestoreViewOnlyWalletViewState
);
}

final ViewOnlyWalletData viewOnlyData;
switch (viewOnlyWalletType) {
case ViewOnlyWalletType.cryptonote:
if (addressController.text.isEmpty ||
viewKeyController.text.isEmpty) {
throw Exception("Missing address and/or private view key fields");
}
viewOnlyData = CryptonoteViewOnlyWalletData(
walletId: info.walletId,
address: addressController.text,
privateViewKey: viewKeyController.text,
);
break;

case ViewOnlyWalletType.addressOnly:
if (addressController.text.isEmpty) {
throw Exception("Address is empty");
}
viewOnlyData = AddressViewOnlyWalletData(
walletId: info.walletId,
address: addressController.text,
);
break;

case ViewOnlyWalletType.xPub:
viewOnlyData = ExtendedKeysViewOnlyWalletData(
walletId: info.walletId,
xPubs: [
XPub(
path: _currentDropDownValue,
encoded: viewKeyController.text,
),
],
);
break;

case ViewOnlyWalletType.spark:
if (sparkViewKeyController.text.isEmpty) {
throw Exception("Spark View Key is empty");
}
viewOnlyData = SparkViewOnlyWalletData(
walletId: info.walletId,
viewKey: sparkViewKeyController.text,
);
break;
}

var node = ref
.read(nodeServiceChangeNotifierProvider)
.getPrimaryNodeFor(currency: widget.coin);
Expand All @@ -212,14 +177,80 @@ class _RestoreViewOnlyWalletViewState
}

try {
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
viewOnlyData: viewOnlyData,
);
final Wallet wallet;
if (isKeysRestore) {
final keysRestoreData = jsonEncode({
"address": addressController.text,
"viewKey": viewKeyController.text,
"spendKey": widget.initialSpendKey!,
});
wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
keysRestoreData: keysRestoreData,
);
} else {
final ViewOnlyWalletData viewOnlyData;
switch (_walletType) {
case ViewOnlyWalletType.cryptonote:
if (addressController.text.isEmpty ||
viewKeyController.text.isEmpty) {
throw Exception(
"Missing address and/or private view key fields",
);
}
viewOnlyData = CryptonoteViewOnlyWalletData(
walletId: info.walletId,
address: addressController.text,
privateViewKey: viewKeyController.text,
);
break;

case ViewOnlyWalletType.addressOnly:
if (addressController.text.isEmpty) {
throw Exception("Address is empty");
}
viewOnlyData = AddressViewOnlyWalletData(
walletId: info.walletId,
address: addressController.text,
);
break;

case ViewOnlyWalletType.xPub:
viewOnlyData = ExtendedKeysViewOnlyWalletData(
walletId: info.walletId,
xPubs: [
XPub(
path: _currentDropDownValue,
encoded: viewKeyController.text,
),
],
);
break;

case ViewOnlyWalletType.spark:
if (sparkViewKeyController.text.isEmpty) {
throw Exception("Spark View Key is empty");
}
viewOnlyData = SparkViewOnlyWalletData(
walletId: info.walletId,
viewKey: sparkViewKeyController.text,
);
break;
}

wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
viewOnlyData: viewOnlyData,
);
}

// TODO: extract interface with isRestore param
switch (wallet) {
Expand Down Expand Up @@ -314,8 +345,12 @@ class _RestoreViewOnlyWalletViewState
@override
void initState() {
super.initState();
addressController = TextEditingController();
viewKeyController = TextEditingController();
addressController = TextEditingController(
text: widget.initialAddress ?? '',
);
viewKeyController = TextEditingController(
text: widget.initialViewKey ?? '',
);
sparkViewKeyController = TextEditingController();

if (widget.coin is Bip39HDCurrency) {
Expand All @@ -326,6 +361,10 @@ class _RestoreViewOnlyWalletViewState
} else if (widget.coin is CryptonoteCurrency) {
_walletType = ViewOnlyWalletType.cryptonote;
}

if (widget.initialAddress != null || widget.initialViewKey != null) {
_enableRestoreButton = true;
}
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class RestoreWalletView extends ConsumerStatefulWidget {
required this.seedWordsLength,
required this.mnemonicPassphrase,
required this.restoreBlockHeight,
this.initialMnemonic,
this.clipboard = const ClipboardWrapper(),
});

Expand All @@ -90,6 +91,7 @@ class RestoreWalletView extends ConsumerStatefulWidget {
final String mnemonicPassphrase;
final int seedWordsLength;
final int restoreBlockHeight;
final String? initialMnemonic;

final ClipboardInterface clipboard;

Expand Down Expand Up @@ -163,6 +165,12 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
// _focusNodes.add(FocusNode());
}

if (widget.initialMnemonic != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_clearAndPopulateMnemonic(widget.initialMnemonic!.split(' '));
});
}

super.initState();
}

Expand Down
Loading
Loading