edge-currency-plugins 3.15.0 → 3.16.0

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.
@@ -156,9 +156,25 @@ var _utils3 = require('./utils');
156
156
  * 2. Address UTXO processing
157
157
  **/
158
158
  const processesPerAddress = 2
159
- let processedCount = 0
159
+ // Which of the two processes have completed, per address (by scriptPubkey).
160
+ // Flags rather than a running count, so an address re-queued after a server
161
+ // drop is not counted twice and the ratio cannot pass 1 with work pending.
162
+ const progressByScriptPubkey = new Map
163
+
164
+
165
+ ()
160
166
  let processedPercent = 0 // last sync ratio emitted
161
- const updateProgressRatio = async () => {
167
+ const updateProgressRatio = async (
168
+ scriptPubkey,
169
+ process
170
+ ) => {
171
+ const progress = _nullishCoalesce(progressByScriptPubkey.get(scriptPubkey), () => ( {
172
+ txs: false,
173
+ utxos: false
174
+ }))
175
+ progress[process] = true
176
+ progressByScriptPubkey.set(scriptPubkey, progress)
177
+
162
178
  // Avoid re-sending sync ratios / sending ratios larger than 1
163
179
  if (processedPercent >= 1) return
164
180
 
@@ -167,8 +183,11 @@ var _utils3 = require('./utils');
167
183
  const expectedProcessCount =
168
184
  Object.keys(taskCache.addressSubscribeCache).length * processesPerAddress
169
185
 
170
- // Increment the processed count
171
- processedCount = processedCount + 1
186
+ let processedCount = 0
187
+ for (const { txs, utxos } of progressByScriptPubkey.values()) {
188
+ if (txs) processedCount++
189
+ if (utxos) processedCount++
190
+ }
172
191
 
173
192
  // If we have no addresses, we should not have not yet began processing.
174
193
  if (expectedProcessCount === 0) throw new Error('No addresses to process')
@@ -244,8 +263,14 @@ var _utils3 = require('./utils');
244
263
  if (running) return
245
264
  running = true
246
265
 
247
- await initializeAddressSubscriptions()
248
- await setLookAhead(common)
266
+ try {
267
+ await initializeAddressSubscriptions()
268
+ await setLookAhead(common)
269
+ } catch (error) {
270
+ // Let the next call try again instead of skipping the initialization:
271
+ running = false
272
+ throw error
273
+ }
249
274
  }
250
275
 
251
276
  emitter.on(
@@ -265,6 +290,30 @@ var _utils3 = require('./utils');
265
290
  }
266
291
  )
267
292
 
293
+ emitter.on(_EngineEmitter.EngineEvent.SERVER_DROPPED, (uri) => {
294
+ // Everything this server told us about may stop short of the chain tip.
295
+ // Re-open every address so the surviving connection re-queries it. Each
296
+ // query starts from the address's stored lastQueriedBlockHeight, which is
297
+ // never above the height of the server that answered it, so the range
298
+ // the dropped server missed is fetched again.
299
+ let requeued = 0
300
+ for (const [address, cacheItem] of Object.entries(
301
+ taskCache.addressSubscribeCache
302
+ )) {
303
+ if (!cacheItem.processing) continue
304
+ cacheItem.processing = false
305
+ taskCache.addressForUtxosCache[address] = {
306
+ processing: false,
307
+ path: cacheItem.path
308
+ }
309
+ // The re-query has to complete again before the address counts as
310
+ // processed:
311
+ progressByScriptPubkey.delete(walletTools.addressToScriptPubkey(address))
312
+ requeued++
313
+ }
314
+ log(`${uri} dropped, re-queued ${requeued} addresses`)
315
+ })
316
+
268
317
  emitter.on(
269
318
  _EngineEmitter.EngineEvent.NEW_ADDRESS_TRANSACTION,
270
319
  async (_uri, response) => {
@@ -290,6 +339,30 @@ var _utils3 = require('./utils');
290
339
  }
291
340
  )
292
341
 
