dev
dart 667 lines 19.5 KB
Raw
1 import 'dart:async';
2
3 import 'package:cw_core/cake_hive.dart';
4 import 'package:cw_core/db/sqlite.dart';
5 import 'package:cw_core/hive_type_ids.dart';
6 import 'package:cw_core/utils/print_verbose.dart';
7 import 'package:cw_core/wallet_info_legacy.dart' as wiLegacy;
8 import 'package:cw_core/wallet_type.dart';
9 import 'package:sqflite/sqflite.dart';
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
32 enum DerivationType {
33 unknown,
34 def, // default is a reserved word
35 nano,
36 bip39,
37 electrum,
38 }
39
40 enum HardwareWalletType {
41 ledger,
42 bitbox,
43 cupcake,
44 coldcard,
45 seedsigner,
46 keystone,
47 trezor;
48 }
49
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
75 static String get selfIdColumn => "${tableName}Id";
76
77 static Future<List<WalletInfoAddressInfo>> selectList(int walletInfoId) async {
78 final query = await db!.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
79 return List.generate(query.length, (index) => WalletInfoAddressInfo.fromJson(query[index]));
80 }
81
82 static Future<int> deleteByWalletInfoId(int walletInfoId) async {
83 return await db!.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
84 }
85
86 static Future<int> insert({
87 required int walletInfoId,
88 required int mapKey,
89 required int accountIndex,
90 required String address,
91 required String label,
92 }) async {
93 return await db!.insert(tableName, {
94 "walletInfoId": walletInfoId,
95 "mapKey": mapKey,
96 "mapValueAccountIndex": accountIndex,
97 "mapValueAddress": address,
98 "mapValueLabel": label,
99 });
100 }
101
102 Map<String, dynamic> toJson() {
103 return {
104 selfIdColumn: id,
105 "walletInfoId": walletInfoId,
106 "mapKey": mapKey,
107 "mapValueAccountIndex": accountIndex,
108 "mapValueAddress": address,
109 "mapValueLabel": label,
110 };
111 }
112
113 factory WalletInfoAddressInfo.fromJson(Map<String, dynamic> json) {
114 return WalletInfoAddressInfo(
115 id: json[selfIdColumn] as int,
116 walletInfoId: json['walletInfoId'] as int,
117 mapKey: json['mapKey'] as int,
118 accountIndex: json['mapValueAccountIndex'] as int,
119 address: json['mapValueAddress'] as String,
120 label: json['mapValueLabel'] as String,
121 );
122 }
123 }
124
125 class WalletInfoAddressMap {
126 WalletInfoAddressMap({
127 required this.id,
128 required this.walletInfoId,
129 required this.addressKey,
130 required this.addressValue,
131 });
132
133 int id;
134 int walletInfoId;
135 String addressKey;
136 String addressValue;
137
138 static String get tableName => 'walletInfoAddressMap';
139
140 static String get selfIdColumn => "${tableName}Id";
141
142 static Future<List<WalletInfoAddressMap>> selectList(int walletInfoId) async {
143 final query = await db!.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
144 return List.generate(query.length, (index) => WalletInfoAddressMap.fromJson(query[index]));
145 }
146
147 static Future<int> deleteByWalletInfoId(int walletInfoId) async {
148 return await db!.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]);
149 }
150
151 static Future<int> insert(int walletInfoId, String addressKey, String addressValue) async {
152 return await db!.insert(tableName, {
153 "walletInfoId": walletInfoId,
154 "addressKey": addressKey,
155 "addressValue": addressValue,
156 });
157 }
158
159 Map<String, dynamic> toJson() {
160 return {
161 selfIdColumn: id,
162 "walletInfoId": walletInfoId,
163 "addressKey": addressKey,
164 "addressValue": addressValue,
165 };
166 }
167
168 factory WalletInfoAddressMap.fromJson(Map<String, dynamic> json) {
169 return WalletInfoAddressMap(
170 id: json[selfIdColumn] as int,
171 walletInfoId: json['walletInfoId'] as int,
172 addressKey: json['addressKey'] as String,
173 addressValue: json['addressValue'] as String,
174 );
175 }
176 }
177
178 class WalletInfoAddress {
179 WalletInfoAddress({
180 this.id = 0,
181 required this.walletInfoId,
182 required this.type,
183 required this.address,
184 });
185
186 int id;
187 int walletInfoId;
188 WalletInfoAddressType type;
189 String address;
190
191 static String get tableName => 'walletInfoAddress';
192
193 static String get selfIdColumn => "${tableName}Id";
194
195 static Future<List<WalletInfoAddress>> selectList(
196 int walletInfoId, WalletInfoAddressType type) async {
197 final query = await db!.query(tableName,
198 where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]);
199 return List.generate(query.length, (index) => WalletInfoAddress.fromJson(query[index]));
200 }
201
202 static Future<int> deleteByAddress(
203 int walletInfoId, WalletInfoAddressType type, String address) async {
204 return await db!.delete(tableName,
205 where: 'walletInfoId = ? AND type = ? AND address = ?',
206 whereArgs: [walletInfoId, type.index, address]);
207 }
208
209 static Future<int> deleteByType(int walletInfoId, WalletInfoAddressType type) async {
210 return await db!.delete(tableName,
211 where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]);
212 }
213
214 static Future<int> insert(int walletInfoId, WalletInfoAddressType type, String address) async {
215 final select = await db!.query(tableName,
216 where: 'walletInfoId = ? AND type = ? AND address = ?',
217 whereArgs: [walletInfoId, type.index, address]);
218 if (select.isNotEmpty) {
219 return select[0][selfIdColumn] as int;
220 }
221 return await db!.insert(tableName, {
222 "walletInfoId": walletInfoId,
223 "type": type.index,
224 "address": address,
225 });
226 }
227
228 Map<String, dynamic> toJson() {
229 return {
230 selfIdColumn: id,
231 "walletInfoId": walletInfoId,
232 "type": type.index,
233 "address": address,
234 };
235 }
236
237 factory WalletInfoAddress.fromJson(Map<String, dynamic> json) {
238 return WalletInfoAddress(
239 id: json[selfIdColumn] as int,
240 walletInfoId: json['walletInfoId'] as int,
241 type: WalletInfoAddressType.values[json['type'] as int],
242 address: json['address'] as String,
243 );
244 }
245 }
246
247 class DerivationInfo {
248 DerivationInfo({
249 this.id = 0,
250 this.derivationType,
251 this.derivationPath,
252 this.balance = "",
253 this.address = "",
254 this.transactionsCount = 0,
255 this.scriptType,
256 this.description,
257 });
258
259 int id;
260
261 static String get tableName => 'walletInfoDerivationInfo';
262
263 static String get selfIdColumn => "${tableName}Id";
264
265 String address;
266 String balance;
267 int transactionsCount;
268 DerivationType? derivationType;
269 String? derivationPath;
270 String? scriptType;
271 String? description;
272
273 static Future<List<DerivationInfo>> selectList(String where, List<dynamic> whereArgs) async {
274 final query = await db!.query(
275 tableName,
276 columns: [
277 selfIdColumn,
278 'address',
279 'balance',
280 'transactionsCount',
281 'derivationType',
282 'derivationPath',
283 'scriptType',
284 'description',
285 ],
286 where: where.isNotEmpty ? where : "1 = 1",
287 whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
288 );
289 return List.generate(query.length, (index) => DerivationInfo.fromJson(query[index]));
290 }
291
292 Map<String, dynamic> toJson() {
293 return {
294 selfIdColumn: id,
295 "address": address,
296 "balance": balance,
297 "transactionsCount": transactionsCount,
298 "derivationType": derivationType?.index,
299 "derivationPath": derivationPath,
300 "scriptType": scriptType,
301 "description": description,
302 };
303 }
304
305 factory DerivationInfo.fromJson(Map<String, dynamic> json) {
306 return DerivationInfo(
307 id: json[selfIdColumn] as int,
308 derivationType: DerivationType.values[json['derivationType'] as int? ?? 0],
309 derivationPath: json['derivationPath'] as String?,
310 balance: json['balance'] as String? ?? "",
311 address: json['address'] as String? ?? "",
312 transactionsCount: json['transactionsCount'] as int? ?? 0,
313 scriptType: json['scriptType'] as String?,
314 description: json['description'] as String?,
315 );
316 }
317
318 Future<int> save() async {
319 final json = toJson();
320 if (json[selfIdColumn] == 0) {
321 json[selfIdColumn] = null;
322 }
323 id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
324 return id;
325 }
326 }
327
328 class WalletInfo {
329 WalletInfo(
330 this.internalId,
331 this.id,
332 this.name,
333 this.type,
334 this.isRecovery,
335 this.restoreHeight,
336 this.timestamp,
337 this.dirPath,
338 this.path,
339 this.address,
340 this.yatEid,
341 this.yatLastUsedAddressRaw,
342 this.showIntroCakePayCard,
343 this.derivationInfoId,
344 this.hardwareWalletType,
345 this.parentAddress,
346 this.hashedWalletIdentifier,
347 this.isNonSeedWallet,
348 this.sortOrder,
349 this.addressPageType,
350 this.receiveInfoboxDismissed,
351 this.showCombinedBalance,
352 this.favoriteTokenAddress)
353 : _yatLastUsedAddressController = StreamController<String>.broadcast();
354
355 factory WalletInfo.external(
356 {required String id,
357 required String name,
358 required WalletType type,
359 required bool isRecovery,
360 required int restoreHeight,
361 required DateTime date,
362 required String dirPath,
363 required String path,
364 required String address,
365 bool? showIntroCakePayCard,
366 String yatEid = '',
367 String yatLastUsedAddressRaw = '',
368 int? derivationInfoId,
369 HardwareWalletType? hardwareWalletType,
370 String? parentAddress,
371 String? hashedWalletIdentifier,
372 bool? isNonSeedWallet,
373 int? sortOrder,
374 bool? receiveInfoboxDismissed,
375 bool? showCombinedBalance,
376 String? favoriteTokenAddress}) {
377 return WalletInfo(
378 0,
379 id,
380 name,
381 type,
382 isRecovery,
383 restoreHeight,
384 date.millisecondsSinceEpoch,
385 dirPath,
386 path,
387 address,
388 yatEid,
389 yatLastUsedAddressRaw,
390 showIntroCakePayCard,
391 derivationInfoId ?? -1,
392 hardwareWalletType,
393 parentAddress,
394 hashedWalletIdentifier,
395 isNonSeedWallet ?? false,
396 sortOrder ?? 0,
397 null,
398 receiveInfoboxDismissed ?? false,
399 showCombinedBalance ?? true,
400 favoriteTokenAddress);
401 }
402
403 static String get tableName => 'walletInfo';
404
405 static String get selfIdColumn => "${tableName}Id";
406
407 int internalId;
408
409 String id;
410 String name;
411 WalletType type;
412 bool isRecovery;
413 int restoreHeight;
414 int timestamp;
415 String dirPath;
416 String path;
417 String address;
418 bool receiveInfoboxDismissed;
419 bool showCombinedBalance;
420 String? favoriteTokenAddress;
421
422 Future<Map<String, String>> getAddresses() async {
423 final list = await WalletInfoAddressMap.selectList(internalId);
424 return Map.fromEntries(list.map((e) => MapEntry(e.addressKey, e.addressValue)));
425 }
426
427 Future<void> setAddresses(Map<String, String> addresses) async {
428 await WalletInfoAddressMap.deleteByWalletInfoId(internalId);
429 final entries = addresses.entries.toList();
430 for (final entry in entries) {
431 await WalletInfoAddressMap.insert(internalId, entry.key, entry.value);
432 }
433 }
434
435 String? yatEid;
436 String? yatLastUsedAddressRaw;
437 bool? showIntroCakePayCard;
438
439 Future<Map<int, List<WalletInfoAddressInfo>>> getAddressInfos() async {
440 final list = await WalletInfoAddressInfo.selectList(internalId);
441 final ret = <int, List<WalletInfoAddressInfo>>{};
442 for (final e in list) {
443 ret[e.mapKey] ??= [];
444 ret[e.mapKey]!.add(e);
445 }
446 return ret;
447 }
448
449 Future<void> setAddressInfos(Map<int, List<WalletInfoAddressInfo>> addressInfos) async {
450 await WalletInfoAddressInfo.deleteByWalletInfoId(internalId);
451 final entries = addressInfos.entries.toList();
452 for (final addressInfo in entries) {
453 final infoList = addressInfo.value.toList();
454 for (final info in infoList) {
455 await WalletInfoAddressInfo.insert(
456 walletInfoId: internalId,
457 mapKey: addressInfo.key,
458 accountIndex: info.accountIndex,
459 address: info.address,
460 label: info.label,
461 );
462 }
463 }
464 }
465
466 Future<Set<String>> getUsedAddresses() async {
467 final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.used);
468 return list.map((e) => e.address).toSet();
469 }
470
471 Future<void> setUsedAddresses(List<String> addresses) async {
472 await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.used);
473 for (final address in addresses) {
474 await WalletInfoAddress.insert(internalId, WalletInfoAddressType.used, address);
475 }
476 }
477
478 Future<Set<String>> getHiddenAddresses() async {
479 final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.hidden);
480 return list.map((e) => e.address).toSet();
481 }
482
483 Future<void> setHiddenAddresses(List<String> addresses) async {
484 await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.hidden);
485 for (final address in addresses) {
486 await WalletInfoAddress.insert(internalId, WalletInfoAddressType.hidden, address);
487 }
488 }
489
490 Future<Set<String>> getManualAddresses() async {
491 final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.manual);
492 return list.map((e) => e.address).toSet();
493 }
494
495 Future<void> setManualAddresses(List<String> addresses) async {
496 await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.manual);
497 for (final address in addresses) {
498 await WalletInfoAddress.insert(internalId, WalletInfoAddressType.manual, address);
499 }
500 }
501
502 Future<void> addAddress(String address, WalletInfoAddressType type) async {
503 await WalletInfoAddress.insert(internalId, type, address);
504 }
505
506 String? addressPageType;
507 String? network;
508 int derivationInfoId;
509 DerivationInfo? _derivationInfo;
510
511 Future<DerivationInfo> getDerivationInfo() async {
512 if (_derivationInfo != null) {
513 return _derivationInfo!;
514 }
515 final list =
516 await DerivationInfo.selectList('walletInfoDerivationInfoId = ?', [derivationInfoId]);
517 if (list.isEmpty) {
518 final di = DerivationInfo(
519 id: 0,
520 derivationType: DerivationType.unknown,
521 );
522 derivationInfoId = await di.save();
523 _derivationInfo = di;
524 return di;
525 }
526 _derivationInfo = list[0];
527 return _derivationInfo!;
528 }
529
530 HardwareWalletType? hardwareWalletType;
531 String? parentAddress;
532 String? hashedWalletIdentifier;
533 bool isNonSeedWallet;
534
535 int sortOrder;
536
537 String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
538
539 set yatLastUsedAddress(String address) {
540 yatLastUsedAddressRaw = address;
541 _yatLastUsedAddressController.add(address);
542 }
543
544 String get yatEmojiId => yatEid ?? '';
545
546 bool get isShowIntroCakePayCard {
547 if (showIntroCakePayCard == null) {
548 return type != WalletType.haven;
549 }
550 return showIntroCakePayCard!;
551 }
552
553 bool get isHardwareWallet => [
554 HardwareWalletType.bitbox,
555 HardwareWalletType.ledger,
556 HardwareWalletType.trezor
557 ].contains(hardwareWalletType);
558
559 DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp);
560
561 Stream<String> get yatLastUsedAddressStream => _yatLastUsedAddressController.stream;
562
563 StreamController<String> _yatLastUsedAddressController;
564
565 Map<String, dynamic> toJson() => {
566 selfIdColumn: internalId,
567 "id": id,
568 "name": name,
569 "type": type.index,
570 "isRecovery": isRecovery ? 1 : 0,
571 "restoreHeight": restoreHeight,
572 "timestamp": timestamp,
573 "dirPath": dirPath,
574 "path": path,
575 "address": address,
576 "yatEid": yatEid,
577 "yatLastUsedAddressRaw": yatLastUsedAddressRaw,
578 "showIntroCakePayCard":
579 showIntroCakePayCard == true ? 1 : 0, // SQL regression: null -> false
580 "walletInfoDerivationInfoId": derivationInfoId,
581 "hardwareWalletType": hardwareWalletType?.index,
582 "parentAddress": parentAddress,
583 "hashedWalletIdentifier": hashedWalletIdentifier,
584 "isNonSeedWallet": isNonSeedWallet ? 1 : 0,
585 "sortOrder": sortOrder,
586 "addressPageType": addressPageType,
587 "receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
588 "showCombinedBalance": showCombinedBalance ? 1 : 0,
589 "favoriteTokenAddress": favoriteTokenAddress,
590 "network": network,
591 };
592
593 factory WalletInfo.fromJson(Map<String, dynamic> json) {
594 final info = WalletInfo(
595 json[selfIdColumn] as int,
596 json['id'] as String,
597 json['name'] as String,
598 WalletType.values[json['type'] as int],
599 (json['isRecovery'] as int) == 1,
600 json['restoreHeight'] as int,
601 json['timestamp'] as int,
602 json['dirPath'] as String,
603 json['path'] as String,
604 json['address'] as String,
605 json['yatEid'] as String?,
606 json['yatLastUsedAddressRaw'] as String?,
607 (json['showIntroCakePayCard'] as int) == 1,
608 json['walletInfoDerivationInfoId'] as int,
609 json['hardwareWalletType'] == null
610 ? null
611 : HardwareWalletType.values[json['hardwareWalletType'] as int],
612 json['parentAddress'] as String?,
613 json['hashedWalletIdentifier'] as String?,
614 (json['isNonSeedWallet'] as int) == 1,
615 json['sortOrder'] as int? ?? 0,
616 json['addressPageType'] as String? ?? null,
617 json['receiveInfoboxDismissed'] != 0,
618 json["showCombinedBalance"] != 0,
619 json["favoriteTokenAddress"] as String? ?? null);
620 info.network = json['network'] as String?;
621 return info;
622 }
623
624 Future<int> save() async {
625 final json = toJson();
626 if (json[selfIdColumn] == 0) {
627 json[selfIdColumn] = null;
628 }
629 if (_derivationInfo != null) {
630 derivationInfoId = await _derivationInfo!.save();
631 }
632 internalId = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
633 return internalId;
634 }
635
636 static Future<int> delete(WalletInfo walletInfo) async {
637 return await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
638 }
639
640 static Future<List<WalletInfo>> selectList(String where, List<dynamic> whereArgs,
641 {String orderBy = 'sortOrder'}) async {
642 final list = await db!.query(
643 tableName,
644 where: where.isNotEmpty ? where : "1 = 1",
645 whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
646 orderBy: orderBy,
647 );
648 return List.generate(list.length, (index) => WalletInfo.fromJson(list[index]));
649 }
650
651 static Future<List<WalletInfo>> getAll() async {
652 return selectList('', []);
653 }
654
655 static Future<WalletInfo?> get(String name, WalletType type) async {
656 final list = await selectList('name = ? AND type = ?', [name, type.index]);
657 if (list.isEmpty) {
658 return null;
659 }
660 return list[0];
661 }
662
663 Future<void> updateRestoreHeight(int height) async {
664 restoreHeight = height;
665 await save();
666 }
667 }