edge-currency-plugins 3.15.0 → 3.17.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.
Files changed (33) hide show
  1. package/android/src/main/assets/edge-currency-plugins/edge-currency-plugins.js +51 -49
  2. package/lib/common/plugin/EngineEmitter.js +10 -0
  3. package/lib/common/plugin/PluginState.js +87 -1
  4. package/lib/common/plugin/types.js +11 -0
  5. package/lib/common/utxobased/engine/ServerStates.js +257 -9
  6. package/lib/common/utxobased/engine/UtxoEngineProcessor.js +147 -22
  7. package/lib/common/utxobased/info/all.js +6 -0
  8. package/lib/common/utxobased/info/bitcoin.js +1 -1
  9. package/lib/common/utxobased/info/ecashbitcoin.js +90 -0
  10. package/lib/common/utxobased/info/ecashbitcoinbetanet.js +91 -0
  11. package/lib/common/utxobased/keymanager/keymanager.js +6 -1
  12. package/lib/common/utxobased/network/Blockbook.js +9 -2
  13. package/lib/common/utxobased/network/BlockbookElectrum.js +243 -77
  14. package/lib/common/utxobased/network/Socket.js +34 -22
  15. package/lib/common/utxobased/network/electrumApi.js +49 -1
  16. package/lib/src/common/plugin/EngineEmitter.d.ts +7 -2
  17. package/lib/src/common/plugin/PluginState.d.ts +30 -0
  18. package/lib/src/common/plugin/types.d.ts +10 -0
  19. package/lib/src/common/utxobased/engine/ServerStates.d.ts +32 -0
  20. package/lib/src/common/utxobased/engine/UtxoEngineProcessor.d.ts +5 -1
  21. package/lib/src/common/utxobased/info/all.d.ts +2 -0
  22. package/lib/src/common/utxobased/info/ecashbitcoin.d.ts +3 -0
  23. package/lib/src/common/utxobased/info/ecashbitcoinbetanet.d.ts +3 -0
  24. package/lib/src/common/utxobased/network/Blockbook.d.ts +6 -1
  25. package/lib/src/common/utxobased/network/BlockbookElectrum.d.ts +2 -0
  26. package/lib/src/common/utxobased/network/Socket.d.ts +4 -0
  27. package/lib/src/common/utxobased/network/electrumApi.d.ts +21 -0
  28. package/lib/test/common/plugin/PluginState.spec.d.ts +1 -0
  29. package/lib/test/common/utxobased/engine/AddressRepair.spec.d.ts +1 -0
  30. package/lib/test/common/utxobased/engine/UtxoEngineProcessor.spec.d.ts +1 -0
  31. package/lib/test/common/utxobased/keymanager/coins/ecashbitcointransactiontest.spec.d.ts +1 -0
  32. package/lib/test/common/utxobased/network/BlockbookElectrum.spec.d.ts +1 -0
  33. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _altcoinjs = require('altcoin-js');
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _altcoinjs = require('altcoin-js');
2
+
2
3
 
3
4
 
4
5
 
@@ -22,6 +23,17 @@ var _Blockbook = require('./Blockbook');
22
23
 
23
24
 
24
25
 
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
25
37
  var _electrumApi = require('./electrumApi');
26
38
 
27
39
 
@@ -49,8 +61,9 @@ var _electrumApi = require('./electrumApi');
49
61
 
50
62
 
51
63
 