342
+ // The DataLayer can count an address that it can no longer read, such as
343
+ // when a write to disk was cut short. Every address derives from the wallet
344
+ // keys and its path, so rebuild the missing row instead of failing the
345
+ // engine. The rebuilt row has no sync state, so the address syncs again.
346
+ const repairAddress = async (path) => {
347
+ const { format, changeIndex, addressIndex } = path
348
+ const pathName = `${format}/${changeIndex}/${addressIndex}`
349
+ log.warn(`Repairing missing data-layer address with '${pathName}' path`)
350
+
351
+ const { scriptPubkey, redeemScript } = walletTools.getScriptPubkey(path)
352
+ await dataLayer.saveAddress(
353
+ _types5.makeAddressData.call(void 0, { scriptPubkey, redeemScript, path })
354
+ )
355
+
356
+ // Read the row back, since the save keeps any state that survived:
357
+ const addressData = await dataLayer.fetchAddress(path)
358
+ if (addressData == null) {
359
+ throw new Error(
360
+ `Missing data-layer address with '${pathName}' path during initialization`
361
+ )
362
+ }
363
+ return addressData
364
+ }
365
+
293
366
  // Initialize the addressSubscribeCache with the existing addresses already
294
367
  // processed by the DataLayer. This happens only once before any call to
295
368
  // setLookAhead.
@@ -318,16 +391,13 @@ var _utils3 = require('./utils');
318
391
  addressIndex < branchAddressCount;
319
392
  addressIndex++
