-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
wallet-manager.ts
262 lines (235 loc) · 7.29 KB
/
wallet-manager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
import type { AbstractAddress } from '@fuel-ts/interfaces';
import type { Keystore } from '@fuel-ts/keystore';
import { encrypt, decrypt } from '@fuel-ts/keystore';
import type { Wallet } from '@fuel-ts/wallet';
import { EventEmitter } from 'events';
import MemoryStorage from './storages/memory-storage';
import type {
StorageAbstract,
Account,
VaultConfig,
VaultsState,
WalletManagerOptions,
WalletManagerState,
} from './types';
import { MnemonicVault } from './vaults/mnemonic-vault';
import { PrivateKeyVault } from './vaults/privatekey-vault';
const ERROR_MESSAGES = {
invalid_vault_type: 'Invalid VaultType',
address_not_found: 'Address not found',
vault_not_found: 'Vault not found',
wallet_not_unlocked: 'Wallet is locked',
passphrase_not_match: "Passphrase didn't match",
};
/**
* Generic assert function to avoid undesirable errors
*/
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
/**
* WalletManager is a upper package to manage multiple vaults like mnemonic and privateKeys.
*
* - VaultTypes can be add to `WalletManager.Vaults` enabling to add custom Vault types.
* - Storage can be instantiate when initializing enabling custom storage types.
*/
export class WalletManager extends EventEmitter {
/**
* Vaults
*
* Vaults are responsible to store secret keys and return an `Wallet` instance,
* to interact with the network.
*
* Each vault has access to its own state
*
*/
static Vaults = [MnemonicVault, PrivateKeyVault];
/**
* Storage
*
* Persistent encrypted data. `The default storage works only on memory`.
*/
readonly storage: StorageAbstract = new MemoryStorage();
/* Key name passed to the storage */
readonly STORAGE_KEY: string = 'WalletManager';
// `This variables are only accessible from inside the class`
#vaults: VaultsState = [];
#passphrase = '';
#isLocked: boolean = true;
constructor(options?: WalletManagerOptions) {
super();
this.storage = options?.storage || this.storage;
}
get isLocked(): boolean {
return this.#isLocked;
}
/**
* List all vaults on the Wallet Manager, this function nto return secret's
*/
getVaults(): Array<{ title?: string; type: string }> {
return this.#vaults.map((v) => ({
title: v.title,
type: v.type,
}));
}
/**
* List all accounts on the Wallet Manager not vault information is revealed
*/
getAccounts(): Account[] {
// Return all accounts from vaults
return this.#vaults.reduce<Array<Account>>(
(result, vaultState) => result.concat(vaultState.vault.getAccounts()),
[]
);
}
/**
* Create a Wallet instance for the specific account
*/
getWallet(address: AbstractAddress): Wallet {
const vaultState = this.#vaults.find((vs) =>
vs.vault.getAccounts().find((a) => a.address.equals(address))
);
assert(vaultState, ERROR_MESSAGES.address_not_found);
return vaultState.vault.getWallet(address);
}
/**
* Export specific account privateKey
*/
exportPrivateKey(address: AbstractAddress) {
assert(!this.#isLocked, ERROR_MESSAGES.wallet_not_unlocked);
const vaultState = this.#vaults.find((vs) =>
vs.vault.getAccounts().find((a) => a.address.equals(address))
);
assert(vaultState, ERROR_MESSAGES.address_not_found);
return vaultState.vault.exportAccount(address);
}
/**
* Add account to a selected vault or on the first vault as default.
* If not vaults are adds it will return error
*/
async addAccount(options?: { vaultIndex: number }) {
// Make sure before add new vault state is fully loaded
await this.loadState();
// Get vault instance
const vaultState = this.#vaults[options?.vaultIndex || 0];
await assert(vaultState, ERROR_MESSAGES.vault_not_found);
// Add account on vault
vaultState.vault.addAccount();
// Save the accounts state
await this.saveState();
}
/**
* Remove vault by index, by remove the vault you also remove all accounts
* created by the vault.
*/
async removeVault(index: number) {
this.#vaults.splice(index, 1);
await this.saveState();
}
/**
* Add Vault, the `vaultConfig.type` will look for the Vaults supported if
* didn't found it will throw.
*/
async addVault(vaultConfig: VaultConfig) {
// Make sure before add new vault state is fully loaded
await this.loadState();
// Check if vault is supported
const Vault = this.getVaultClass(vaultConfig.type);
// create Vault instance
const vault = new Vault(vaultConfig);
// Push vaults to state
this.#vaults = this.#vaults.concat({
title: vaultConfig.title,
type: vaultConfig.type,
vault,
});
// Persist data on storage
await this.saveState();
}
/**
* Lock wallet. It removes passphrase from class instance, encrypt and hide all address and
* secrets.
*/
async lock() {
this.#isLocked = true;
// Clean state vaults from state
this.#vaults = [];
// Clean password from state
this.#passphrase = '';
// Emit event that wallet is locked
this.emit('lock');
}
/**
* Unlock wallet. It sets passphrase on WalletManger instance load all address from configured vaults.
* Vaults with secrets are not unlocked or instantiated on this moment.
*/
async unlock(passphrase: string) {
// Set password on state
this.#passphrase = passphrase;
// Set locked state to true
this.#isLocked = false;
// Load state
await this.loadState();
// Emit event that wallet is unlocked
this.emit('unlock');
}
/**
* Retrieve and decrypt WalletManager state from storage
*/
async loadState() {
await assert(!this.#isLocked, ERROR_MESSAGES.wallet_not_unlocked);
const data = await this.storage.getItem<string>(this.STORAGE_KEY);
if (data) {
const state = await decrypt<WalletManagerState>(this.#passphrase, <Keystore>JSON.parse(data));
this.#vaults = this.#deserializeVaults(state.vaults);
}
}
/**
* Store encrypted WalletManager state on storage
*/
private async saveState() {
await assert(!this.#isLocked, ERROR_MESSAGES.wallet_not_unlocked);
const encryptedData = await encrypt(this.#passphrase, {
vaults: this.#serializeVaults(this.#vaults),
});
this.storage.setItem(this.STORAGE_KEY, JSON.stringify(encryptedData));
this.emit('update');
}
/**
* Serialize all vaults to store
*
* `This is only accessible from inside the class`
*/
#serializeVaults(vaults: VaultsState) {
return vaults.map(({ title, type, vault }) => ({
title,
type,
data: vault.serialize(),
}));
}
/**
* Deserialize all vaults to state
*
* `This is only accessible from inside the class`
*/
#deserializeVaults(vaults: VaultsState) {
return vaults.map(({ title, type, data: vaultConfig }) => {
const VaultClass = this.getVaultClass(type);
return {
title,
type,
vault: new VaultClass(<VaultConfig>vaultConfig),
};
});
}
/**
* Return a instantiable Class reference from `WalletManager.Vaults` supported list.
*/
private getVaultClass(type: string) {
const VaultClass = WalletManager.Vaults.find((v) => v.type === type);
assert(VaultClass, ERROR_MESSAGES.invalid_vault_type);
return VaultClass;
}
}