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.
- package/android/src/main/assets/edge-currency-plugins/edge-currency-plugins.js +51 -49
- package/lib/common/plugin/EngineEmitter.js +10 -0
- package/lib/common/plugin/PluginState.js +87 -1
- package/lib/common/plugin/types.js +11 -0
- package/lib/common/utxobased/engine/ServerStates.js +257 -9
- package/lib/common/utxobased/engine/UtxoEngineProcessor.js +147 -22
- package/lib/common/utxobased/info/all.js +6 -0
- package/lib/common/utxobased/info/bitcoin.js +1 -1
- package/lib/common/utxobased/info/ecashbitcoin.js +90 -0
- package/lib/common/utxobased/info/ecashbitcoinbetanet.js +91 -0
- package/lib/common/utxobased/keymanager/keymanager.js +6 -1
- package/lib/common/utxobased/network/Blockbook.js +9 -2
- package/lib/common/utxobased/network/BlockbookElectrum.js +243 -77
- package/lib/common/utxobased/network/Socket.js +34 -22
- package/lib/common/utxobased/network/electrumApi.js +49 -1
- package/lib/src/common/plugin/EngineEmitter.d.ts +7 -2
- package/lib/src/common/plugin/PluginState.d.ts +30 -0
- package/lib/src/common/plugin/types.d.ts +10 -0
- package/lib/src/common/utxobased/engine/ServerStates.d.ts +32 -0
- package/lib/src/common/utxobased/engine/UtxoEngineProcessor.d.ts +5 -1
- package/lib/src/common/utxobased/info/all.d.ts +2 -0
- package/lib/src/common/utxobased/info/ecashbitcoin.d.ts +3 -0
- package/lib/src/common/utxobased/info/ecashbitcoinbetanet.d.ts +3 -0
- package/lib/src/common/utxobased/network/Blockbook.d.ts +6 -1
- package/lib/src/common/utxobased/network/BlockbookElectrum.d.ts +2 -0
- package/lib/src/common/utxobased/network/Socket.d.ts +4 -0
- package/lib/src/common/utxobased/network/electrumApi.d.ts +21 -0
- package/lib/test/common/plugin/PluginState.spec.d.ts +1 -0
- package/lib/test/common/utxobased/engine/AddressRepair.spec.d.ts +1 -0
- package/lib/test/common/utxobased/engine/UtxoEngineProcessor.spec.d.ts +1 -0
- package/lib/test/common/utxobased/keymanager/coins/ecashbitcointransactiontest.spec.d.ts +1 -0
- package/lib/test/common/utxobased/network/BlockbookElectrum.spec.d.ts +1 -0
- 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
|
-
|
|
53
|
-
|
|
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
|
-
)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
300
|
+
if (result.done === true) return result.value
|
|
301
|
+
const task = result.value
|
|
125
302
|
if (typeof task === 'boolean') return task
|
|
126
303
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
362
|
+
const { watchBlocks } = instance
|
|
363
|
+
instance.watchBlocks = deferredBlockSub => {
|
|
364
|
+
isWatchingBlocks = true
|
|
365
|
+
watchBlocks(deferredBlockSub)
|
|
212
366
|
}
|
|
213
|
-
instance.
|
|
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 +
|
|
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
|
-
|
|
281
|
-
.
|
|
282
|
-
.
|
|
283
|
-
|
|
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 +
|
|
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
|
-
|
|
356
|
-
.
|
|
357
|
-
.
|
|
358
|
-
|
|
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)
|
|
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
|
-
|
|
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.
|