1
import 'dart:async';
2
3
-import 'package:cw_core/address_info.dart';
3
+import 'package:cw_core/db/sqlite.dart';
4
import 'package:cw_core/hive_type_ids.dart';
5
+import 'package:cw_core/utils/print_verbose.dart';
6
import 'package:cw_core/wallet_type.dart';
6
-import 'package:hive/hive.dart';
7
-
8
-part 'wallet_info.g.dart';
7
+import 'package:sqflite/sqflite.dart';
8
+import 'package:cw_core/cake_hive.dart';
9
+import 'package:cw_core/wallet_info_legacy.dart' as wiLegacy;
10
+
11
+Future<void> performHiveMigration() async {
12
+ try {
13
+ if (!CakeHive.isAdapterRegistered(wiLegacy.WalletInfo.typeId)) {
14
+ CakeHive.registerAdapter(wiLegacy.WalletInfoAdapter());
15
+ }
16
+ if (!CakeHive.isAdapterRegistered(DERIVATION_TYPE_TYPE_ID)) {
17
+ CakeHive.registerAdapter(wiLegacy.DerivationTypeAdapter());
18
+ }
19
+ if (!CakeHive.isAdapterRegistered(wiLegacy.DerivationInfo.typeId)) {
20
+ CakeHive.registerAdapter(wiLegacy.DerivationInfoAdapter());
21
+ }
22
+ if (!CakeHive.isAdapterRegistered(HARDWARE_WALLET_TYPE_TYPE_ID)) {
23
+ CakeHive.registerAdapter(wiLegacy.HardwareWalletTypeAdapter());
24
+ }
25
+ final walletInfoBox = await CakeHive.openBox<wiLegacy.WalletInfo>(wiLegacy.WalletInfo.boxName);
26
+ await wiLegacy.WalletInfo.migrateAllToSqlite(walletInfoBox);
27
+ } catch (e) {
28
+ printV('Error performing Hive migration: $e, continuing anyway');
29
+ }
30
+}
31
10
-@HiveType(typeId: DERIVATION_TYPE_TYPE_ID)
32
enum DerivationType {
12
- @HiveField(0)
33
unknown,
14
- @HiveField(1)
34
def, // default is a reserved word
16
- @HiveField(2)
35
nano,
18
- @HiveField(3)
36
bip39,
20
- @HiveField(4)
37
electrum,
38
}
39
24
-@HiveType(typeId: HARDWARE_WALLET_TYPE_TYPE_ID)
40
enum HardwareWalletType {
26
- @HiveField(0)
41
ledger,
28
- @HiveField(1)
42
bitbox,
30
- @HiveField(2)
43
cupcake,
32
- @HiveField(3)
44
coldcard,
34
- @HiveField(4)
45
seedsigner,
36
- @HiveField(5)
46
keystone,
38
- @HiveField(6)
47
trezor,
48
}
49
42
-@HiveType(typeId: DerivationInfo.typeId)
43
-class DerivationInfo extends HiveObject {
50
+enum WalletInfoAddressType {
51
+ used,
52
+ hidden,
53
+ manual,
54
+}
55
+
56
+class WalletInfoAddressInfo {
57
+ WalletInfoAddressInfo({
58
+ this.id = 0,
59
+ required this.walletInfoId,
60
+ required this.mapKey,
61
+ required this.accountIndex,
62
+ required this.address,
63
+ required this.label,
64
+ });
65
+
66
+ int id;
67
+ int walletInfoId;
68
+ int mapKey;
69
+ int accountIndex;
70
+ String address;
71
+ String label;
72
+
73
+ static String get tableName => 'walletInfoAddressInfo';
74
+ static String get selfIdColumn => "${tableName}Id";
75
+
76
+ static Future<List<WalletInfoAddressInfo>> selectList(int walletInfoId) async {
77
+ final query = await db.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
78
+ return List.generate(query.length, (index) => WalletInfoAddressInfo.fromJson(query[index]));
79
+ }
80
+
81
+ static Future<int> deleteByWalletInfoId(int walletInfoId) async {
82
+ return await db.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
83
+ }
84
+ static Future<int> insert({
85
+ required int walletInfoId,
86
+ required int mapKey,
87
+ required int accountIndex,
88
+ required String address,
89
+ required String label,
90
+ }) async {
91
+ return await db.insert(tableName, {
92
+ "walletInfoId": walletInfoId,
93
+ "mapKey": mapKey,
94
+ "mapValueAccountIndex": accountIndex,
95
+ "mapValueAddress": address,
96
+ "mapValueLabel": label,
97
+ });
98
+ }
99
+
100
+ Map<String, dynamic> toJson() {
101
+ return {
102
+ selfIdColumn: id,
103
+ "walletInfoId": walletInfoId,
104
+ "mapKey": mapKey,
105
+ "mapValueAccountIndex": accountIndex,
106
+ "mapValueAddress": address,
107
+ "mapValueLabel": label,
108
+ };
109
+ }
110
+
111
+ factory WalletInfoAddressInfo.fromJson(Map<String, dynamic> json) {
112
+ return WalletInfoAddressInfo(
113
+ id: json[selfIdColumn] as int,
114
+ walletInfoId: json['walletInfoId'] as int,
115
+ mapKey: json['mapKey'] as int,
116
+ accountIndex: json['mapValueAccountIndex'] as int,
117
+ address: json['mapValueAddress'] as String,
118
+ label: json['mapValueLabel'] as String,
119
+ );
120
+ }
121
+}
122
+
123
+class WalletInfoAddressMap {
124
+ WalletInfoAddressMap({
125
+ required this.id,
126
+ required this.walletInfoId,
127
+ required this.addressKey,
128
+ required this.addressValue,
129
+ });
130
+
131
+
132
+ int id;
133
+ int walletInfoId;
134
+ String addressKey;
135
+ String addressValue;
136
+
137
+ static String get tableName => 'walletInfoAddressMap';
138
+ static String get selfIdColumn => "${tableName}Id";
139
+
140
+ static Future<List<WalletInfoAddressMap>> selectList(int walletInfoId) async {
141
+ final query = await db.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
142
+ return List.generate(query.length, (index) => WalletInfoAddressMap.fromJson(query[index]));
143
+ }
144
+ static Future<int> deleteByWalletInfoId(int walletInfoId) async {
145
+ return await db.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
146
+ }
147
+ static Future<int> insert(int walletInfoId, String addressKey, String addressValue) async {
148
+ return await db.insert(tableName, {
149
+ "walletInfoId": walletInfoId,
150
+ "addressKey": addressKey,
151
+ "addressValue": addressValue,
152
+ });
153
+ }
154
+
155
+ Map<String, dynamic> toJson() {
156
+ return {
157
+ selfIdColumn: id,
158
+ "walletInfoId": walletInfoId,
159
+ "addressKey": addressKey,
160
+ "addressValue": addressValue,
161
+ };
162
+ }
163
+
164
+ factory WalletInfoAddressMap.fromJson(Map<String, dynamic> json) {
165
+ return WalletInfoAddressMap(
166
+ id: json[selfIdColumn] as int,
167
+ walletInfoId: json['walletInfoId'] as int,
168
+ addressKey: json['addressKey'] as String,
169
+ addressValue: json['addressValue'] as String,
170
+ );
171
+ }
172
+}
173
+
174
+class WalletInfoAddress {
175
+ WalletInfoAddress({
176
+ this.id = 0,
177
+ required this.walletInfoId,
178
+ required this.type,
179
+ required this.address,
180
+ });
181
+
182
+ int id;
183
+ int walletInfoId;
184
+ WalletInfoAddressType type;
185
+ String address;
186
+
187
+ static String get tableName => 'walletInfoAddress';
188
+ static String get selfIdColumn => "${tableName}Id";
189
+
190
+ static Future<List<WalletInfoAddress>> selectList(int walletInfoId, WalletInfoAddressType type) async {
191
+ final query = await db.query(tableName, where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]);
192
+ return List.generate(query.length, (index) => WalletInfoAddress.fromJson(query[index]));
193
+ }
194
+
195
+ static Future<int> deleteByAddress(int walletInfoId, WalletInfoAddressType type, String address) async {
196
+ return await db.delete(tableName, where: 'walletInfoId = ? AND type = ? AND address = ?', whereArgs: [walletInfoId, type.index, address]);
197
+ }
198
+
199
+ static Future<int> deleteByType(int walletInfoId, WalletInfoAddressType type) async {
200
+ return await db.delete(tableName, where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]);
201
+ }
202
+
203
+ static Future<int> insert(int walletInfoId, WalletInfoAddressType type, String address) async {
204
+ final select = await db.query(tableName, where: 'walletInfoId = ? AND type = ? AND address = ?', whereArgs: [walletInfoId, type.index, address]);
205
+ if (select.isNotEmpty) {
206
+ return select[0][selfIdColumn] as int;
207
+ }
208
+ return await db.insert(tableName, {
209
+ "walletInfoId": walletInfoId,
210
+ "type": type.index,
211
+ "address": address,
212
+ });
213
+ }
214
+
215
+ Map<String, dynamic> toJson() {
216
+ return {
217
+ selfIdColumn: id,
218
+ "walletInfoId": walletInfoId,
219
+ "type": type.index,
220
+ "address": address,
221
+ };
222
+ }
223
+
224
+ factory WalletInfoAddress.fromJson(Map<String, dynamic> json) {
225
+ return WalletInfoAddress(
226
+ id: json[selfIdColumn] as int,
227
+ walletInfoId: json['walletInfoId'] as int,
228
+ type: WalletInfoAddressType.values[json['type'] as int],
229
+ address: json['address'] as String,
230
+ );
231
+ }
232
+}
233
+
234
+class DerivationInfo {
235
DerivationInfo({
236
+ this.id = 0,
237
this.derivationType,
238
this.derivationPath,
239
this.balance = "",
243
this.description,
244
});
245
54
- static const typeId = DERIVATION_INFO_TYPE_ID;
246
+ int id;
247
56
- @HiveField(0, defaultValue: '')
57
- String address;
248
+ static String get tableName => 'walletInfoDerivationInfo';
249
+ static String get selfIdColumn => "${tableName}Id";
250
59
- @HiveField(1, defaultValue: '')
251
+ String address;
252
String balance;
61
-
62
- @HiveField(2, defaultValue: 0)
253
int transactionsCount;
64
-
65
- @HiveField(3)
254
DerivationType? derivationType;
67
-
68
- @HiveField(4)
255
String? derivationPath;
256
+ String? scriptType;
257
+ String? description;
258
+
259
+ static Future<List<DerivationInfo>> selectList(String where, List<dynamic> whereArgs) async {
260
+ final query = await db.query(
261
+ tableName,
262
+ columns: [
263
+ selfIdColumn,
264
+ 'address',
265
+ 'balance',
266
+ 'transactionsCount',
267
+ 'derivationType',
268
+ 'derivationPath',
269
+ 'scriptType',
270
+ 'description',
271
+ ],
272
+ where: where.isNotEmpty ? where : "1 = 1",
273
+ whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
274
+ );
275
+ return List.generate(query.length, (index) => DerivationInfo.fromJson(query[index]));
276
+ }
277
71
- @HiveField(5)
72
- final String? scriptType;
278
+ Map<String, dynamic> toJson() {
279
+ return {
280
+ selfIdColumn: id,
281
+ "address": address,
282
+ "balance": balance,
283
+ "transactionsCount": transactionsCount,
284
+ "derivationType": derivationType?.index,
285
+ "derivationPath": derivationPath,
286
+ "scriptType": scriptType,
287
+ "description": description,
288
+ };
289
+ }
290
74
- @HiveField(6)
75
- final String? description;
291
+ factory DerivationInfo.fromJson(Map<String, dynamic> json ) {
292
+ return DerivationInfo(
293
+ id: json[selfIdColumn] as int,
294
+ derivationType: DerivationType.values[json['derivationType'] as int? ?? 0],
295
+ derivationPath: json['derivationPath'] as String?,
296
+ balance: json['balance'] as String? ?? "",
297
+ address: json['address'] as String? ?? "",
298
+ transactionsCount: json['transactionsCount'] as int? ?? 0,
299
+ scriptType: json['scriptType'] as String?,
300
+ description: json['description'] as String?,
301
+ );
302
+ }
303
+
304
+ Future<int> save() async {
305
+ final json = toJson();
306
+ if (json[selfIdColumn] == 0) {
307
+ json[selfIdColumn] = null;
308
+ }
309
+ id = await db.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
310
+ return id;
311
+ }
312
}
313
78
-@HiveType(typeId: WalletInfo.typeId)
79
-class WalletInfo extends HiveObject {
314
+class WalletInfo {
315
WalletInfo(
316
+ this.internalId,
317
this.id,
318
this.name,
319
this.type,
326
this.yatEid,
327
this.yatLastUsedAddressRaw,
328
this.showIntroCakePayCard,
93
- this.derivationInfo,
329
+ this.derivationInfoId,
330
this.hardwareWalletType,
331
this.parentAddress,
332
this.hashedWalletIdentifier,
333
this.isNonSeedWallet,
334
+ this.sortOrder,
335
) : _yatLastUsedAddressController = StreamController<String>.broadcast();
336
337
factory WalletInfo.external({
347
bool? showIntroCakePayCard,
348
String yatEid = '',
349
String yatLastUsedAddressRaw = '',
113
- DerivationInfo? derivationInfo,
350
+ int? derivationInfoId,
351
HardwareWalletType? hardwareWalletType,
352
String? parentAddress,
353
String? hashedWalletIdentifier,
354
bool? isNonSeedWallet,
355
+ int? sortOrder,
356
}) {
357
return WalletInfo(
358
+ 0,
359
id,
360
name,
361
type,
368
yatEid,
369
yatLastUsedAddressRaw,
370
showIntroCakePayCard,
132
- derivationInfo,
371
+ derivationInfoId ?? -1,
372
hardwareWalletType,
373
parentAddress,
374
hashedWalletIdentifier,
375
isNonSeedWallet ?? false,
376
+ sortOrder ?? 0,
377
);
378
}
379
140
- static const typeId = WALLET_INFO_TYPE_ID;
141
- static const boxName = 'WalletInfo';
380
+ static String get tableName => 'walletInfo';
381
+ static String get selfIdColumn => "${tableName}Id";
382
143
- @HiveField(0, defaultValue: '')
144
- String id;
383
+ int internalId;
384
146
- @HiveField(1, defaultValue: '')
385
+ String id;
386
String name;
148
-
149
- @HiveField(2)
387
WalletType type;
151
-
152
- @HiveField(3, defaultValue: false)
388
bool isRecovery;
154
-
155
- @HiveField(4, defaultValue: 0)
389
int restoreHeight;
157
-
158
- @HiveField(5, defaultValue: 0)
390
int timestamp;
160
-
161
- @HiveField(6, defaultValue: '')
391
String dirPath;
163
-
164
- @HiveField(7, defaultValue: '')
392
String path;
166
-
167
- @HiveField(8, defaultValue: '')
393
String address;
394
+ Future<Map<String, String>> getAddresses() async {
395
+ final list = await WalletInfoAddressMap.selectList(internalId);
396
+ return Map.fromEntries(list.map((e) => MapEntry(e.addressKey, e.addressValue)));
397
+ }
398
170
- @HiveField(10)
171
- Map<String, String>? addresses;
399
+ Future<void> setAddresses(Map<String, String> addresses) async {
400
+ await WalletInfoAddressMap.deleteByWalletInfoId(internalId);
401
+ final keys = addresses.keys.toList();
402
+ for (final address in keys) {
403
+ await WalletInfoAddressMap.insert(internalId, address, addresses[address]!);
404
+ }
405
+ }
406
173
- @HiveField(11)
407
String? yatEid;
175
-
176
- @HiveField(12)
408
String? yatLastUsedAddressRaw;
178
-
179
- @HiveField(13)
409
bool? showIntroCakePayCard;
410
+ Future<Map<int, List<WalletInfoAddressInfo>>> getAddressInfos() async {
411
+ final list = await WalletInfoAddressInfo.selectList(internalId);
412
+ final ret = <int, List<WalletInfoAddressInfo>>{};
413
+ for (final e in list) {
414
+ ret[e.mapKey] ??= [];
415
+ ret[e.mapKey]!.add(e);
416
+ }
417
+ return ret;
418
+ }
419
182
- @HiveField(14)
183
- Map<int, List<AddressInfo>>? addressInfos;
420
+ Future<void> setAddressInfos(Map<int, List<WalletInfoAddressInfo>> addressInfos) async {
421
+ await WalletInfoAddressInfo.deleteByWalletInfoId(internalId);
422
+ final entries = addressInfos.entries.toList();
423
+ for (final addressInfo in entries) {
424
+ for (final info in addressInfo.value) {
425
+ await WalletInfoAddressInfo.insert(
426
+ walletInfoId: internalId,
427
+ mapKey: addressInfo.key,
428
+ accountIndex: info.accountIndex,
429
+ address: info.address,
430
+ label: info.label,
431
+ );
432
+ }
433
+ }
434
+ }
435
185
- @HiveField(15)
186
- List<String>? usedAddresses;
436
+ Future<Set<String>> getUsedAddresses() async {
437
+ final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.used);
438
+ return list.map((e) => e.address).toSet();
439
+ }
440
+ Future<void> setUsedAddresses(List<String> addresses) async {
441
+ await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.used);
442
+ for (final address in addresses) {
443
+ await WalletInfoAddress.insert(internalId, WalletInfoAddressType.used, address);
444
+ }
445
+ }
446
188
- @deprecated
189
- @HiveField(16)
190
- DerivationType? derivationType; // no longer used
447
+ Future<Set<String>> getHiddenAddresses() async {
448
+ final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.hidden);
449
+ return list.map((e) => e.address).toSet();
450
+ }
451
+ Future<void> setHiddenAddresses(List<String> addresses) async {
452
+ await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.hidden);
453
+ for (final address in addresses) {
454
+ await WalletInfoAddress.insert(internalId, WalletInfoAddressType.hidden, address);
455
+ }
456
+ }
457
192
- @deprecated
193
- @HiveField(17)
194
- String? derivationPath; // no longer used
458
+ Future<Set<String>> getManualAddresses() async {
459
+ final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.manual);
460
+ return list.map((e) => e.address).toSet();
461
+ }
462
+ Future<void> setManualAddresses(List<String> addresses) async {
463
+ await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.manual);
464
+ for (final address in addresses) {
465
+ await WalletInfoAddress.insert(internalId, WalletInfoAddressType.manual, address);
466
+ }
467
+ }
468
196
- @HiveField(18)
197
- String? addressPageType;
469
+ Future<void> addAddress(String address, WalletInfoAddressType type) async {
470
+ await WalletInfoAddress.insert(internalId, type, address);
471
+ }
472
199
- @HiveField(19)
473
+ String? addressPageType;
474
String? network;
201
-
202
- @HiveField(20)
203
- DerivationInfo? derivationInfo;
204
-
205
- @HiveField(21)
475
+ int derivationInfoId;
476
+ DerivationInfo? _derivationInfo;
477
+ Future<DerivationInfo> getDerivationInfo() async {
478
+ if (_derivationInfo != null) {
479
+ return _derivationInfo!;
480
+ }
481
+ final list = await DerivationInfo.selectList('walletInfoDerivationInfoId = ?', [derivationInfoId]);
482
+ if (list.isEmpty) {
483
+ final di = DerivationInfo(
484
+ id: 0,
485
+ derivationType: DerivationType.unknown,
486
+ );
487
+ derivationInfoId = await di.save();
488
+ _derivationInfo = di;
489
+ return di;
490
+ }
491
+ _derivationInfo = list[0];
492
+ return _derivationInfo!;
493
+ }
494
HardwareWalletType? hardwareWalletType;
207
-
208
- @HiveField(22)
495
String? parentAddress;
210
-
211
- @HiveField(23)
212
- List<String>? hiddenAddresses;
213
-
214
- @HiveField(24)
215
- List<String>? manualAddresses;
216
-
217
- @HiveField(25)
496
String? hashedWalletIdentifier;
219
-
220
- @HiveField(26, defaultValue: false)
497
bool isNonSeedWallet;
498
+
499
+ int sortOrder;
500
501
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
502
526
527
StreamController<String> _yatLastUsedAddressController;
528
529
+ Map<String, dynamic> toJson() => {
530
+ selfIdColumn: internalId,
531
+ "id": id,
532
+ "name": name,
533
+ "type": type.index,
534
+ "isRecovery": isRecovery ? 1 : 0,
535
+ "restoreHeight": restoreHeight,
536
+ "timestamp": timestamp,
537
+ "dirPath": dirPath,
538
+ "path": path,
539
+ "address": address,
540
+ "yatEid": yatEid,
541
+ "yatLastUsedAddressRaw": yatLastUsedAddressRaw,
542
+ "showIntroCakePayCard": showIntroCakePayCard == true ? 1 : 0, // SQL regression: null -> false
543
+ "walletInfoDerivationInfoId": derivationInfoId,
544
+ "hardwareWalletType": hardwareWalletType?.index,
545
+ "parentAddress": parentAddress,
546
+ "hashedWalletIdentifier": hashedWalletIdentifier,
547
+ "isNonSeedWallet": isNonSeedWallet ? 1 : 0,
548
+ "sortOrder": sortOrder,
549
+ };
550
+
551
+ factory WalletInfo.fromJson(Map<String, dynamic> json) {
552
+ return WalletInfo(
553
+ json[selfIdColumn] as int,
554
+ json['id'] as String,
555
+ json['name'] as String,
556
+ WalletType.values[json['type'] as int],
557
+ (json['isRecovery'] as int) == 1,
558
+ json['restoreHeight'] as int,
559
+ json['timestamp'] as int,
560
+ json['dirPath'] as String,
561
+ json['path'] as String,
562
+ json['address'] as String,
563
+ json['yatEid'] as String?,
564
+ json['yatLastUsedAddressRaw'] as String?,
565
+ (json['showIntroCakePayCard'] as int) == 1,
566
+ json['walletInfoDerivationInfoId'] as int,
567
+ json['hardwareWalletType'] == null ? null : HardwareWalletType.values[json['hardwareWalletType'] as int],
568
+ json['parentAddress'] as String?,
569
+ json['hashedWalletIdentifier'] as String?,
570
+ (json['isNonSeedWallet'] as int) == 1,
571
+ json['sortOrder'] as int? ?? 0,
572
+ );
573
+ }
574
+
575
+ Future<int> save() async {
576
+ final json = toJson();
577
+ if (json[selfIdColumn] == 0) {
578
+ json[selfIdColumn] = null;
579
+ }
580
+ if (_derivationInfo != null) {
581
+ derivationInfoId = await _derivationInfo!.save();
582
+ }
583
+ internalId = await db.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
584
+ return internalId;
585
+ }
586
+
587
+ static Future<int> delete(WalletInfo walletInfo) async {
588
+ return await db.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
589
+ }
590
+
591
+ static Future<List<WalletInfo>> selectList(String where, List<dynamic> whereArgs, {String orderBy = 'sortOrder'}) async {
592
+ final list = await db.query(
593
+ tableName,
594
+ where: where.isNotEmpty ? where : "1 = 1",
595
+ whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
596
+ orderBy: orderBy,
597
+ );
598
+ return List.generate(list.length, (index) => WalletInfo.fromJson(list[index]));
599
+ }
600
+
601
+ static Future<List<WalletInfo>> getAll() async {
602
+ return selectList('', []);
603
+ }
604
+
605
+ static Future<WalletInfo?> get(String name, WalletType type) async {
606
+ final list = await selectList('name = ? AND type = ?', [name, type.index]);
607
+ if (list.isEmpty) {
608
+ return null;
609
+ }
610
+ return list[0];
611
+ }
612
+
613
Future<void> updateRestoreHeight(int height) async {
614
restoreHeight = height;
615
await save();