52
- const asResponseMessage = (raw) => {
53
- const message = _cleaners.asJSON.call(void 0,
64
+
65
+ const asElectrumMessage = _cleaners.asJSON.call(void 0,
66
+ _cleaners.asEither.call(void 0,
54
67
  _cleaners.asObject.call(void 0, {
55
68
  id: _cleaners.asString,
56
69
  result: _cleaners.asUnknown,
@@ -60,8 +73,30 @@ const asResponseMessage = (raw) => {
60
73
  _cleaners.asObject.call(void 0, { connected: _cleaners.asString })
61
74
  )
62
75
  )
76
+ }),
77
+ // A subscription's notifications carry its method in place of an id:
78
+ _cleaners.asObject.call(void 0, {
79
+ method: _cleaners.asString,
80
+ params: _cleaners.asArray.call(void 0, _cleaners.asUnknown)
63
81
  })
64
- )(raw)
82
+ )
83
+ )
84
+
85
+ const { method: headersSubscribeMethod } = _electrumApi.headersSubscribeMessage.call(void 0, )
86
+
87
+ const asResponseMessage = (raw) => {
88
+ const message = asElectrumMessage(raw)
89
+
90
+ // The socket files each subscription under its method name:
91
+ if ('method' in message) {
92
+ return { id: message.method, data: message.params[0] }
93
+ }
94
+
95
+ // Electrum answers a subscription with the current header, not the
96
+ // acknowledgement the socket waits for. fetchInfo reports that height:
97
+ if (message.id === headersSubscribeMethod && message.error == null) {
98
+ return { id: message.id, data: { subscribed: true } }
99
+ }
65
100
 
66
101
  return {
67
102
  id: message.id,
@@ -70,11 +105,20 @@ const asResponseMessage = (raw) => {
70
105
  }
71
106
  }
72
107
 
73
- const asResponse = (raw) => {
108
+ const makeAsResponse = (
109
+ isWatchingBlocks
110
+ ) => (raw) => {
74
111
  const payloadString = _cleaners.asString.call(void 0, raw).trimEnd()
75
112
  const messageStrings = payloadString.split('\n')
76
113
  const messages = messageStrings.map(asResponseMessage)
77
- return messages
114
+
115
+ // fetchInfo's blockchain.headers.subscribe subscribes the connection to new
116
+ // blocks, so the server can announce one before watchBlocks files the
117
+ // subscription. The socket drops connections over messages it cannot route,
118
+ // so announcements until then are skipped. watchBlocks reports each block
119
+ // after it subscribes:
120
+ if (isWatchingBlocks()) return messages
121
+ return messages.filter(message => message.id !== headersSubscribeMethod)
78
122
  }
79
123
 
80
124
  function makeBlockbookElectrum(
@@ -98,14 +142,149 @@ const asResponse = (raw) => {
98
142
  address,
99
143
  coin: pluginInfo.coinInfo.name
100
144
  })
101
- const scriptHashBuffer = _altcoinjs.crypto.sha256(Buffer.from(scriptPubkey, 'hex'))
102
- const scriptHash = scriptHashBuffer.reverse().toString('hex')
103
- return scriptHash
145
+ return toElectrumScriptHash(Buffer.from(scriptPubkey, 'hex'))
104
146
  }
105
147
 
148
+ // Makes the Electrum requests for a Blockbook task, and returns the
149
+ // Blockbook response built from their answers:
150
+ function* translateTask(
151
+ task
152
+ ) {
153
+ // Translate getAccountUtxo to blockchain.scripthash.listunspent:
154
+ if (task.method === 'getAccountUtxo') {
155
+ const params = task.params
156
+ const address =
157
+ asAddress != null ? asAddress(params.descriptor) : params.descriptor
158
+ const scriptHash = addressToScriptHash(address)
159
+
160
+ // Safe because yielding WsTask<ListUnspentResponse> should get
161
+ // ListUnspentResponse next.
162
+ const electrumUtxos = (yield {
163
+ ..._electrumApi.listUnspentMessage.call(void 0, scriptHash)
164
+ })
165
+
166
+ const blockbookUtxos = electrumUtxos.map(
167
+ utxo => ({
168
+ txid: utxo.tx_hash,
169
+ vout: utxo.tx_pos,
170
+ value: utxo.value.toString(),
171
+ height: utxo.height
172
+ })
173
+ )
174
+ return blockbookUtxos
175
+ }
176
+
177
+ // Get Address Transactions:
178
+ if (task.method === 'getAccountInfo') {
179
+ const params = task.params
180
+ const address =
181
+ asAddress != null ? asAddress(params.descriptor) : params.descriptor
182
+ const scriptHash = addressToScriptHash(address)
183
+
184
+ // Safe because yielding WsTask<AddressTransactionsResponse> should get
185
+ // AddressTransactionsResponse next.
186
+ const electrumUtxos = (yield {
187
+ ..._electrumApi.addressTransactionsMessage.call(void 0, scriptHash)
188
+ })
189
+
190
+ const blockbookUtxos = {
191
+ address, // unused
192
+ balance: '0', // unused
193
+ totalReceived: '0', // unused
194
+ totalSent: '0', // unused
195
+ txs: electrumUtxos.length,
196
+ unconfirmedBalance: '0', // unused
197
+ unconfirmedTxs: electrumUtxos.reduce(
198
+ // Electrum puts mempool transactions at height 0 or -1:
199
+ (sum, tx) => sum + (tx.height <= 0 ? 1 : 0),
200
+ 0
201
+ ),
202
+ txids: [], // unused
203
+ transactions: [], // TODO: this requires an extra query per txid
204
+ page: 0, // unused
205
+ totalPages: 1,
206
+ itemsOnPage: 0 // unused
207
+ }
208
+ return blockbookUtxos
209
+ }
210
+
211
+ // Translate getTransaction to blockchain.transaction.get:
212
+ if (task.method === 'getTransaction') {
213
+ const params = task.params
214
+ return yield* transactionTask(params.txid)
215
+ }
216
+
217
+ throw new Error(`${task.method} not supported for Electrum connections`)
218
+ }
219
+
220
+ function* transactionTask(
221
+ txid
222
+ ) {
223
+ // Safe because yielding WsTask<RawTransactionResponse> should get
224
+ // RawTransactionResponse next.
225
+ const hex = (yield {
226
+ ..._electrumApi.rawTransactionMessage.call(void 0, txid)
227
+ })
228
+ const transaction = _altcoinjs.Transaction.fromHex(hex)
229
+
230
+ // Electrum only reports a transaction's height in the history of a
231
+ // script it pays. Servers refuse the history of busy scripts, so each
232
+ // paid script is tried in turn. When none answers the task fails, so the
233
+ // engine retries it instead of taking the transaction as unconfirmed:
234
+ let blockHeight = 0
235
+ let historyError
236
+ for (const { script } of transaction.outs) {
237
+ if (script[0] === _altcoinjs.opcodes.OP_RETURN) continue
238
+ try {
239
+ // Safe because yielding WsTask<AddressTransactionsResponse> should get
240
+ // AddressTransactionsResponse next.
241
+ const history = (yield {
242
+ ..._electrumApi.addressTransactionsMessage.call(void 0, toElectrumScriptHash(script))
243
+ })
244
+ blockHeight = _nullishCoalesce(_optionalChain([history, 'access', _ => _.find, 'call', _2 => _2(item => item.tx_hash === txid), 'optionalAccess', _3 => _3.height]), () => ( 0))
245
+ historyError = undefined
246
+ break
247
+ } catch (error) {
248
+ log.warn(`failed to fetch the height of transaction ${txid}:`, error)
249
+ historyError = error
250
+ }
251
+ }
252
+ if (historyError != null) throw historyError
253
+
254
+ // The engine reads a fetched transaction's hex, height, outpoints, and
255
+ // outputs. It keeps the date, fee, and input amounts it already stored,
256
+ // which Electrum could only supply with a request per previous transaction:
257
+ const isCoinbase = transaction.isCoinbase()
258
+ return {
259
+ txid,
260
+ hex,
261
+ // Like Blockbook, Electrum puts mempool transactions at height 0 or -1:
262
+ blockHeight,
263
+ confirmations: 0,
264
+ blockTime: 0,
265
+ fees: '0',
266
+ vin: transaction.ins.map((input, n) => ({
267
+ addresses: [],
268
+ n,
269
+ sequence: input.sequence,
270
+ txid: Buffer.from(input.hash).reverse().toString('hex'),
271
+ value: '0',
272
+ vout: input.index,
273
+ coinbase: isCoinbase ? input.script.toString('hex') : undefined
274
+ })),
275
+ vout: transaction.outs.map((output, n) => ({
276
+ n,
277
+ value: output.value.toString(),
278
+ addresses: [], // unused, the engine reads the script in `hex`
279
+ hex: output.script.toString('hex')
280
+ }))
281
+ }
282
+ }
283
+
284
+ let isWatchingBlocks = false
106
285
  const instance = _Blockbook.makeBlockbook.call(void 0, {
107
286
  asAddress,
108
- asResponse,
287
+ asResponse: makeAsResponse(() => isWatchingBlocks),
109
288
  connectionUri,
110
289
  engineEmitter,
111
290
  initOptions,
@@ -114,85 +293,47 @@ const asResponse = (raw) => {
114
293
  uri
115
294
  ) {
116
295
  const generator = taskGeneratorFn(uri)
117
- let nextValue
296
+ let result = await generator.next()
118
297
 
119
298
  while (true) {
120
- const result = await generator.next(nextValue)
121
- const task = result.value
122
-
123
299
  // If the generator result is not a WsTask we will return it:
124
- if (_optionalChain([result, 'optionalAccess', _ => _.done]) === true) return result.value
300
+ if (result.done === true) return result.value
301
+ const task = result.value
125
302
  if (typeof task === 'boolean') return task
126
303
 
127
- // Translate getAccountUtxo to blockchain.scripthash.listunspent:
128
- if (task.method === 'getAccountUtxo') {
129
- const params = task.params
130
- const address =
131
- asAddress != null ? asAddress(params.descriptor) : params.descriptor
132
- const scriptHash = addressToScriptHash(address)
133
-
134
- // Safe because yielding WsTask<ListUnspentResponse> should get
135
- // ListUnspentResponse next.
136
- const electrumUtxos = (yield {
137
- ..._electrumApi.listUnspentMessage.call(void 0, scriptHash)
138
- })
139
-
140
- const blockbookUtxos = electrumUtxos.map(
141
- utxo => ({
142
- txid: utxo.tx_hash,
143
- vout: utxo.tx_pos,
144
- value: utxo.value.toString(),
145
- height: utxo.height
146
- })
147
- )
148
- nextValue = blockbookUtxos
149
- continue
150
- }
151
-
152
- // Get Address Transactions:
153
- if (task.method === 'getAccountInfo') {
154
- const params = task.params
155
- const address =
156
- asAddress != null ? asAddress(params.descriptor) : params.descriptor
157
- const scriptHash = addressToScriptHash(address)
158
-
159
- // Safe because yielding WsTask<AddressTransactionsResponse> should get
160
- // AddressTransactionsResponse next.
161
- const electrumUtxos = (yield {
162
- ..._electrumApi.addressTransactionsMessage.call(void 0, scriptHash)
163
- })
164
-
165
- const blockbookUtxos = {
166
- address, // unused
167
- balance: '0', // unused
168
- totalReceived: '0', // unused
169
- totalSent: '0', // unused
170
- txs: electrumUtxos.length,
171
- unconfirmedBalance: '0', // unused
172
- unconfirmedTxs: electrumUtxos.reduce(
173
- (sum, tx) => (sum + tx.height >= 0 ? 1 : 0),
174
- 0
175
- ),
176
- txids: [], // unused
177
- transactions: [], // TODO: this requires an extra query per txid
178
- page: 0, // unused
179
- totalPages: 1,
180
- itemsOnPage: 0 // unused
181
- }
182
- nextValue = blockbookUtxos
304
+ let response
305
+ try {
306
+ response = yield* translateTask(task)
307
+ } catch (error) {
308
+ // Hand the failure to the task that made the request, as the socket
309
+ // does for the requests it sends unchanged:
310
+ result = await generator.throw(error)
183
311
  continue
184
312
  }
313
+ result = await generator.next(response)
185
314
  }
186
315
  },
187
316
  ping: async () => {
188
317
  return await instance.promisifyWsMessage(_electrumApi.pingMessage.call(void 0, ))
189
318
  },
319
+ subscribeNewBlockMessage: () => ({
320
+ ..._electrumApi.headersSubscribeMessage.call(void 0, ),
321
+ // Notifications hold the header itself, with no response envelope:
322
+ cleaner: (raw) => {
323
+ const header = _electrumApi.asHeadersSubscribeResponse.call(void 0, raw)
324
+ return { height: header.height, hash: headerToBlockHash(header.hex) }
325
+ }
326
+ }),
190
327
  socketEmitter,
191
328
  walletId
192
329
  })
193
330
 
194
- instance.broadcastTx = async () => {
195
- throw new Error('broadcastTx not supported for Electrum connections')
331
+ instance.broadcastTx = async transaction => {
332
+ const txid = await instance.promisifyWsMessage(
333
+ _electrumApi.broadcastTransactionMessage.call(void 0, transaction.signedTx)
334
+ )
335
+ // Blockbook reports the broadcast txid as `result`:
336
+ return { result: txid }
196
337
  }
197
338
  instance.fetchAddress = async () => {
198
339
  throw new Error('fetchAddress not supported for Electrum connections')
@@ -201,18 +342,43 @@ const asResponse = (raw) => {
201
342
  throw new Error('fetchAddressUtxos not supported for Electrum connections')
202
343
  }
203
344
  instance.fetchInfo = async () => {
204
- throw new Error('fetchInfo not supported for Electrum connections')
345
+ const header = await instance.promisifyWsMessage(_electrumApi.headersSubscribeMessage.call(void 0, ))
346
+ return {
347
+ name: pluginInfo.currencyInfo.displayName, // unused
348
+ shortcut: pluginInfo.currencyInfo.currencyCode, // unused
349
+ decimals: 8, // unused
350
+ version: '', // unused
351
+ bestHeight: header.height,
352
+ bestHash: headerToBlockHash(header.hex),
353
+ block0Hash: '', // unused
354
+ testnet: false, // unused
355
+ backend: undefined // unused
356
+ }
205
357
  }
206
358
  instance.fetchTransaction = async () => {
207
359
  throw new Error('fetchTransaction not supported for Electrum connections')
208
360
  }
209
361
 
210
- instance.watchAddresses = async () => {
211
- return // Fail gracefully
362
+ const { watchBlocks } = instance
363
+ instance.watchBlocks = deferredBlockSub => {
364
+ isWatchingBlocks = true
365
+ watchBlocks(deferredBlockSub)
212
366
  }
213
- instance.watchBlocks = async () => {
367
+ instance.watchAddresses = async () => {
214
368
  return // Fail gracefully
215
369
  }
216
370
 
217
371
  return instance
218
372
  } exports.makeBlockbookElectrum = makeBlockbookElectrum;
373
+
374
+ /** True for the URIs of Electrum servers, as opposed to Blockbook servers. */
375
+ const isElectrumUri = (uri) =>
376
+ /^electrumwss?:/.test(uri); exports.isElectrumUri = isElectrumUri
377
+
378
+ // Electrum indexes scripts by their SHA-256, in reverse byte order:
379
+ const toElectrumScriptHash = (scriptPubkey) =>
380
+ _altcoinjs.crypto.sha256(scriptPubkey).reverse().toString('hex')
381
+
382
+ // A block's hash is its header's double SHA-256, in reverse byte order:
383
+ const headerToBlockHash = (headerHex) =>
384
+ _altcoinjs.crypto.hash256(Buffer.from(headerHex, 'hex')).reverse().toString('hex')
@@ -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
 
@@ -261,10 +267,27 @@ const asResponseDefault = (raw) => [
261
267
  }
262
268
  }
263
269
 
270
+ // A task resumes with each response or failure, and then makes its next
271
+ // request or reports with a boolean that it is done:
272
+ const submitNextTask = (
273
+ generator,
274
+ resumed
275
+ ) => {
276
+ resumed
277
+ .then(result => {
278
+ const task = result.value
279
+ if (typeof task === 'boolean') return
280
+ instance.submitTask(task, generator)
281
+ })
282
+ .catch(e => {
283
+ log.error(e.message)
284
+ })
285
+ }
286
+
264
287
  const onTimer = () => {
265
288
  log(`socket timer with server ${uri} expired, check if healthCheck needed`)
266
289
  const now = Date.now() - TIMER_SLACK
267
- if (lastKeepAlive + KEEP_ALIVE_MS < now) {
290
+ if (lastKeepAlive + keepAliveMs < now) {
268
291
  log(`submitting healthCheck to server ${uri}`)
269
292
  lastKeepAlive = now
270
293
  config
@@ -277,11 +300,10 @@ const asResponseDefault = (raw) => [
277
300
 
278
301
  for (const [id, request] of Object.entries(pendingRequests)) {
279
302
  if (request.startTime + timeout < now) {
280
- request.generator
281
- .throw(new Error(`Timeout for request ${id}`))
282
- .catch(e => {
283
- log.error(e.message)
284
- })
303
+ submitNextTask(
304
+ request.generator,
305
+ request.generator.throw(new Error(`Timeout for request ${id}`))
306
+ )
285
307
  _utils.removeItem.call(void 0, pendingRequests, id)
286
308
  }
287
309
  }
@@ -291,7 +313,7 @@ const asResponseDefault = (raw) => [
291
313
 
292
314
  const setupTimer = () => {
293
315
  log(`setupTimer with server ${uri}`)
294
- let nextWakeUp = lastWakeUp + WAKE_UP_MS
316
+ let nextWakeUp = lastWakeUp + wakeUpMs
295
317
  for (const request of Object.values(pendingRequests)) {
296
318
  const to = request.startTime + timeout
297
319
  if (to < nextWakeUp) nextWakeUp = to
@@ -352,23 +374,13 @@ const asResponseDefault = (raw) => [
352
374
  } else {
353
375
  nextValue = responseMessage.data
354
376
  }
355
- request.generator
356
- .next(nextValue)
357
- .then(result => {
358
- const task = result.value
359
- if (typeof task === 'boolean') {
360
- return
361
- }
362
- instance.submitTask(task, request.generator)
363
- })
364
- .catch(e => {
365
- log.error(e.message)
366
- })
377
+ submitNextTask(
378
+ request.generator,
379
+ request.generator.next(nextValue)
380
+ )
367
381
  } catch (error) {
368
382
  console.log({ uri, error, response: responseMessage, request })
369
- request.generator.throw(error).catch(e => {
370
- log.error(e.message)
371
- })
383
+ submitNextTask(request.generator, request.generator.throw(error))
372
384
  }
373
385
  continue
374
386
  }
@@ -144,13 +144,32 @@ var _cleaners = require('cleaners');
144
144
  const pingMessage = () => {
145
145
  return {
146
146
  method: 'server.ping',
147
- params: undefined,
147
+ // Electrum servers such as Fulcrum reject the `{}` the socket sends for
148
+ // missing params as named params, so an empty array is required:
149
+ params: [],
148
150
  cleaner: exports.asElectrumResponse.call(void 0, exports.asPingResponse)
149
151
  }
150
152
  }; exports.pingMessage = pingMessage
151
153
 
152
154
  const asPingResponse = _cleaners.asMaybe.call(void 0, _cleaners.asUndefined); exports.asPingResponse = asPingResponse
153
155
 
156
+ /**
157
+ * Subscribe to Headers (Get Block Height)
158
+ */
159
+ const headersSubscribeMessage = () => ({
160
+ method: 'blockchain.headers.subscribe',
161
+ params: [],
162
+ cleaner: exports.asElectrumResponse.call(void 0, exports.asHeadersSubscribeResponse)
163
+ }); exports.headersSubscribeMessage = headersSubscribeMessage
164
+
165
+
166
+
167
+ // The chain tip's header, which the server also sends for each new tip:
168
+ const asHeadersSubscribeResponse = _cleaners.asObject.call(void 0, {
169
+ height: _cleaners.asNumber,
170
+ hex: _cleaners.asString
171
+ }); exports.asHeadersSubscribeResponse = asHeadersSubscribeResponse
172
+
154
173
  /**
155
174
  * List Unspent (Get UTXOs)
156
175
  */
@@ -201,3 +220,32 @@ var _cleaners = require('cleaners');
201
220
  }); exports.transactionMessage = transactionMessage
202
221
 
203
222
  const asTransactionResponse = exports.asElectrumTransaction; exports.asTransactionResponse = asTransactionResponse
223
+
224
+ /**
225
+ * Get Raw Transaction
226
+ */
227
+ const rawTransactionMessage = (
228
+ txHash
229
+ ) => ({
230
+ method: 'blockchain.transaction.get',
231
+ params: [txHash, false],
232
+ cleaner: exports.asElectrumResponse.call(void 0, exports.asRawTransactionResponse)
233
+ }); exports.rawTransactionMessage = rawTransactionMessage
234
+
235
+ const asRawTransactionResponse = _cleaners.asString; exports.asRawTransactionResponse = asRawTransactionResponse
236
+
237
+ /**
238
+ * Broadcast Transaction
239
+ */
240
+ const broadcastTransactionMessage = (
241
+ rawTx
242
+ ) => ({
243
+ method: 'blockchain.transaction.broadcast',
244
+ params: [rawTx],
245
+ cleaner: exports.asElectrumResponse.call(void 0, exports.asBroadcastTransactionResponse)
246
+ }); exports.broadcastTransactionMessage = broadcastTransactionMessage
247
+
248
+
249
+
250
+ // The server answers with the transaction's txid:
251
+ const asBroadcastTransactionResponse = _cleaners.asString; exports.asBroadcastTransactionResponse = asBroadcastTransactionResponse
@@ -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>;
@@ -161,6 +161,16 @@ export interface CoinInfo {
161
161
  * build-in replay-protection mechanisms.
162
162
  */
163
163
  sighash?: number;
164
+ /**
165
+ * Optional nLockTime value set on every transaction. This is used by forks
166
+ * whose nodes treat a magic nLockTime as final while the parent chain reads
167
+ * it as an unreachable block height, making the fork's transactions
168
+ * impossible to replay on the parent chain.
169
+ *
170
+ * Transactions for these currencies always have a non-final input sequence,
171
+ * since nodes ignore nLockTime when every input sequence is final.
172
+ */
173
+ lockTime?: number;
164
174
  /**
165
175
  * A function to be passed to AltcoinJS `signInput` method. This is used to
166
176
  * get the input hash for the signature algorithm before signing the input.