dev
dart 338 lines 11.7 KB
Raw
1 import 'dart:convert';
2 import 'dart:io';
3 import 'package:bip39/bip39.dart' as bip39;
4 import 'package:cw_core/encryption_file_utils.dart';
5 import 'package:cw_core/wallet_keys_file.dart';
6 import 'package:cw_decred/api/libdcrwallet.dart';
7 import 'package:cw_decred/mnemonic_validation.dart';
8 import 'package:cw_decred/wallet_creation_credentials.dart';
9 import 'package:cw_decred/wallet.dart';
10 import 'package:cw_core/wallet_base.dart';
11 import 'package:cw_core/wallet_service.dart';
12 import 'package:cw_core/pathForWallet.dart';
13 import 'package:cw_core/wallet_info.dart';
14 import 'package:cw_core/wallet_type.dart';
15 import 'package:path/path.dart';
16 import 'package:hive/hive.dart';
17 import 'package:cw_core/unspent_coins_info.dart';
18
19 class DecredWalletService extends WalletService<
20 DecredNewWalletCredentials,
21 DecredRestoreWalletFromSeedCredentials,
22 DecredRestoreWalletFromPubkeyCredentials,
23 DecredRestoreWalletFromHardwareCredentials> {
24 DecredWalletService(this.unspentCoinsInfoSource, this.isDirect);
25
26 final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
27 final bool isDirect;
28 final seedRestorePath = "m/44'/42'";
29 static final seedRestorePathTestnet = "m/44'/1'";
30 static final pubkeyRestorePath = "m/44'/42'/0'";
31 static final pubkeyRestorePathTestnet = "m/44'/1'/0'";
32 final mainnet = "mainnet";
33 final testnet = "testnet";
34 static Libwallet? libwallet;
35
36 Future<void> init() async {
37 if (libwallet != null) {
38 return;
39 }
40 libwallet = await Libwallet.spawn();
41 // Init logging with no directory to force printing to stdout and only
42 // print ERROR level logs.
43 libwallet!.initLibdcrwallet("", "err");
44 }
45
46 void closeLibwallet() {
47 if (libwallet == null) {
48 return;
49 }
50 libwallet!.close();
51 libwallet = null;
52 }
53
54 @override
55 WalletType getType() => WalletType.decred;
56
57 @override
58 Future<bool> isWalletExit(String name) async =>
59 File(await pathForWallet(name: name, type: getType())).existsSync();
60
61 @override
62 Future<DecredWallet> create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async {
63 final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
64 final mnemonic = (credentials.mnemonic?.isNotEmpty == true)
65 ? credentials.mnemonic!
66 : bip39.generateMnemonic(strength: strength);
67 validateDecredMnemonic(mnemonic, allowNativeSeed: false);
68 await this.init();
69 final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
70 final network = isTestnet == true ? testnet : mainnet;
71 final config = {
72 "name": credentials.walletInfo!.name,
73 "datadir": dirPath,
74 "pass": credentials.password!,
75 "mnemonic": mnemonic,
76 "seedpass": credentials.passphrase ?? "",
77 "birthday": DateTime.now().millisecondsSinceEpoch ~/ 1000,
78 "net": network,
79 "unsyncedaddrs": true,
80 };
81 await libwallet!.createWallet(jsonEncode(config));
82 final di = await credentials.walletInfo!.getDerivationInfo();
83 di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
84 di.derivationType = DerivationType.bip39;
85 await di.save();
86 credentials.walletInfo!.save();
87 credentials.walletInfo!.network = network;
88 // ios will move our wallet directory when updating. Since we must
89 // recalculate the new path every time we open the wallet, ensure this path
90 // is not used. An older wallet will have a directory here which is a
91 // condition for moving the wallet when opening, so this must be kept blank
92 // going forward.
93 credentials.walletInfo!.dirPath = "";
94 credentials.walletInfo!.path = "";
95 final wallet = _createWalletInstance(
96 credentials.walletInfo!,
97 di,
98 credentials.password!,
99 passphrase: credentials.passphrase,
100 );
101 await wallet.init();
102 await wallet.save();
103 return wallet;
104 }
105
106 void copyDirectorySync(Directory source, Directory destination) {
107 /// create destination folder if not exist
108 if (!destination.existsSync()) {
109 destination.createSync(recursive: true);
110 }
111
112 /// get all files from source (recursive: false is important here)
113 source.listSync(recursive: false).forEach((entity) {
114 final newPath = destination.path + Platform.pathSeparator + basename(entity.path);
115 if (entity is File) {
116 entity.rename(newPath);
117 } else if (entity is Directory) {
118 copyDirectorySync(entity, Directory(newPath));
119 }
120 });
121 }
122
123 Future<void> moveWallet(String fromPath, String toPath) async {
124 final oldWalletDir = new Directory(fromPath);
125 final newWalletDir = new Directory(toPath);
126 copyDirectorySync(oldWalletDir, newWalletDir);
127 // It would be ideal to delete the old directory here, but ios will error
128 // sometimes with "OS Error: No such file or directory, errno = 2" even
129 // after checking if it exists.
130 }
131
132 @override
133 Future<DecredWallet> openWallet(String name, String password) async {
134 final walletInfo = await WalletInfo.get(name, getType());
135 if (walletInfo == null) {
136 throw Exception('Wallet not found');
137 }
138 final di = await walletInfo.getDerivationInfo();
139 if (walletInfo.network == null || walletInfo.network == "") {
140 walletInfo.network = di.derivationPath == seedRestorePathTestnet ||
141 di.derivationPath == pubkeyRestorePathTestnet
142 ? testnet
143 : mainnet;
144 walletInfo.save();
145 }
146
147 await this.init();
148
149 // Cake wallet version 4.27.0 and earlier gave a wallet dir that did not
150 // match the name. Move those to the correct place.
151 final dirPath = await pathForWalletDir(name: name, type: getType());
152 if (walletInfo.path != "") {
153 // On ios the stored dir no longer exists. We can only trust the basename.
154 // dirPath may already be updated and lost the basename, so look at path.
155 final randomBasename = basename(walletInfo.path);
156 final oldDir = await pathForWalletDir(name: randomBasename, type: getType());
157 if (oldDir != dirPath) {
158 await this.moveWallet(oldDir, dirPath);
159 }
160 // Clear the path so this does not trigger again.
161 walletInfo.dirPath = "";
162 walletInfo.path = "";
163 await walletInfo.save();
164 }
165
166 final config = {
167 "name": name,
168 "datadir": dirPath,
169 "net": walletInfo.network,
170 "unsyncedaddrs": true,
171 };
172 await libwallet!.loadWallet(jsonEncode(config));
173 final passphrase = await _loadPassphrase(name, password);
174 final wallet = _createWalletInstance(walletInfo, di, password, passphrase: passphrase);
175 await wallet.init();
176 await _persistSeedDerivationType(wallet);
177 return wallet;
178 }
179
180 @override
181 Future<void> remove(String wallet) async {
182 File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
183 final walletInfo = await WalletInfo.get(wallet, getType());
184 if (walletInfo == null) {
185 throw Exception('Wallet not found');
186 }
187 await WalletInfo.delete(walletInfo);
188 }
189
190 @override
191 Future<void> rename(String currentName, String password, String newName) async {
192 final currentWalletInfo = await WalletInfo.get(currentName, getType());
193 if (currentWalletInfo == null) {
194 throw Exception('Wallet not found');
195 }
196 final di = await currentWalletInfo.getDerivationInfo();
197 final network =
198 di.derivationPath == seedRestorePathTestnet || di.derivationPath == pubkeyRestorePathTestnet
199 ? testnet
200 : mainnet;
201 currentWalletInfo.network = network;
202 currentWalletInfo.save();
203 if (libwallet == null) {
204 libwallet = await Libwallet.spawn();
205 libwallet!.initLibdcrwallet("", "err");
206 }
207 final currentWallet = _createWalletInstance(currentWalletInfo, di, password);
208
209 await currentWallet.renameWalletFiles(newName);
210
211 final newWalletInfo = currentWalletInfo;
212 newWalletInfo.id = WalletBase.idFor(newName, getType());
213 newWalletInfo.name = newName;
214 newWalletInfo.dirPath = "";
215 newWalletInfo.path = "";
216
217 await newWalletInfo.save();
218 }
219
220 @override
221 Future<DecredWallet> restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials,
222 {bool? isTestnet}) async {
223 validateDecredMnemonic(credentials.mnemonic);
224 await this.init();
225 final network = isTestnet == true ? testnet : mainnet;
226 final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
227 final config = {
228 "name": credentials.walletInfo!.name,
229 "datadir": dirPath,
230 "pass": credentials.password!,
231 "mnemonic": credentials.mnemonic,
232 "seedpass": credentials.passphrase ?? "",
233 "net": network,
234 "unsyncedaddrs": true,
235 };
236 await libwallet!.createWallet(jsonEncode(config));
237 final di = await credentials.walletInfo!.getDerivationInfo();
238 di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
239 di.derivationType =
240 bip39.validateMnemonic(credentials.mnemonic) ? DerivationType.bip39 : DerivationType.def;
241 await di.save();
242 credentials.walletInfo!.network = network;
243 credentials.walletInfo!.dirPath = "";
244 credentials.walletInfo!.path = "";
245 final wallet = _createWalletInstance(
246 credentials.walletInfo!,
247 di,
248 credentials.password!,
249 passphrase: credentials.passphrase,
250 );
251 await wallet.init();
252 await wallet.save();
253 return wallet;
254 }
255
256 EncryptionFileUtils get _encryptionFileUtils => encryptionFileUtilsFor(isDirect);
257
258 DecredWallet _createWalletInstance(
259 WalletInfo walletInfo,
260 DerivationInfo di,
261 String password, {
262 String? passphrase,
263 }) {
264 return DecredWallet(
265 walletInfo,
266 di,
267 password,
268 unspentCoinsInfoSource,
269 libwallet!,
270 closeLibwallet,
271 passphrase: passphrase,
272 encryptionFileUtils: _encryptionFileUtils,
273 );
274 }
275
276 Future<String?> _loadPassphrase(String name, String password) async {
277 if (!await WalletKeysFile.hasKeysFile(name, getType())) {
278 return null;
279 }
280 try {
281 final keys = await WalletKeysFile.readKeysFile(
282 name,
283 getType(),
284 password,
285 _encryptionFileUtils,
286 );
287 return keys.passphrase;
288 } catch (_) {
289 return null;
290 }
291 }
292
293 Future<void> _persistSeedDerivationType(DecredWallet wallet) async {
294 final seed = wallet.seed;
295 if (seed == null || seed.isEmpty) {
296 return;
297 }
298 final expected =
299 bip39.validateMnemonic(seed) ? DerivationType.bip39 : DerivationType.def;
300 if (wallet.derivationInfo.derivationType == expected) {
301 return;
302 }
303 wallet.derivationInfo.derivationType = expected;
304 await wallet.derivationInfo.save();
305 }
306
307 // restoreFromKeys only supports restoring a watch only wallet from an account
308 // pubkey.
309 @override
310 Future<DecredWallet> restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials,
311 {bool? isTestnet}) async {
312 await this.init();
313 final network = isTestnet == true ? testnet : mainnet;
314 final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
315 final config = {
316 "name": credentials.walletInfo!.name,
317 "datadir": dirPath,
318 "pubkey": credentials.pubkey,
319 "net": network,
320 "unsyncedaddrs": true,
321 };
322 await libwallet!.createWatchOnlyWallet(jsonEncode(config));
323 final di = await credentials.walletInfo!.getDerivationInfo();
324 di.derivationPath = isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath;
325 await di.save();
326 credentials.walletInfo!.network = network;
327 credentials.walletInfo!.dirPath = "";
328 credentials.walletInfo!.path = "";
329 final wallet = _createWalletInstance(credentials.walletInfo!, di, credentials.password!);
330 await wallet.init();
331 return wallet;
332 }
333
334 @override
335 Future<DecredWallet> restoreFromHardwareWallet(
336 DecredRestoreWalletFromHardwareCredentials credentials) async =>
337 throw UnimplementedError();
338 }