@exodus/headless 2.0.0-alpha.1 → 2.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,70 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [2.0.0-alpha.10](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.9...@exodus/headless@2.0.0-alpha.10) (2023-04-30)
7
+
8
+ ### Features
9
+
10
+ - add pricingClient to headless ([#1439](https://github.com/ExodusMovement/exodus-hydra/issues/1439)) ([2a82a4c](https://github.com/ExodusMovement/exodus-hydra/commit/2a82a4c663b542178580908aaaff840524d55369))
11
+ - attach wallet account atoms ([#1437](https://github.com/ExodusMovement/exodus-hydra/issues/1437)) ([2648699](https://github.com/ExodusMovement/exodus-hydra/commit/264869986c175091e6bc7c48e852946bd60911d6))
12
+
13
+ ## [2.0.0-alpha.9](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.8...@exodus/headless@2.0.0-alpha.9) (2023-04-26)
14
+
15
+ ### Features
16
+
17
+ - add available-assets to headless ([#1361](https://github.com/ExodusMovement/exodus-hydra/issues/1361)) ([900d142](https://github.com/ExodusMovement/exodus-hydra/commit/900d142018ff112f36ef0f7ef53cf5020020e3ca))
18
+
19
+ ## [2.0.0-alpha.8](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.7...@exodus/headless@2.0.0-alpha.8) (2023-04-25)
20
+
21
+ ### Features
22
+
23
+ - add enabled-assets to headless ([#1235](https://github.com/ExodusMovement/exodus-hydra/issues/1235)) ([a808b75](https://github.com/ExodusMovement/exodus-hydra/commit/a808b750bd911c33241ae710fbf8d8ba7e2073a6))
24
+ - add lifecycle docs ([#1313](https://github.com/ExodusMovement/exodus-hydra/issues/1313)) ([df18faa](https://github.com/ExodusMovement/exodus-hydra/commit/df18faa9c341404c2feaadf51ec46236c54e2f15))
25
+ - add remoteConfig module ([#1370](https://github.com/ExodusMovement/exodus-hydra/issues/1370)) ([ebc888d](https://github.com/ExodusMovement/exodus-hydra/commit/ebc888d6440c509a19dd6c9c600033fd41128ad7))
26
+ - add support for plugins ([#1351](https://github.com/ExodusMovement/exodus-hydra/issues/1351)) ([fac0e2a](https://github.com/ExodusMovement/exodus-hydra/commit/fac0e2a727b0e71ad5f0619b8457c7e80612f707))
27
+
28
+ ## [2.0.0-alpha.7](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.6...@exodus/headless@2.0.0-alpha.7) (2023-04-24)
29
+
30
+ ### ⚠ BREAKING CHANGES
31
+
32
+ - pass compatibility modes config to wallet (#1223)
33
+
34
+ ### Features
35
+
36
+ - add blockchain metadata to headless ([#1293](https://github.com/ExodusMovement/exodus-hydra/issues/1293)) ([04390a7](https://github.com/ExodusMovement/exodus-hydra/commit/04390a71cb01744cd427f470735fa0748b9157ca))
37
+ - add namespaceStorage preprocessor ([#1322](https://github.com/ExodusMovement/exodus-hydra/issues/1322)) ([95f403e](https://github.com/ExodusMovement/exodus-hydra/commit/95f403e59c87f90901247619c727496ebac0a399))
38
+ - pass compatibility modes config to wallet ([#1223](https://github.com/ExodusMovement/exodus-hydra/issues/1223)) ([b49b640](https://github.com/ExodusMovement/exodus-hydra/commit/b49b64097af7969e210e5ecf5395ee7f40a13eac))
39
+
40
+ ## [2.0.0-alpha.6](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.5...@exodus/headless@2.0.0-alpha.6) (2023-04-20)
41
+
42
+ ### Features
43
+
44
+ - add walletAccounts in headless ([#1184](https://github.com/ExodusMovement/exodus-hydra/issues/1184)) ([8d9c93d](https://github.com/ExodusMovement/exodus-hydra/commit/8d9c93d86173476211d596e883a72b51b645e866))
45
+
46
+ ## [2.0.0-alpha.5](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.4...@exodus/headless@2.0.0-alpha.5) (2023-04-20)
47
+
48
+ ### Features
49
+
50
+ - force restart on import ([#1297](https://github.com/ExodusMovement/exodus-hydra/issues/1297)) ([3900196](https://github.com/ExodusMovement/exodus-hydra/commit/39001966ce7d962ddde9df8aa3b93619053569f8))
51
+ - **headless:** clear storage when seed not present ([#1300](https://github.com/ExodusMovement/exodus-hydra/issues/1300)) ([c3c76f5](https://github.com/ExodusMovement/exodus-hydra/commit/c3c76f5953d91e5ef308b129f1bb2fec0dc41500))
52
+ - unlock encrypted storage on wallet unlock ([#1304](https://github.com/ExodusMovement/exodus-hydra/issues/1304)) ([09ea5f2](https://github.com/ExodusMovement/exodus-hydra/commit/09ea5f20ff9547557f59d4c416cee3291f348fdf))
53
+
54
+ ## [2.0.0-alpha.4](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.3...@exodus/headless@2.0.0-alpha.4) (2023-04-13)
55
+
56
+ **Note:** Version bump only for package @exodus/headless
57
+
58
+ ## [2.0.0-alpha.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.2...@exodus/headless@2.0.0-alpha.3) (2023-04-11)
59
+
60
+ ### Features
61
+
62
+ - export subscribe/unsubscribe ([#1161](https://github.com/ExodusMovement/exodus-hydra/issues/1161)) ([7da465f](https://github.com/ExodusMovement/exodus-hydra/commit/7da465fc1734fd0818a55ff740704adb8c6ae222))
63
+
64
+ ## [2.0.0-alpha.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0-alpha.1...@exodus/headless@2.0.0-alpha.2) (2023-04-10)
65
+
66
+ ### Features
67
+
68
+ - restart reason ([#1163](https://github.com/ExodusMovement/exodus-hydra/issues/1163)) ([d2b10c7](https://github.com/ExodusMovement/exodus-hydra/commit/d2b10c71229078f52883badb5634b377c61b8b1e))
69
+
6
70
  ## [2.0.0-alpha.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/headless@2.0.0...@exodus/headless@2.0.0-alpha.1) (2023-04-05)
7
71
 
8
72
  ### Features
package/README.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  The headless Exodus wallet SDK
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Quick Start](#quick-start)
8
+ - [Lifecycle](#lifecycle)
9
+ - [Port](#port)
10
+ - [Adapters](#adapters)
11
+ - [Config](#config)
12
+ - [Headless API](#headless-api)
13
+
5
14
  ## Quick Start
6
15
 
7
16
  ```js
@@ -34,15 +43,74 @@ const exodus = container.resolve()
34
43
  await exodus.wallet.create({ passphrase: 'my-super-secure-passphrase' })
35
44
  ```
36
45
 
46
+ ## Lifecycle
47
+
48
+ The headless wallet instance transitions through different states during his life. When moving from one another, there are two things that are triggered:
49
+
50
+ - Fires hook call: Allows modules to subscribe and halt transition until listener resolves.
51
+ - Fires event call: Emitted after all hooks had been executed and resolved.
52
+
53
+ Below you can find a state diagram that visualize what hooks are triggered between state transitions:
54
+
55
+ ```mermaid
56
+ stateDiagram-v2
57
+ direction LR
58
+ [*] --> Starting
59
+
60
+ state Starting {
61
+ direction LR
62
+
63
+ state "Needs clear storage?*" as NeedsClear
64
+ state "Is importing?" as IsImporting
65
+
66
+ NeedsClear --> Clearing : CLEAR
67
+ Clearing --> IsImporting
68
+ IsImporting --> Importing : IMPORT
69
+ }
70
+
71
+ Starting --> Started : START
72
+
73
+ state Started {
74
+ direction LR
75
+
76
+ state "Has seed?" as HasSeed
77
+
78
+ HasSeed --> Empty : No
79
+ HasSeed --> Locked : Yes
80
+
81
+ Empty --> Locked : CREATE \n---or---\nIMPORT
82
+
83
+ Locked --> Migrating : MIGRATE
84
+ Migrating --> Unlocked : UNLOCK
85
+ Unlocked --> Locked : LOCK
86
+
87
+ Unlocked --> Restarting : DELETE
88
+
89
+
90
+ }
91
+
92
+ Restarting --> [*] : RESTART
93
+
94
+ Started --> Started : LOAD
95
+
96
+ classDef conditional fill:#ededed,stroke:#c2c2c2,color:#a6a6a6,stroke-width:2;
97
+
98
+ class NeedsClear conditional
99
+ class IsImporting conditional
100
+ class HasSeed conditional
101
+ ```
102
+
103
+ \* Wallet needs to clear storage if seed is not present or is restoring a new wallet
104
+
37
105
  ## Port
38
106
 
39
107
  Event bus between headless wallet and client
40
108
 
41
- | Method | Type | Description |
42
- | -------------------- | ------------------------------------------ | ------------------------------------------ |
43
- | subscribe | `({ type: string, payload: any }) => void` | Subscribe to receive events. |
44
- | unsubscribe(handler) | `({ type: string, payload: any }) => void` | Unsubscribe handler from receiving events. |
45
- | emit | `(type: string, payload: any) => void` | Emit an event. |
109
+ | Method | Type | Description |
110
+ | ----------- | ------------------------------------------ | ------------------------------------------ |
111
+ | subscribe | `({ type: string, payload: any }) => void` | Subscribe to receive events. |
112
+ | unsubscribe | `({ type: string, payload: any }) => void` | Unsubscribe handler from receiving events. |
113
+ | emit | `(type: string, payload: any) => void` | Emit an event. |
46
114
 
47
115
  ## Adapters
48
116
 
@@ -121,21 +189,21 @@ Currently not used.
121
189
 
122
190
  > Type: object
123
191
 
124
- | Method | Type | Description |
125
- | ------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
126
- | exists | `async () => boolean` | Checks if a wallet exists on the device. |
127
- | create | `async ({ passphrase?: string }) => void` | Creates new wallet |
128
- | import | `async ({ mnemonic: string, passphrase?: string }) => void` | Imports existing wallet |
129
- | delete | `async ({ passphrase?: string }) => void` | Deletes wallet. Passphrase is mandatory if wallet was created with one |
130
- | lock | `async () => void` | Locks wallet |
131
- | unlock | `async ({ passphrase?: string }) => void` | Unlocks wallet. Passphrase is mandatory if wallet was created with one |
132
- | isLocked | `async () => boolean` | Checks if wallet is locked |
133
- | getMnemonic | `async () => void` | Sets wallet as backed up |
134
- | changePassphrase | `async ({ currentPassphrase?: string. newPassphrase: string }) => void` | Change passphrase. Current passphrase is mandatory if wallet was created with one |
135
- | restoreFromCurrentPhrase | `async ({ passphrase?: string }) => void` | Restore current wallet. Passphrase is mandatory if wallet was created with one |
136
- | load | `async () => void` | Loads UI by rehydratating data through port (`BE Only`) |
137
- | unload | `async () => void` | Unloads UI by rehydratating data through port (`BE Only`) |
138
- | changeLockTimer | `async ({ ttl: number }) => void` | Change auto unlock ttl (`BE Only`) |
192
+ | Method | Type | Description |
193
+ | ------------------------ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
194
+ | exists | `async () => boolean` | Checks if a wallet exists on the device. |
195
+ | create | `async ({ passphrase?: string }) => void` | Creates new wallet |
196
+ | import | `async ({ mnemonic: string, passphrase?: string, forceRestart?: boolean }) => void` | Imports existing wallet |
197
+ | delete | `async ({ passphrase?: string }) => void` | Deletes wallet. Passphrase is mandatory if wallet was created with one |
198
+ | lock | `async () => void` | Locks wallet |
199
+ | unlock | `async ({ passphrase?: string }) => void` | Unlocks wallet. Passphrase is mandatory if wallet was created with one |
200
+ | isLocked | `async () => boolean` | Checks if wallet is locked |
201
+ | getMnemonic | `async () => void` | Sets wallet as backed up |
202
+ | changePassphrase | `async ({ currentPassphrase?: string. newPassphrase: string }) => void` | Change passphrase. Current passphrase is mandatory if wallet was created with one |
203
+ | restoreFromCurrentPhrase | `async ({ passphrase?: string }) => void` | Restore current wallet. Passphrase is mandatory if wallet was created with one |
204
+ | load | `async () => void` | Loads UI by rehydratating data through port (`BE Only`) |
205
+ | unload | `async () => void` | Unloads UI by rehydratating data through port (`BE Only`) |
206
+ | changeLockTimer | `async ({ ttl: number }) => void` | Change auto unlock ttl (`BE Only`) |
139
207
 
140
208
  ## isMnemonicValid
141
209
 
@@ -147,3 +215,52 @@ Currently not used.
147
215
  | wordlist | `string[]` | Words list to check against. |
148
216
 
149
217
  **Returns**: `boolean`
218
+
219
+ ## walletAccounts
220
+
221
+ > Type: object
222
+
223
+ | Method | Type | Description |
224
+ | ---------- | ------------------------------------------- | -------------------------------------------------------------- |
225
+ | create | `async (walletAccountFields) => void` | Creates a new WalletAccount with the provided fields. |
226
+ | update | `async (name, walletAccountFields) => void` | Updates an existing WalletAccount with the provided fields. |
227
+ | enable | `async (name) => void` | Enables an existing WalletAccount. |
228
+ | disable | `async (name) => void` | Disables an existing WalletAccount. |
229
+ | getEnabled | `async () => object` | Returns enabled walletAccounts as `{ [name]: WalletAccount }`. |
230
+
231
+ ## blockchainMetadata
232
+
233
+ > Type: object
234
+
235
+ | Method | Type | Description |
236
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
237
+ | getTxLog | `async ({ assetName, walletAccount }) => TxSet` | Get the TxSet for the provided `assetName`/`walletAccount` combination (AssetSource) |
238
+ | getLoadedTxLogs | `async () => object` | Get the all loaded `TxSet`s as a `{ [walletAccount]: { [assetName]: TxSet } } object` |
239
+ | updateTxs | `async ({ assetName: string, walletAccount: string, txs: object[] }) => void` | Add or update txs with updates provided in `txs` |
240
+ | overwriteTxs | `async ({ assetName: string, walletAccount: string, txs: object[], notifyReceivedTxs: ?boolean }) => void` | Overwrite specified txs. |
241
+ | clearTxs | `async ({ assetName: string, walletAccount: string }) => void` | Remove txs for AssetSource. |
242
+ | removeTxs | `async ({ assetName: string, walletAccount: string, txs: object[] }) => void` | Remove provided txs. |
243
+ | getAccountState | `async ({ assetName, walletAccount }) => AccountState` | Get the AccountState for the provided AssetSource |
244
+ | getLoadedAccountStates | `async () => object` | Get the all loaded `AccountState`s as a `{ [walletAccount]: { [assetName]: AccountState } } object` |
245
+ | updateAccountState | `async ({ assetName: string, walletAccount: string, newData: object }) => void` | Update accountState for AssetSource. |
246
+ | removeAccountState | `async ({ assetName: string, walletAccount: string }) => void` | Remove accountState for AssetSource. |
247
+ | batch | `() => Batch` | Create a batch of updates. See [blockchainMetadata](../blockchain-metadata) README for batching details. |
248
+
249
+ ## assets
250
+
251
+ > Type: object
252
+
253
+ | Method | Type | Description |
254
+ | ----------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
255
+ | enable | `async (assetNames: string[]) => void` | Enables assets. |
256
+ | disable | `async (assetNames: string[]) => void` | Disables assets. |
257
+ | addAndEnableToken | `async (assetId: string, baseAssetName: string) => string` | Adds and enables a custom token. Returns the created `asset`'s `.name` |
258
+
259
+ ## subscribe
260
+
261
+ > Type: function
262
+
263
+ | Argument | Type | Description |
264
+ | ----------- | ------------------------------------------ | ------------------------------------------ |
265
+ | subscribe | `({ type: string, payload: any }) => void` | Subscribe to receive events. |
266
+ | unsubscribe | `({ type: string, payload: any }) => void` | Unsubscribe handler from receiving events. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/headless",
3
- "version": "2.0.0-alpha.1",
3
+ "version": "2.0.0-alpha.10",
4
4
  "description": "The headless Exodus wallet SDK",
5
5
  "author": "Exodus Movement Inc",
6
6
  "main": "src/index.js",
@@ -27,26 +27,42 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@exodus/atoms": "^2.9.0",
30
+ "@exodus/available-assets": "^2.0.0",
31
+ "@exodus/basic-utils": "^2.0.0",
32
+ "@exodus/blockchain-metadata": "^8.0.1",
33
+ "@exodus/config": "7.0.0",
30
34
  "@exodus/dependency-injection": "^1.2.0",
31
35
  "@exodus/dependency-preprocessors": "^2.0.2",
36
+ "@exodus/enabled-assets": "^5.0.4",
37
+ "@exodus/exodus-pricing-client": "^1.1.0",
32
38
  "@exodus/fetch": "^1.2.1",
33
- "@exodus/key-identifier-provider": "^1.1.1",
34
- "@exodus/keychain": "^3.2.1",
39
+ "@exodus/key-identifier-provider": "^1.1.3",
40
+ "@exodus/keychain": "^4.0.0",
35
41
  "@exodus/module": "^1.0.0",
36
- "@exodus/wallet": "^5.0.0",
37
- "@exodus/wallet-compatibility-modes": "1.0.2",
42
+ "@exodus/wallet": "^6.0.1",
43
+ "@exodus/wallet-accounts": "^8.0.1",
44
+ "@exodus/wallet-compatibility-modes": "^2.0.0",
38
45
  "bip39": "2.6.0",
46
+ "events": "^3.3.0",
39
47
  "lodash": "https://registry.yarnpkg.com/@exodus/lodash/-/lodash-4.17.21-exodus.2.tgz",
40
48
  "minimalistic-assert": "^1.0.1"
41
49
  },
42
50
  "devDependencies": {
51
+ "@exodus/bitcoin-meta": "^1.0.0",
52
+ "@exodus/currency": "^2.2.0",
43
53
  "@exodus/ethereum-lib": "^2.22.2",
54
+ "@exodus/ethereum-meta": "^1.0.23",
55
+ "@exodus/models": "^8.11.1",
44
56
  "@exodus/solana-lib": "^1.3.11",
57
+ "@exodus/solana-meta": "^1.0.2",
58
+ "@exodus/storage-encrypted": "^1.1.2",
45
59
  "@exodus/storage-memory": "^1.1.0",
46
60
  "@exodus/wild-emitter": "^1.0.0",
47
61
  "buffer-json": "^2.0.0",
48
62
  "eslint": "^8.33.0",
49
- "jest": "^29.1.2"
63
+ "events": "^3.3.0",
64
+ "jest": "^29.1.2",
65
+ "p-defer": "^4.0.0"
50
66
  },
51
- "gitHead": "885fc898545049d57c52696e9456aab6c2ecc1a2"
67
+ "gitHead": "468f15a295d9b9cd5463c33bbae6d3bb93028689"
52
68
  }
package/src/api.js CHANGED
@@ -1,9 +1,19 @@
1
1
  import { validateMnemonic as isMnemonicValid } from 'bip39'
2
2
 
3
- const createApi = ({ ioc }) => {
3
+ const createApi = ({ ioc, port }) => {
4
4
  const { passphraseCache } = ioc.getByType('adapter')
5
5
 
6
- const { application, wallet } = ioc.getByType('module')
6
+ const {
7
+ application,
8
+ assetsModule,
9
+ blockchainMetadata,
10
+ enabledAssets,
11
+ remoteConfig,
12
+ wallet,
13
+ walletAccounts,
14
+ } = ioc.getByType('module')
15
+
16
+ const { enabledWalletAccountsAtom } = ioc.getByType('atom')
7
17
 
8
18
  return {
9
19
  wallet: {
@@ -27,7 +37,42 @@ const createApi = ({ ioc }) => {
27
37
  changeLockTimer: application.changeLockTimer,
28
38
  isLocked: () => wallet.isLocked(),
29
39
  },
40
+ walletAccounts: {
41
+ create: walletAccounts.create,
42
+ update: walletAccounts.update,
43
+ disable: walletAccounts.disable,
44
+ enable: walletAccounts.enable,
45
+ getEnabled: enabledWalletAccountsAtom.get,
46
+ },
47
+ blockchainMetadata: {
48
+ getTxLog: blockchainMetadata.getTxLog,
49
+ getLoadedTxLogs: blockchainMetadata.getLoadedTxLogs,
50
+ updateTxs: blockchainMetadata.updateTxs,
51
+ overwriteTxs: blockchainMetadata.overwriteTxs,
52
+ clearTxs: blockchainMetadata.clearTxs,
53
+ removeTxs: blockchainMetadata.removeTxs,
54
+ getAccountState: blockchainMetadata.getAccountState,
55
+ getLoadedAccountStates: blockchainMetadata.getLoadedAccountStates,
56
+ updateAccountState: blockchainMetadata.updateAccountState,
57
+ removeAccountState: blockchainMetadata.removeAccountState,
58
+ batch: blockchainMetadata.batch,
59
+ },
60
+ assets: {
61
+ enable: enabledAssets.enable,
62
+ disable: enabledAssets.disable,
63
+ addAndEnableToken: async (...args) => {
64
+ const asset = await assetsModule.addToken(...args)
65
+ await enabledAssets.enable([asset.name])
66
+ return asset.name
67
+ },
68
+ },
69
+ remoteConfig: {
70
+ get: remoteConfig.get,
71
+ getAll: remoteConfig.getAll,
72
+ },
30
73
  isMnemonicValid,
74
+ subscribe: port.subscribe.bind(port),
75
+ unsubscribe: port.unsubscribe.bind(port),
31
76
  }
32
77
  }
33
78
 
@@ -50,6 +50,7 @@ class Application extends ExodusModule {
50
50
  this.#storage = unsafeStorage.namespace('flags')
51
51
 
52
52
  this.#applicationStarted = new Promise((resolve) => (this.#resolveStart = resolve))
53
+ this.setMaxListeners(Number.POSITIVE_INFINITY)
53
54
  }
54
55
 
55
56
  start = async () => {
@@ -63,6 +64,11 @@ class Application extends ExodusModule {
63
64
  if (isDeleting || isImporting) {
64
65
  await this.#storage.batchDelete([DELETE_FLAG, IMPORT_FLAG])
65
66
  await this.#passphraseCache.clear()
67
+ }
68
+
69
+ const walletExists = await this.#wallet.exists()
70
+
71
+ if (isImporting || !walletExists) {
66
72
  await this.fire(HOOKS.clear)
67
73
  }
68
74
 
@@ -141,19 +147,21 @@ class Application extends ExodusModule {
141
147
 
142
148
  await this.#applicationStarted
143
149
 
144
- if (await this.#wallet.exists()) {
145
- await this.#wallet.import(opts)
146
- await this.#storage.set(IMPORT_FLAG, true)
150
+ const walletExists = await this.#wallet.exists()
147
151
 
148
- this.emit('restart')
152
+ const { forceRestart, ...wallet } = opts
149
153
 
150
- return
151
- }
154
+ await this.#wallet.import(wallet)
155
+
156
+ if (forceRestart || walletExists) {
157
+ await this.#storage.set(IMPORT_FLAG, true)
152
158
 
153
- await this.#wallet.import(opts)
154
- await this.fire(HOOKS.import)
159
+ this.emit('restart', { reason: 'import' })
160
+ } else {
161
+ await this.fire(HOOKS.import)
155
162
 
156
- this._logger.log('wallet imported')
163
+ this._logger.log('wallet imported')
164
+ }
157
165
  }
158
166
 
159
167
  getMnemonic = async (opts) => this.#wallet.getMnemonic(opts)
@@ -236,7 +244,7 @@ class Application extends ExodusModule {
236
244
 
237
245
  delete = async () => {
238
246
  await this.#storage.set(DELETE_FLAG, true)
239
- this.emit('restart')
247
+ this.emit('restart', { reason: 'delete' })
240
248
  }
241
249
 
242
250
  changeLockTimer = async ({ ttl }) => {
@@ -0,0 +1,40 @@
1
+ const emitAtomValue = async (opts) => {
2
+ const { port, atomId, atom } = opts
3
+ const value = 'value' in opts ? opts.value : await atom.get()
4
+ port.emit(atomId, value)
5
+ }
6
+
7
+ export const emitFromAtoms = ({ atoms, port }) =>
8
+ Object.entries(atoms).forEach(async ([atomId, atom]) => emitAtomValue({ port, atomId, atom }))
9
+
10
+ const attachAtom = ({ port, application, logger, atom, atomId, lifecycleEvents }) => {
11
+ let loaded = false
12
+
13
+ lifecycleEvents.forEach((event) =>
14
+ application.on(event, async () => {
15
+ loaded = true
16
+ await emitAtomValue({ port, atomId, atom })
17
+ })
18
+ )
19
+
20
+ application.on('clear', async () => {
21
+ try {
22
+ await atom?.set(undefined)
23
+ } catch (error) {
24
+ logger.debug('failed to clear atom', error)
25
+ // noop. atom might not support set
26
+ }
27
+ })
28
+
29
+ atom.observe((value) => {
30
+ if (loaded) emitAtomValue({ port, atomId, atom, value })
31
+ })
32
+ }
33
+
34
+ const attachAtoms = ({ port, application, logger, atoms, lifecycleEvents = ['start'] }) => {
35
+ for (const [atomId, atom] of Object.entries(atoms)) {
36
+ attachAtom({ port, application, logger, atom, atomId, lifecycleEvents })
37
+ }
38
+ }
39
+
40
+ export default attachAtoms
@@ -0,0 +1,6 @@
1
+ export const atomsToAttach = [
2
+ //
3
+ 'availableAssetNamesAtom',
4
+ 'enabledWalletAccountsAtom',
5
+ 'walletAccountsAtom',
6
+ ]
@@ -1,4 +1,16 @@
1
- import { createInMemoryAtom } from '@exodus/atoms'
1
+ import { createInMemoryAtom, createRemoteConfigAtomFactory } from '@exodus/atoms'
2
+ import {
3
+ walletAccountsAtomDefinition,
4
+ enabledWalletAccountsAtomDefinition,
5
+ } from '@exodus/wallet-accounts/atoms'
6
+
7
+ import {
8
+ enabledAndDisabledAssetsAtomDefinition,
9
+ enabledAssetsAtomDefinition,
10
+ } from '@exodus/enabled-assets/atoms'
11
+
12
+ import { availableAssetNamesAtomDefinition } from '@exodus/available-assets/atoms'
13
+
2
14
  import { withType } from './utils'
3
15
 
4
16
  const createAtomDependencies = () =>
@@ -10,6 +22,25 @@ const createAtomDependencies = () =>
10
22
  dependencies: [],
11
23
  },
12
24
  },
25
+ {
26
+ definition: walletAccountsAtomDefinition,
27
+ storage: { namespace: 'walletAccounts' },
28
+ },
29
+ { definition: enabledWalletAccountsAtomDefinition },
30
+ { definition: enabledAndDisabledAssetsAtomDefinition },
31
+ { definition: enabledAssetsAtomDefinition },
32
+ { definition: availableAssetNamesAtomDefinition },
33
+ {
34
+ definition: {
35
+ id: 'pricingServerUrlAtom',
36
+ factory: ({ config, remoteConfig }) =>
37
+ createRemoteConfigAtomFactory({ remoteConfig })({
38
+ path: config.pricingServerPath,
39
+ defaultValue: config.defaultPricingServerUrl,
40
+ }),
41
+ dependencies: ['config', 'remoteConfig'],
42
+ },
43
+ },
13
44
  ].map(withType('atom'))
14
45
 
15
46
  export default createAtomDependencies
@@ -8,6 +8,7 @@ import createAdapterDependencies from './adapters'
8
8
  import createAtomDependencies from './atoms'
9
9
  import createModuleDependencies from './modules'
10
10
  import { wrapConstant } from './utils'
11
+ import createPluginDependencies from './plugins'
11
12
 
12
13
  const adapterKeys = [
13
14
  // ...
@@ -16,6 +17,9 @@ const adapterKeys = [
16
17
  'legacyPrivToPub',
17
18
  'seedStorage',
18
19
  'unsafeStorage',
20
+ 'fusion',
21
+ 'fetch',
22
+ 'freeze',
19
23
  ]
20
24
 
21
25
  const createDependencies = ({ adapters, config }) => {
@@ -33,11 +37,14 @@ const createDependencies = ({ adapters, config }) => {
33
37
 
34
38
  const adaptersTree = createAdapterDependencies({ adapters, config })
35
39
 
40
+ const plugins = createPluginDependencies()
41
+
36
42
  return []
37
43
  .concat(adaptersTree)
38
44
  .concat(configs)
39
45
  .concat(modules)
40
46
  .concat(atoms)
47
+ .concat(plugins)
41
48
  .concat(wrapConstant({ id: 'logger', type: 'module', value: logger }))
42
49
  }
43
50
 
@@ -1,10 +1,18 @@
1
- import { createKeychain } from '@exodus/keychain'
1
+ import createRemoteConfig from '@exodus/config/remote'
2
+ import keychainDefinition from '@exodus/keychain/module'
2
3
  import walletDefinition from '@exodus/wallet/module'
4
+ import walletAccountsDefinition from '@exodus/wallet-accounts/module'
5
+ import blockchainMetadataDefinition from '@exodus/blockchain-metadata/module'
6
+ import enabledAssetsModuleDefinition from '@exodus/enabled-assets/module'
7
+ import availableAssetsModuleDefinition from '@exodus/available-assets/module'
3
8
  import createKeyIdentifierProvider from '@exodus/key-identifier-provider'
4
9
  import walletCompatibilityModesDefinition from '@exodus/wallet-compatibility-modes/module'
10
+ import EventEmitter from 'events/'
5
11
 
6
12
  import createApplication from '../application'
7
13
  import { withType } from './utils'
14
+ import unlockEncryptedStorageDefinition from '../unlock-encrypted-storage'
15
+ import createExodusPricingClient from '@exodus/exodus-pricing-client'
8
16
 
9
17
  const createModuleDependencies = () =>
10
18
  [
@@ -23,11 +31,7 @@ const createModuleDependencies = () =>
23
31
  },
24
32
  },
25
33
  {
26
- definition: {
27
- id: 'keychain',
28
- factory: createKeychain,
29
- dependencies: ['legacyPrivToPub'],
30
- },
34
+ definition: keychainDefinition,
31
35
  },
32
36
  {
33
37
  definition: walletDefinition,
@@ -35,6 +39,31 @@ const createModuleDependencies = () =>
35
39
  {
36
40
  definition: walletCompatibilityModesDefinition,
37
41
  },
42
+ { definition: unlockEncryptedStorageDefinition },
43
+ { definition: walletAccountsDefinition },
44
+ {
45
+ definition: blockchainMetadataDefinition,
46
+ storage: { namespace: ['blockchain', 'v1'] },
47
+ },
48
+ { definition: enabledAssetsModuleDefinition },
49
+ {
50
+ definition: {
51
+ id: 'remoteConfig',
52
+ factory: (deps) => {
53
+ const eventEmitter = new EventEmitter().setMaxListeners(Number.POSITIVE_INFINITY)
54
+ return createRemoteConfig({ eventEmitter, ...deps })
55
+ },
56
+ dependencies: ['fetch', 'freeze', 'config', 'logger'],
57
+ },
58
+ },
59
+ { definition: availableAssetsModuleDefinition },
60
+ {
61
+ definition: {
62
+ id: 'pricingClient',
63
+ factory: createExodusPricingClient,
64
+ dependencies: ['fetch', 'pricingServerUrlAtom'],
65
+ },
66
+ },
38
67
  ].map(withType('module'))
39
68
 
40
69
  export default createModuleDependencies
@@ -0,0 +1,7 @@
1
+ import plugins from '../plugins'
2
+ import { withType } from './utils'
3
+
4
+ const createPluginDependencies = () =>
5
+ plugins.map((definition) => ({ definition })).map(withType('plugin'))
6
+
7
+ export default createPluginDependencies
package/src/index.js CHANGED
@@ -1,35 +1,104 @@
1
+ import { pick } from '@exodus/basic-utils'
1
2
  import createIOC from './ioc'
2
3
  import createApi from './api'
4
+ import {
5
+ createAccountStatesUpdateHandler,
6
+ createLoadWalletAccountsHandler,
7
+ } from './utils/blockchain-metadata'
8
+
9
+ import attachPlugins from './plugins/attach'
10
+ import attachAtoms from './atoms/attach'
11
+ import { atomsToAttach } from './constants'
3
12
 
4
13
  const createExodus = ({ adapters, config, port }) => {
5
14
  const ioc = createIOC({ adapters, config })
15
+ const { headless: headlessConfig } = config
6
16
 
7
17
  const resolve = () => {
8
18
  ioc.resolve()
9
19
 
10
- const { assetsModule } = ioc.getByType('adapter')
20
+ const { assetsModule, storage } = ioc.getByType('adapter')
21
+
22
+ const {
23
+ application,
24
+ blockchainMetadata,
25
+ enabledAssets,
26
+ remoteConfig,
27
+ unlockEncryptedStorage,
28
+ walletAccounts,
29
+ } = ioc.getByType('module')
30
+
31
+ const handleLoadWalletAccounts = createLoadWalletAccountsHandler({
32
+ blockchainMetadata,
33
+ port,
34
+ config,
35
+ })
36
+
37
+ const handleAccountStatesUpdate = createAccountStatesUpdateHandler({
38
+ blockchainMetadata,
39
+ port,
40
+ config,
41
+ })
42
+
43
+ blockchainMetadata.on('load-wallet-accounts', handleLoadWalletAccounts)
44
+ blockchainMetadata.on('tx-logs-update', (payload) => port.emit('tx-logs-update', payload))
45
+ blockchainMetadata.on('account-states-update', handleAccountStatesUpdate)
11
46
 
12
- const { application } = ioc.getByType('module')
47
+ remoteConfig.on('sync', ({ current }) => port.emit('remote-config', current))
13
48
 
14
49
  application.hook('start', () => {
50
+ remoteConfig.load()
51
+
15
52
  port.emit('start')
16
53
  })
17
54
 
18
55
  application.hook('unlock', async () => {
56
+ if (typeof storage.unlock === 'function') unlockEncryptedStorage(storage)
57
+
58
+ remoteConfig.sync()
59
+
19
60
  await assetsModule.load()
61
+ await walletAccounts.load()
62
+ await Promise.all([
63
+ //
64
+ blockchainMetadata.load(),
65
+ enabledAssets.load(),
66
+ ])
67
+ })
68
+
69
+ application.on('unlock', () => {
70
+ port.emit('unlock')
20
71
  })
21
72
 
22
73
  application.hook('clear', async () => {
23
- await Promise.all([assetsModule.clear()])
74
+ await Promise.all([
75
+ //
76
+ assetsModule.clear(),
77
+ walletAccounts.clear(),
78
+ blockchainMetadata.clear(),
79
+ enabledAssets.clear(),
80
+ ])
81
+
82
+ port.emit('clear')
83
+ })
84
+
85
+ application.on('restart', (payload) => {
86
+ port.emit('restart', payload)
24
87
  })
25
88
 
26
- application.on('restart', () => {
27
- port.emit('restart')
89
+ attachAtoms({
90
+ port,
91
+ application,
92
+ logger: ioc.get('createLogger')('attachAtoms'),
93
+ atoms: pick(ioc.getByType('atom'), atomsToAttach),
94
+ lifecycleEvents: headlessConfig?.attachAtomsLifecycleEvents,
28
95
  })
29
96
 
97
+ attachPlugins({ application, plugins: ioc.getByType('plugin') })
98
+
30
99
  application.start()
31
100
 
32
- return createApi({ ioc })
101
+ return createApi({ ioc, port })
33
102
  }
34
103
 
35
104
  return { ...ioc, resolve }
package/src/ioc.js CHANGED
@@ -3,6 +3,7 @@ import preprocess from '@exodus/dependency-preprocessors'
3
3
  import alias from '@exodus/dependency-preprocessors/src/preprocessors/alias'
4
4
  import logify from '@exodus/dependency-preprocessors/src/preprocessors/logify'
5
5
  import namespaceConfig from '@exodus/dependency-preprocessors/src/preprocessors/namespace-config'
6
+ import namespaceStorage from '@exodus/dependency-preprocessors/src/preprocessors/namespace-storage'
6
7
 
7
8
  import createDependencies from './dependencies'
8
9
 
@@ -11,7 +12,13 @@ const createIOC = ({ adapters, config }) => {
11
12
 
12
13
  const logger = createLogger('exodus:ioc')
13
14
  const dependencies = createDependencies({ adapters, config })
14
- const preprocessors = [logify({ createLogger }), namespaceConfig(), alias()]
15
+ const preprocessors = [
16
+ logify({ createLogger }),
17
+ namespaceConfig(),
18
+ alias(),
19
+ // NOTE: order matters, this should come after `alias`
20
+ namespaceStorage(),
21
+ ]
15
22
  const definitions = preprocess({ dependencies, preprocessors })
16
23
  const ioc = createIocContainer({ logger })
17
24
 
@@ -0,0 +1,19 @@
1
+ import { kebabCase, memoize } from 'lodash'
2
+
3
+ // e.g. onUnlock -> unlock -> onUnlock, onChangePassphrase -> change-passphrase
4
+ const getApplicationHookName = memoize((lifecycleMethod) =>
5
+ kebabCase(lifecycleMethod.replace(/^on/, ''))
6
+ )
7
+
8
+ const attachPlugins = ({ plugins, application }) => {
9
+ Object.entries(plugins).forEach(([name, lifecycleMethods]) => {
10
+ const entries = Object.entries(lifecycleMethods || {})
11
+
12
+ for (const [lifecycleMethod, fn] of entries) {
13
+ const hookName = getApplicationHookName(lifecycleMethod)
14
+ application.hook(hookName, fn)
15
+ }
16
+ })
17
+ }
18
+
19
+ export default attachPlugins
@@ -0,0 +1,3 @@
1
+ import logLifecyclePlugin from './log-lifecycle'
2
+
3
+ export default [logLifecyclePlugin]
@@ -0,0 +1,25 @@
1
+ const logLifecycle = {
2
+ id: 'logLifecyclePlugin',
3
+ type: 'plugin',
4
+ factory: ({ logger }) => {
5
+ return {
6
+ onLock: () => logger.debug('onLock'),
7
+ onUnlock: () => logger.debug('onUnlock'),
8
+ onClear: () => logger.debug('onClear'),
9
+ onImport: () => logger.debug('onImport'),
10
+ onMigrate: () => logger.debug('onMigrate'),
11
+ onStart: () => logger.debug('onStart'),
12
+ onLoad: () => logger.debug('onLoad'),
13
+ onUnload: () => logger.debug('onUnload'),
14
+ onCreate: () => logger.debug('onCreate'),
15
+ onBackup: () => logger.debug('onBackup'),
16
+ onRestore: () => logger.debug('onRestore'),
17
+ onRestoreCompleted: () => logger.debug('onRestoreCompleted'),
18
+ onAssetsSynced: () => logger.debug('onAssetsSynced'),
19
+ onChangePassphrase: () => logger.debug('onChangePassphrase'),
20
+ }
21
+ },
22
+ dependencies: ['logger'],
23
+ }
24
+
25
+ export default logLifecycle
@@ -0,0 +1,19 @@
1
+ import { EXODUS_KEY_IDS } from '@exodus/keychain/module'
2
+
3
+ const createUnlockEncryptedStorage = ({ keychain }) => {
4
+ return async (encryptedStorage) => {
5
+ // should the key id be part of a `config` dep?
6
+ const sodiumEncryptor = keychain.createSodiumEncryptor(EXODUS_KEY_IDS.WALLET_INFO)
7
+
8
+ await encryptedStorage.unlock({
9
+ encrypt: (data) => sodiumEncryptor.encryptSecretBox({ data }),
10
+ decrypt: (data) => sodiumEncryptor.decryptSecretBox({ data }),
11
+ })
12
+ }
13
+ }
14
+
15
+ export default {
16
+ id: 'unlockEncryptedStorage',
17
+ factory: createUnlockEncryptedStorage,
18
+ dependencies: ['keychain'],
19
+ }
@@ -0,0 +1,48 @@
1
+ import { mapValues } from '@exodus/basic-utils'
2
+
3
+ const createAccountStatesTransformer = ({ serializePortPayloads, blockchainMetadata }) => {
4
+ return serializePortPayloads
5
+ ? (payload) =>
6
+ mapValues(payload, (byAsset, walletAccount) =>
7
+ mapValues(
8
+ byAsset,
9
+ (accountState, assetName) =>
10
+ blockchainMetadata.serializePayload({ walletAccount, assetName, accountState })
11
+ .accountState
12
+ )
13
+ )
14
+ : (payload) => payload
15
+ }
16
+
17
+ export function createAccountStatesUpdateHandler({
18
+ port,
19
+ blockchainMetadata,
20
+ config: { serializePortPayloads = false },
21
+ }) {
22
+ const transform = createAccountStatesTransformer({ serializePortPayloads, blockchainMetadata })
23
+
24
+ return (payload) => {
25
+ port.emit('account-states-update', transform(payload))
26
+ }
27
+ }
28
+
29
+ export function createLoadWalletAccountsHandler({
30
+ port,
31
+ blockchainMetadata,
32
+ config: { serializePortPayloads = false },
33
+ }) {
34
+ const transformAccountStates = createAccountStatesTransformer({
35
+ serializePortPayloads,
36
+ blockchainMetadata,
37
+ })
38
+
39
+ return async () => {
40
+ const [txLogs, accountStates] = await Promise.all([
41
+ blockchainMetadata.getLoadedTxLogs(),
42
+ blockchainMetadata.getLoadedAccountStates(),
43
+ ])
44
+
45
+ port.emit('tx-logs', txLogs)
46
+ port.emit('account-states', transformAccountStates(accountStates))
47
+ }
48
+ }