320
393
  ) {
321
- const addressData = await dataLayer.fetchAddress({
394
+ const path = {
322
395
  format,
323
396
  changeIndex: branch,
324
397
  addressIndex
325
- })
326
- if (addressData == null) {
327
- throw new Error(
328
- `Missing data-layer address with '${format}/${branch}/${addressIndex}' path during initialization`
329
- )
330
398
  }
399
+ const addressData =
400
+ await _asyncNullishCoalesce((await dataLayer.fetchAddress(path)), async () => ( (await repairAddress(path))))
331
401
  const { address } = walletTools.scriptPubkeyToAddress({
332
402
  changePath,
333
403
  scriptPubkey: addressData.scriptPubkey
@@ -360,7 +430,7 @@ var _utils3 = require('./utils');
360
430
  return {
361
431
  processedPercent,
362
432
  async start() {
363
- processedCount = 0
433
+ progressByScriptPubkey.clear()
364
434
  processedPercent = 0
365
435
 
366
436
  await run()
@@ -694,6 +764,12 @@ const internalGetFreshAddress = async (
694
764
 
695
765
 
696
766
 
767
+
768
+
769
+
770
+
771
+
772
+
697
773
 
698
774
 
699
775
 
@@ -1208,6 +1284,17 @@ async function* processAddressForTransactions(
1208
1284
  throw new Error(`could not find address with script pubkey ${scriptPubkey}`)
1209
1285
  }
1210
1286
 
1287
+ // The address was queued with the height of whichever server subscribed
1288
+ // it, but any server can answer the query. Never record more than the
1289
+ // answering server has indexed, so a lagging server cannot move
1290
+ // lastQueriedBlockHeight past blocks it never saw. A height of 0 means this
1291
+ // server has not reported one yet, so nothing it answers can be trusted to
1292
+ // move the cursor; the next query covers the range again.
1293
+ const queriedHeight =
1294
+ serverState.blockHeight > 0
1295
+ ? Math.min(blockHeight, serverState.blockHeight)
1296
+ : addressData.lastQueriedBlockHeight
1297
+
1211
1298
  try {
1212
1299
  const addressResponse = yield* common.serverStates.addressQueryTask(
1213
1300
  serverUri,
@@ -1287,23 +1374,25 @@ async function* processAddressForTransactions(
1287
1374
  addressForTransactionsCache[address] = {
1288
1375
  ...cacheItem,
1289
1376
  processing: false,
1290
- page: page + 1
1377
+ page: page + 1,
1378
+ blockHeight: queriedHeight
1291
1379
  }
1292
1380
  return true
1293
1381
  }
1294
1382
 
1295
1383
  // Update the lastQueriedBlockHeight for the address
1296
- addressData.lastQueriedBlockHeight = blockHeight
1384
+ addressData.lastQueriedBlockHeight = queriedHeight
1297
1385
 
1298
1386
  // Save/update the fully-processed address
1299
1387
  await common.dataLayer.saveAddress(addressData)
1300
1388
 
1301
- // Update the progress now that the transactions for an address have processed
1302
- await common.updateProgressRatio()
1303
-
1304
- // Call setLookAhead to update the lookahead
1389
+ // Extend the lookahead before counting progress, so any addresses it adds
1390
+ // are in the total before this address's flag can bring the ratio to 1:
1305
1391
  await setLookAhead(common)
1306
1392
 
1393
+ // Update the progress now that the transactions for an address have processed
1394
+ await common.updateProgressRatio(scriptPubkey, 'txs')
1395
+
1307
1396
  // Add the address to the server cache
1308
1397
  serverState.addresses.add(address)
1309
1398
 
@@ -1573,13 +1662,14 @@ const processDataLayerUtxos = async (
1573
1662
  })
1574
1663
  }
1575
1664
 
1576
- // Update the progress now that the UTXOs for an address have been processed
1577
- await common.updateProgressRatio()
1578
-
1665
+ // Extend the lookahead first, as in processAddressForTransactions:
1579
1666
  await setLookAhead(common).catch(err => {
1580
1667
  common.log.error(err)
1581
1668
  throw err
1582
1669
  })
1670
+
1671
+ // Update the progress now that the UTXOs for an address have been processed
1672
+ await common.updateProgressRatio(scriptPubkey, 'utxos')
1583
1673
  }
1584
1674
 
1585
1675
  /**
@@ -103,6 +103,9 @@ var _Socket = require('./Socket');
103
103
 
104
104
 
105
105
 
106
+
107
+
108
+
106
109
 
107
110
 
108
111
 
@@ -277,7 +280,9 @@ var _Socket = require('./Socket');
277
280
  taskGeneratorFn,
278
281
  log,
279
282
  emitter: socketEmitter,
280
- walletId
283
+ walletId,
284
+ keepAliveMs: config.keepAliveMs,
285
+ wakeUpMs: config.wakeUpMs
281
286
  })
282
287
 
283
288
  async function ping() {
@@ -87,6 +87,10 @@ const WAKE_UP_MS = 5000 // interval at which we wakeUp and potentially onQueueSp
87
87
 
88
88
 
89
89
 
90
+
91
+
92
+
93
+
90
94
 
91
95
 
92
96
 
@@ -134,6 +138,8 @@ const asResponseDefault = (raw) => [
134
138
  let connected = false
135
139
  let cancelConnect = false
136
140
  const timeout = 1000 * (_nullishCoalesce(config.timeout, () => ( 30)))
141
+ const keepAliveMs = _nullishCoalesce(config.keepAliveMs, () => ( KEEP_ALIVE_MS))
142
+ const wakeUpMs = _nullishCoalesce(config.wakeUpMs, () => ( WAKE_UP_MS))
137
143
  let trackedError
138
144
  let timer
139
145
 
@@ -264,7 +270,7 @@ const asResponseDefault = (raw) => [
264
270
  const onTimer = () => {
265
271
  log(`socket timer with server ${uri} expired, check if healthCheck needed`)
266
272
  const now = Date.now() - TIMER_SLACK
267
- if (lastKeepAlive + KEEP_ALIVE_MS < now) {
273
+ if (lastKeepAlive + keepAliveMs < now) {
268
274
  log(`submitting healthCheck to server ${uri}`)
269
275
  lastKeepAlive = now
270
276
  config
@@ -291,7 +297,7 @@ const asResponseDefault = (raw) => [
291
297
 
292
298
  const setupTimer = () => {
293
299
  log(`setupTimer with server ${uri}`)
294
- let nextWakeUp = lastWakeUp + WAKE_UP_MS
300
+ let nextWakeUp = lastWakeUp + wakeUpMs
295
301
  for (const request of Object.values(pendingRequests)) {
296
302
  const to = request.startTime + timeout
297
303
  if (to < nextWakeUp) nextWakeUp = to
@@ -6,11 +6,11 @@ export declare interface EngineEmitter {
6
6
  emit: ((event: EngineEvent.SEEN_TX_CHECKPOINT, checkpoint: string) => boolean) & ((event: EngineEvent.TRANSACTIONS, transactionEvents: EdgeTransactionEvent[]) => boolean) & ((event: EngineEvent.TRANSACTIONS_CHANGED, transactions: EdgeTransaction[]) => boolean) & ((event: EngineEvent.ADDRESS_BALANCE_CHANGED, currencyCode: string, addressBalanceChanges: Array<{
7
7
  scriptPubkey: string;
8
8
  balance: string;
9
- }>) => boolean) & ((event: EngineEvent.WALLET_BALANCE_CHANGED, currencyCode: string, nativeBalance: string) => boolean) & ((event: EngineEvent.BLOCK_HEIGHT_CHANGED, uri: string, blockHeight: number) => boolean) & ((event: EngineEvent.NEW_ADDRESS_TRANSACTION, uri: string, newTx: SubscribeAddressResponse) => boolean) & ((event: EngineEvent.ADDRESSES_CHECKED, progressRatio: number) => boolean) & ((event: EngineEvent.TXIDS_CHANGED, txids: EdgeTxidMap) => boolean);
9
+ }>) => boolean) & ((event: EngineEvent.WALLET_BALANCE_CHANGED, currencyCode: string, nativeBalance: string) => boolean) & ((event: EngineEvent.BLOCK_HEIGHT_CHANGED, uri: string, blockHeight: number) => boolean) & ((event: EngineEvent.NEW_ADDRESS_TRANSACTION, uri: string, newTx: SubscribeAddressResponse) => boolean) & ((event: EngineEvent.ADDRESSES_CHECKED, progressRatio: number) => boolean) & ((event: EngineEvent.TXIDS_CHANGED, txids: EdgeTxidMap) => boolean) & ((event: EngineEvent.SERVER_DROPPED, uri: string) => boolean);
10
10
  on: ((event: EngineEvent.SEEN_TX_CHECKPOINT, listener: (checkpoint: string) => Promise<void> | void) => this) & ((event: EngineEvent.TRANSACTIONS, listener: (transactionEvents: EdgeTransactionEvent[]) => Promise<void> | void) => boolean) & ((event: EngineEvent.TRANSACTIONS_CHANGED, listener: (transactions: EdgeTransaction[]) => Promise<void> | void) => this) & ((event: EngineEvent.ADDRESS_BALANCE_CHANGED, listener: (currencyCode: string, addressBalanceChanges: Array<{
11
11
  scriptPubkey: string;
12
12
  balance: string;
13
- }>) => Promise<void> | void) => this) & ((event: EngineEvent.WALLET_BALANCE_CHANGED, listener: (currencyCode: string, nativeBalance: string) => Promise<void> | void) => this) & ((event: EngineEvent.BLOCK_HEIGHT_CHANGED, listener: (uri: string, blockHeight: number) => Promise<void> | void) => this) & ((event: EngineEvent.NEW_ADDRESS_TRANSACTION, listener: (uri: string, newTx: SubscribeAddressResponse) => Promise<void> | void) => this) & ((event: EngineEvent.ADDRESSES_CHECKED, listener: (progressRatio: number) => Promise<void> | void) => this) & ((event: EngineEvent.TXIDS_CHANGED, listener: (txids: EdgeTxidMap) => Promise<void> | void) => this);
13
+ }>) => Promise<void> | void) => this) & ((event: EngineEvent.WALLET_BALANCE_CHANGED, listener: (currencyCode: string, nativeBalance: string) => Promise<void> | void) => this) & ((event: EngineEvent.BLOCK_HEIGHT_CHANGED, listener: (uri: string, blockHeight: number) => Promise<void> | void) => this) & ((event: EngineEvent.NEW_ADDRESS_TRANSACTION, listener: (uri: string, newTx: SubscribeAddressResponse) => Promise<void> | void) => this) & ((event: EngineEvent.ADDRESSES_CHECKED, listener: (progressRatio: number) => Promise<void> | void) => this) & ((event: EngineEvent.TXIDS_CHANGED, listener: (txids: EdgeTxidMap) => Promise<void> | void) => this) & ((event: EngineEvent.SERVER_DROPPED, listener: (uri: string) => Promise<void> | void) => this);
14
14
  }
15
15
  export declare class EngineEmitter extends EventEmitter {
16
16
  }
@@ -25,6 +25,11 @@ export declare enum EngineEvent {
25
25
  NEW_ADDRESS_TRANSACTION = "address:transaction:changed",
26
26
  ADDRESSES_CHECKED = "addresses:checked",
27
27
  TXIDS_CHANGED = "txids:changed",
28
+ /**
29
+ * The engine disconnected a server on purpose because it was found to be
30
+ * out of sync. Data fetched through it may be behind the chain.
31
+ */
32
+ SERVER_DROPPED = "server:dropped",
28
33
  CONNECTION_OPEN = "connection:open",
29
34
  CONNECTION_CLOSE = "connection:close",
30
35
  CONNECTION_TIMER = "connection:timer"
@@ -1,8 +1,23 @@
1
1
  import { Disklet } from 'disklet';
2
2
  import { EdgeIo, EdgeLog } from 'edge-core-js/types';
3
+ import { ServerHealth } from '../utxobased/engine/ServerStates';
3
4
  import { UtxoUserSettings } from '../utxobased/engine/types';
4
5
  import { UtxoEngineProcessor } from '../utxobased/engine/UtxoEngineProcessor';
5
6
  import { InfoPayload } from './types';
7
+ /**
8
+ * How long a server dropped for being out of sync stays out of the pick
9
+ * order. Long enough for a lagging indexer to catch up, short enough that a
10
+ * recovered server is tried again without an app restart. After expiry the
11
+ * server is probed again on connect and dropped again if still behind.
12
+ */
13
+ export declare const SERVER_QUARANTINE_MS: number;
14
+ /**
15
+ * Runs a server's health probe, or hands back the one started for the same
16
+ * URI less than `minIntervalMs` ago, whether it is still in flight or has
17
+ * already answered.
18
+ */
19
+ export declare type ShareServerProbe = (uri: string, minIntervalMs: number, probe: () => Promise<ServerHealth>) => Promise<ServerHealth>;
20
+ export declare function makeShareServerProbe(): ShareServerProbe;
6
21
  /** A JSON object (as opposed to an array or primitive). */
7
22
  interface JsonObject {
8
23
  [name: string]: unknown;
@@ -28,6 +43,21 @@ export interface PluginState {
28
43
  load: () => Promise<PluginState>;
29
44
  serverScoreDown: (uri: string) => void;
30
45
  serverScoreUp: (uri: string, score: number) => void;
46
+ /**
47
+ * Keeps a server out of getLocalServers for a while. Shared by every
48
+ * engine of this plugin, so one wallet's finding spares the others the
49
+ * same connect-probe-drop cycle. In memory only: it does not outlive the
50
+ * plugin, and a restart gives the server a fresh chance.
51
+ */
52
+ quarantineServer: (uri: string, durationMs?: number) => void;
53
+ isServerQuarantined: (uri: string) => boolean;
54
+ hasQuarantinedServers: () => boolean;
55
+ /**
56
+ * Shares health probes between every engine of this plugin, so N wallets
57
+ * connected to the same server cost one REST request per interval rather
58
+ * than N. Each engine still applies the answer to its own connection.
59
+ */
60
+ shareServerProbe: ShareServerProbe;
31
61
  clearCache: () => Promise<void>;
32
62
  getLocalServers: (numServersWanted: number, includePatterns?: Array<string | RegExp>) => string[];
33
63
  refreshServers: (updatedCustomServers?: string[]) => Promise<void>;
@@ -8,10 +8,20 @@ import { AddressResponse, AddressUtxosResponse, BlockbookTransaction } from '../
8
8
  import Deferred from '../network/Deferred';
9
9
  import { WsTask, WsTaskGenerator } from '../network/Socket';
10
10
  import { UtxoInitOptions } from './types';
11
+ /**
12
+ * What the last REST health probe said about a connected server:
13
+ * - `pending`: the probe has not answered yet.
14
+ * - `healthy`: Blockbook reported `inSync: true`.
15
+ * - `unhealthy`: Blockbook reported `inSync: false`.
16
+ * - `unknown`: the server has no HTTP twin to probe, or the probe failed
17
+ * (network error, non-200, unparseable body). Never treated as unhealthy.
18
+ */
19
+ export declare type ServerHealth = 'pending' | 'healthy' | 'unhealthy' | 'unknown';
11
20
  export interface ServerState {
12
21
  blockbook: Blockbook;
13
22
  blockSubscriptionStatus: 'unsubscribed' | 'subscribing' | 'subscribed';
14
23
  blockHeight: number;
24
+ health: ServerHealth;
15
25
  txids: Set<string>;
16
26
  addresses: Set<string>;
17
27
  }
@@ -28,6 +38,14 @@ interface ServerStateConfig {
28
38
  * seconds the socket layer uses for a request. Exposed for tests.
29
39
  */
30
40
  broadcastTimeoutMs?: number;
41
+ /**
42
+ * Timing overrides so tests can drive the keepalive probe cycle in
43
+ * milliseconds rather than minutes. Production uses the defaults.
44
+ */
45
+ healthProbeMinIntervalMs?: number;
46
+ healthProbeTimeoutMs?: number;
47
+ keepAliveMs?: number;
48
+ wakeUpMs?: number;
31
49
  }
32
50
  export interface ServerStates {
33
51
  setPickNextTaskCB: (callback: (uri: string) => AsyncGenerator<WsTask<unknown> | boolean>) => void;
@@ -43,6 +61,7 @@ export interface ServerStates {
43
61
  watchAddresses: (uri: string, addresses: string[], deferredAddressSub?: Deferred<unknown>) => void;
44
62
  watchBlocks: (uri: string) => void;
45
63
  getBlockHeight: (uri: string) => number;
64
+ getServerHealth: (uri: string) => ServerHealth | undefined;
46
65
  addressQueryTask: (serverUri: string, address: string, params: {
47
66
  lastQueriedBlockHeight: number;
48
67
  page: number;
@@ -67,5 +86,17 @@ export declare const NOWNODES_BROADCAST_DELAY_MS = 2000;
67
86
  * open. This mirrors the socket figure.
68
87
  */
69
88
  export declare const BROADCAST_ATTEMPT_TIMEOUT_MS = 30000;
89
+ /**
90
+ * Upper bound on a REST health probe. The probe runs off the socket keepalive
91
+ * and must never hold the connection's timer for long.
92
+ */
93
+ export declare const HEALTH_PROBE_TIMEOUT_MS = 10000;
94
+ /**
95
+ * Probes of one server closer together than this share a single request,
96
+ * across every wallet of the plugin. The connect path and the first
97
+ * keepalive can land within milliseconds of each other, and N wallets each
98
+ * run their own keepalive; one answer serves them all.
99
+ */
100
+ export declare const HEALTH_PROBE_MIN_INTERVAL_MS = 10000;
70
101
  export declare function makeServerStates(config: ServerStateConfig): ServerStates;
71
102
  export {};
@@ -42,7 +42,7 @@ interface CommonParams {
42
42
  emitter: EngineEmitter;
43
43
  taskCache: TaskCache;
44
44
  pendingTimeouts: Set<NodeJS.Timeout>;
45
- updateProgressRatio: () => void;
45
+ updateProgressRatio: (scriptPubkey: string, process: AddressProcess) => Promise<void>;
46
46
  updateSeenTxCheckpoint: () => void;
47
47
  io: EdgeIo;
48
48
  log: EdgeLog;
@@ -75,6 +75,8 @@ interface AddressForUtxosCache {
75
75
  path: ChangePath;
76
76
  };
77
77
  }
78
+ /** The two per-address jobs that make up the sync progress ratio. */
79
+ declare type AddressProcess = 'txs' | 'utxos';
78
80
  interface AddressSubscribeCache {
79
81
  [key: string]: {
80
82
  processing: boolean;
@@ -42,6 +42,9 @@ interface BlockbookConfig {
42
42
  ping?: () => Promise<void>;
43
43
  socketEmitter: SocketEmitter;
44
44
  walletId: string;
45
+ /** Socket timing overrides, for tests. */
46
+ keepAliveMs?: number;
47
+ wakeUpMs?: number;
45
48
  }
46
49
  export declare function makeBlockbook(config: BlockbookConfig): Blockbook;
47
50
  export {};
@@ -39,6 +39,10 @@ interface SocketConfig {
39
39
  taskGeneratorFn: TaskGeneratorFn;
40
40
  timeout?: number;
41
41
  walletId: string;
42
+ /** Keepalive interval override, for tests. Defaults to KEEP_ALIVE_MS. */
43
+ keepAliveMs?: number;
44
+ /** Timer wake-up interval override, for tests. Defaults to WAKE_UP_MS. */
45
+ wakeUpMs?: number;
42
46
  }
43
47
  export declare type WsResponse = WsResponseMessage[];
44
48
  export interface WsResponseMessage {
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-currency-plugins",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "Edge currency plugins to fetch and process wallet accounts, balances and transactions.",
5
5
  "homepage": "https://edge.app/",
6
6
  "repository": "https://github.com/EdgeApp/edge-currency-plugins",