UNPKG

52.3 kBJavaScriptView Raw
1const crypto = require('crypto')
2const http = require('http')
3const https = require('https')
4const path = require('path')
5const querystring = require('querystring')
6const url = require('url')
7const braveCrypto = require('brave-crypto')
8const passphraseUtil = braveCrypto.passphrase
9
10const anonize = require('node-anonize2-relic-emscripten')
11const backoff = require('@ambassify/backoff-strategies')
12const balance = require('bat-balance')
13const Joi = require('joi')
14const random = require('random-lib')
15const { sign } = require('http-request-signature')
16const stringify = require('json-stable-stringify')
17const underscore = require('underscore')
18const uuid = require('uuid')
19
20const batPublisher = require('bat-publisher')
21
22const SEED_LENGTH = 32
23const HKDF_SALT = new Uint8Array([ 126, 244, 99, 158, 51, 68, 253, 80, 133, 183, 51, 180, 77, 62, 74, 252, 62, 106, 96, 125, 241, 110, 134, 87, 190, 208, 158, 84, 125, 69, 246, 207, 162, 247, 107, 172, 37, 34, 53, 246, 105, 20, 215, 5, 248, 154, 179, 191, 46, 17, 6, 72, 210, 91, 10, 169, 145, 248, 22, 147, 117, 24, 105, 12 ])
24const LEDGER_SERVERS = {
25 'staging': {
26 v1: 'https://ledger-staging.brave.com',
27 v2: 'https://ledger-staging.mercury.basicattentiontoken.org'
28 },
29 'production': {
30 v1: 'https://ledger.brave.com',
31 v2: 'https://ledger.mercury.basicattentiontoken.org'
32 }
33}
34
35const Client = function (personaId, options, state) {
36 if (!(this instanceof Client)) return new Client(personaId, options, state)
37
38 const self = this
39
40 const now = underscore.now()
41 const later = now + (15 * msecs.minute)
42
43 self.options = underscore.defaults(underscore.clone(options || {}),
44 { version: 'v1', debugP: false, loggingP: false, verboseP: false })
45
46 const env = self.options.environment || 'production'
47 const version = self.options.version || 'v2'
48 underscore.defaults(self.options,
49 { server: LEDGER_SERVERS[env][version],
50 prefix: '/' + self.options.version
51 })
52 underscore.keys(self.options).forEach(function (option) {
53 if ((option.lastIndexOf('P') + 1) === option.length) self.options[option] = Client.prototype.boolion(self.options[option])
54 })
55 if (typeof self.options.server === 'string') {
56 self.options.server = url.parse(self.options.server)
57 if (!self.options.server) throw new Error('invalid options.server')
58 }
59
60 if (typeof self.options.roundtrip !== 'undefined') {
61 if (typeof self.options.roundtrip !== 'function') throw new Error('invalid roundtrip option (must be a function)')
62
63 self._innerTrip = self.options.roundtrip.bind(self)
64 self.roundtrip = function (params, callback) { self._innerTrip(params, self.options, callback) }
65 } else if (self.options.debugP) self.roundtrip = self._roundTrip
66 else throw new Error('security audit requires options.roundtrip for non-debug use')
67 self._retryTrip = self._retryTrip.bind(self)
68 if (self.options.debugP) console.log(JSON.stringify(self.options, null, 2))
69
70 // Workaround #20
71 if (state && state.properties && state.properties.wallet && state.properties.wallet.keyinfo) {
72 let seed = state.properties.wallet.keyinfo.seed
73 if (!(seed instanceof Uint8Array)) {
74 seed = new Uint8Array(Object.values(seed))
75 }
76
77 state.properties.wallet.keyinfo.seed = seed
78 }
79 self.state = underscore.defaults(state || {}, { personaId: personaId, options: self.options, ballots: [], transactions: [] })
80 self.logging = []
81
82 if (self.options.rulesTestP) {
83 self.state.updatesStamp = now - 1
84 if (self.options.verboseP) self.state.updatesDate = new Date(self.state.updatesStamp)
85 }
86 if ((self.state.updatesStamp) && (self.state.updatesStamp > later)) {
87 self.state.updatesStamp = later
88 if (self.options.verboseP) self.state.updatesDate = new Date(self.state.updatesStamp)
89 }
90
91 if (self.state.wallet) throw new Error('deprecated state (alpha) format')
92
93 this.seqno = 0
94 this.callbacks = {}
95 if (!self.options.createWorker) self.initializeHelper()
96}
97
98const msecs = {
99 day: 24 * 60 * 60 * 1000,
100 hour: 60 * 60 * 1000,
101 minute: 60 * 1000,
102 second: 1000
103}
104
105Client.prototype.sync = function (callback) {
106 const self = this
107
108 const now = underscore.now()
109 let ballot, ballots, i, memo, transaction, updateP
110
111 if (typeof callback !== 'function') throw new Error('sync missing callback parameter')
112
113 if (!self.state.properties) self.state.properties = {}
114 if ((self.state.reconcileStamp === null) || (isNaN(self.state.reconcileStamp))) {
115 console.log('day', 14 * msecs.day)
116 memo = { prevoiusStamp: self.state.reconcileStamp }
117 self.state.reconcileStamp = now + (14 * msecs.day)
118 memo.reconcileStamp = self.state.reconcileStamp
119 memo.reconcileDate = new Date(self.state.reconcileStamp)
120 self.memo('sync', memo)
121
122 self._log('sync', { reconcileStamp: self.state.reconcileStamp })
123 self.setTimeUntilReconcile(self.state.reconcileStamp)
124 }
125 // the caller is responsible for checking that the reconcileStamp is too historic...
126 if ((self.state.properties.days) && (self.state.reconcileStamp > (now + (self.state.properties.days * msecs.day)))) {
127 self._log('sync', { reconcileStamp: self.state.reconcileStamp })
128 return self.setTimeUntilReconcile(null, callback)
129 }
130
131// begin: legacy updates...
132 if (self.state.ruleset) {
133 self.state.ruleset.forEach(function (rule) {
134 if (rule.consequent) return
135
136 self.state.updatesStamp = now - 1
137 if (self.options.verboseP) self.state.updatesDate = new Date(self.state.updatesStamp)
138 })
139 delete self.state.ruleset
140 }
141 if (!self.state.ruleset) {
142 self.state.ruleset = [
143 { condition: 'SLD === \'youtube.com\' && pathname.indexOf(\'/channel/\') === 0',
144 consequent: '\'youtube#channel:\' + pathname.split(\'/\')[2]',
145 description: 'youtube channels'
146 },
147 {
148 condition: '/^[a-z][a-z].gov$/.test(SLD)',
149 consequent: 'QLD + \'.\' + SLD',
150 description: 'governmental sites'
151 },
152 {
153 condition: "TLD === 'gov' || /^go.[a-z][a-z]$/.test(TLD) || /^gov.[a-z][a-z]$/.test(TLD)",
154 consequent: 'SLD',
155 description: 'governmental sites'
156 },
157 {
158 condition: "SLD === 'keybase.pub'",
159 consequent: 'QLD + \'.\' + SLD',
160 description: 'keybase users'
161 },
162 {
163 condition: true,
164 consequent: 'SLD',
165 description: 'the default rule'
166 }
167 ]
168 }
169 if (self.state.verifiedPublishers) {
170 delete self.state.verifiedPublishers
171 self.state.updatesStamp = now - 1
172 if (self.options.verboseP) self.state.updatesDate = new Date(self.state.updatesStamp)
173 }
174// end: legacy updates...
175
176 if ((self.credentials) && (!self.state.rulesV2Stamp)) {
177 try {
178 const bootstrap = require(path.join(__dirname, 'bootstrap'))
179
180 underscore.extend(self.state, bootstrap)
181 return callback(null, self.state, msecs.minute)
182 } catch (ex) {}
183 }
184
185 if (self.state.updatesStamp < now) {
186 return self._updateRules(function (err) {
187 if (err) self._log('_updateRules', { message: err.toString() })
188
189 self._log('sync', { delayTime: msecs.minute })
190 callback(null, self.state, msecs.minute)
191 })
192 }
193
194 if (!self.credentials) self.credentials = {}
195
196 if (!self.state.persona) return self._registerPersona(callback)
197 self.credentials.persona = new anonize.Credential(self.state.persona)
198
199 if (self.options.verboseP) console.log('+++ busyP=' + self.busyP())
200
201 ballots = underscore.shuffle(self.state.ballots)
202 for (i = ballots.length - 1; i >= 0; i--) {
203 ballot = ballots[i]
204 transaction = underscore.find(self.state.transactions, function (transaction) {
205 return ((transaction.credential) &&
206 (ballot.viewingId === transaction.viewingId) &&
207 ((!ballot.prepareBallot) || (!ballot.delayStamp) || (ballot.delayStamp <= now)))
208 })
209 if (!transaction) continue
210
211 if (!ballot.prepareBallot) return self._prepareBallot(ballot, transaction, callback)
212 return self._commitBallot(ballot, transaction, callback)
213 }
214
215 transaction = underscore.find(self.state.transactions, function (transaction) {
216 if ((transaction.credential) || (transaction.ballots)) return
217
218 try { return self._registerViewing(transaction.viewingId, callback) } catch (ex) {
219 self._log('_registerViewing', { errP: 1, message: ex.toString(), stack: ex.stack })
220 }
221 })
222
223 if (self.state.currentReconcile) return self._currentReconcile(callback)
224
225 for (i = self.state.transactions.length - 1; i > 0; i--) {
226 transaction = self.state.transactions[i]
227 ballot = underscore.find(self.state.ballots, function (ballot) { return (ballot.viewingId === transaction.viewingId) })
228
229 if ((transaction.count === transaction.votes) && (!!transaction.credential) && (!ballot)) {
230 self.state.transactions[i] = underscore.omit(transaction, [ 'credential', 'surveyorIds', 'err' ])
231 updateP = true
232 }
233 }
234 if (updateP) {
235 self._log('sync', { delayTime: msecs.minute })
236 return callback(null, self.state, msecs.minute)
237 }
238
239 self._log('sync', { result: true })
240 return true
241}
242
243const propertyList = [ 'setting', 'days', 'fee' ]
244
245Client.prototype.getBraveryProperties = function () {
246 const errP = !this.state.properties
247
248 this._log('getBraveryProperties', { errP: errP, result: underscore.pick(this.state.properties || {}, propertyList) })
249 if (errP) throw new Error('Ledger client initialization incomplete.')
250
251 return underscore.pick(this.state.properties, propertyList)
252}
253
254Client.prototype.setBraveryProperties = function (properties, callback) {
255 const self = this
256
257 if (typeof callback !== 'function') throw new Error('setBraveryProperties missing callback parameter')
258
259 properties = underscore.pick(properties, propertyList)
260 self._log('setBraveryProperties', properties)
261
262 underscore.defaults(self.state.properties, properties)
263 callback(null, self.state)
264}
265
266Client.prototype.getPaymentId = function () {
267 const paymentId = this.state.properties && this.state.properties.wallet && this.state.properties.wallet.paymentId
268
269 this._log('getPaymentId')
270
271 return paymentId
272}
273
274Client.prototype.getWalletAddress = function () {
275 const wallet = this.state.properties && this.state.properties.wallet
276
277 this._log('getWalletAddress')
278
279 if (!wallet) return
280
281 if ((wallet.addresses) && (wallet.addresses.BAT)) return wallet.addresses.BAT
282
283 return wallet.address
284}
285
286Client.prototype.getWalletAddresses = function () {
287 const wallet = this.state.properties && this.state.properties.wallet
288 let addresses
289
290 this._log('getWalletAddresses')
291
292 if (!wallet) return
293
294 addresses = underscore.extend({}, wallet.addresses)
295 if (wallet.address) addresses.BTC = wallet.address
296 return addresses
297}
298
299Client.prototype.getWalletProperties = function (amount, currency, callback) {
300 const self = this
301
302 const prefix = self.options.prefix + '/wallet/'
303 let params, errP, path, suffix
304
305 if (typeof amount === 'function') {
306 callback = amount
307 amount = null
308 currency = null
309 } else if (typeof currency === 'function') {
310 callback = currency
311 currency = null
312 }
313
314 if (typeof callback !== 'function') throw new Error('getWalletProperties missing callback parameter')
315
316 errP = (!self.state.properties) || (!self.state.properties.wallet)
317 self._log('getWalletProperties', { errP: errP })
318 if (errP) throw new Error('Ledger client initialization incomplete.')
319
320 if ((!self.state.currentReconcile) && (self.state.reconcileStamp) && (self.state.reconcileStamp > underscore.now())) {
321 params = underscore.pick(self.state.properties.wallet, ['addresses', 'paymentId'])
322 }
323 if (params) {
324 balance.getProperties(params, self.options, (err, provider, result) => {
325 self._log('getWalletProperties', { method: 'GET', path: 'getProperties', errP: !!err })
326 if (err) return callback(err)
327
328 if (!result.addresses) result.addresses = underscore.clone(self.state.properties.wallet.addresses)
329 callback(null, result)
330 })
331 return
332 }
333
334 suffix = '?balance=true&' + self._getWalletParams({ amount: amount, currency: currency })
335 path = prefix + self.state.properties.wallet.paymentId + suffix
336 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
337 self._log('getWalletProperties', { method: 'GET', path: prefix + '...' + suffix, errP: !!err })
338 if (err) return callback(err)
339
340 callback(null, body)
341 })
342}
343
344Client.prototype.setTimeUntilReconcile = function (timestamp, callback) {
345 const now = underscore.now()
346
347 if ((!timestamp) || (timestamp < now)) {
348 let days = 30
349 if (this.state && this.state.properties && this.state.properties.days) {
350 days = this.state.properties.days
351 }
352 timestamp = now + (days * msecs.day)
353 }
354 this.state.reconcileStamp = timestamp
355 if (this.options.verboseP) this.state.reconcileDate = new Date(this.state.reconcileStamp)
356
357 if (callback) {
358 callback(null, this.state)
359 }
360}
361
362Client.prototype.timeUntilReconcile = function (synopsis) {
363 if (!this.state.reconcileStamp) {
364 this._log('isReadyToReconcile', { errP: true })
365 throw new Error('Ledger client initialization incomplete.')
366 }
367
368 if (this.state.currentReconcile) {
369 this._log('isReadyToReconcile', { reason: 'already reconciling', reconcileStamp: this.state.reconcileStamp })
370 return false
371 }
372
373 this._fuzzing(synopsis)
374 return (this.state.reconcileStamp - underscore.now())
375}
376
377Client.prototype.isReadyToReconcile = function (synopsis) {
378 const delayTime = this.timeUntilReconcile()
379
380 this._log('isReadyToReconcile', { delayTime: delayTime })
381 this._fuzzing(synopsis)
382 return ((typeof delayTime === 'boolean') ? delayTime : (delayTime <= 0))
383}
384
385Client.prototype.reconcile = function (viewingId, callback) {
386 const self = this
387
388 const prefix = self.options.prefix + '/surveyor/contribution/current/'
389 let delayTime, path, schema, validity
390
391 if (!callback) {
392 callback = viewingId
393 viewingId = null
394 }
395 if (typeof callback !== 'function') throw new Error('reconcile missing callback parameter')
396
397 try {
398 if (!self.state.reconcileStamp) throw new Error('Ledger client initialization incomplete.')
399 if (self.state.properties.setting === 'adFree') {
400 if (!viewingId) throw new Error('missing viewingId parameter')
401
402 schema = Joi.string().guid().required().description('opaque identifier for viewing submissions')
403
404 validity = Joi.validate(viewingId, schema)
405 if (validity.error) throw new Error(validity.error)
406 }
407 } catch (ex) {
408 this._log('reconcile', { errP: true })
409 throw ex
410 }
411
412 delayTime = this.state.reconcileStamp - underscore.now()
413 if (delayTime > 0) {
414 this._log('reconcile', { reason: 'not time to reconcile', delayTime: delayTime })
415 return callback(null, null, delayTime)
416 }
417 if (this.state.currentReconcile) {
418 delayTime = random.randomInt({ min: msecs.second, max: (this.options.debugP ? 1 : 10) * msecs.minute })
419 this._log('reconcile', { reason: 'already reconciling', delayTime: delayTime, reconcileStamp: this.state.reconcileStamp })
420 return callback(null, null, delayTime)
421 }
422
423 this._log('reconcile', { setting: self.state.properties.setting })
424 if (self.state.properties.setting !== 'adFree') {
425 throw new Error('setting not (yet) supported: ' + self.state.properties.setting)
426 }
427
428 path = prefix + self.credentials.persona.parameters.userId
429 self._retryTrip(self, { path: path, method: 'GET', useProxy: true }, function (err, response, body) {
430 const surveyorInfo = body
431 let i
432
433 self._log('reconcile', { method: 'GET', path: prefix + '...', errP: !!err })
434 if (err) return callback(err)
435
436 for (i = self.state.transactions.length - 1; i >= 0; i--) {
437 if (self.state.transactions[i].surveyorId !== surveyorInfo.surveyorId) continue
438
439 delayTime = random.randomInt({ min: msecs.second, max: (self.options.debugP ? 1 : 10) * msecs.minute })
440 self._log('reconcile',
441 { reason: 'awaiting a new surveyorId', delayTime: delayTime, surveyorId: surveyorInfo.surveyorId })
442 return callback(null, null, delayTime)
443 }
444
445 self.state.currentReconcile = { viewingId: viewingId, surveyorInfo: surveyorInfo, timestamp: 0 }
446 self._log('reconcile', { delayTime: msecs.minute })
447 callback(null, self.state, msecs.minute)
448 })
449}
450
451Client.prototype.ballots = function (viewingId) {
452 let i, count, transaction
453
454 count = 0
455 for (i = this.state.transactions.length - 1; i >= 0; i--) {
456 transaction = this.state.transactions[i]
457 if ((transaction.votes < transaction.count) && ((transaction.viewingId === viewingId) || (!viewingId))) {
458 count += transaction.count - transaction.votes
459 }
460 }
461 return count
462}
463
464Client.prototype.vote = function (publisher, viewingId) {
465 let i, transaction
466
467 if (!publisher) throw new Error('missing publisher parameter')
468
469 for (i = this.state.transactions.length - 1; i >= 0; i--) {
470 transaction = this.state.transactions[i]
471 if (transaction.votes >= transaction.count) continue
472
473 if ((transaction.viewingId === viewingId) || (!viewingId)) break
474 }
475 if (i < 0) return
476
477 this.state.ballots.push({ viewingId: transaction.viewingId,
478 surveyorId: transaction.surveyorIds[transaction.votes],
479 publisher: publisher,
480 offset: transaction.votes
481 })
482 transaction.votes++
483
484 return this.state
485}
486
487Client.prototype.report = function () {
488 const entries = this.logging
489
490 this.logging = []
491 if (entries.length) return entries
492}
493
494Client.prototype.generateKeypair = function () {
495 const wallet = this.state.properties && this.state.properties.wallet
496
497 if (!wallet) {
498 if (!this.state.properties) this.state.properties = {}
499
500 this.state.properties.wallet = { keyinfo: { seed: braveCrypto.getSeed(SEED_LENGTH) } }
501 } else if (!wallet.keyinfo) {
502 throw new Error('invalid wallet')
503 }
504 return this.getKeypair()
505}
506
507Client.prototype.getKeypair = function () {
508 if (this.state && this.state.properties && this.state.properties.wallet &&
509 this.state.properties.wallet.keyinfo && this.state.properties.wallet.keyinfo.seed) {
510 return braveCrypto.deriveSigningKeysFromSeed(this.state.properties.wallet.keyinfo.seed, HKDF_SALT)
511 }
512 throw new Error('invalid or uninitialized wallet')
513}
514
515Client.prototype.getWalletPassphrase = function (state, options) {
516 if (!state) state = this.state
517
518 const wallet = state.properties && state.properties.wallet
519
520 this._log('getWalletPassphrase')
521
522 if (!wallet) return
523
524 if ((wallet.keyinfo) && (wallet.keyinfo.seed)) {
525 const seed = Buffer.from(wallet.keyinfo.seed)
526 const passPhrase = passphraseUtil.fromBytesOrHex(seed, options && options.useNiceware)
527
528 return passPhrase && passPhrase.split(' ')
529 }
530}
531
532Client.prototype.recoverKeypair = function (passPhrase) {
533 var seed
534 this._log('recoverKeypair')
535 try {
536 seed = Buffer.from(passphraseUtil.toBytes32(passPhrase))
537 } catch (ex) {
538 throw new Error('invalid passphrase:' + ex.toString())
539 }
540
541 if (seed && seed.length === SEED_LENGTH) {
542 if (!this.state.properties) this.state.properties = {}
543
544 this.state.properties.wallet = { keyinfo: { seed: seed } }
545 } else {
546 throw new Error('internal error, seed returned is invalid')
547 }
548 return this.getKeypair()
549}
550
551Client.prototype.isValidPassPhrase = function (passPhrase) {
552 if (!passPhrase || typeof passPhrase !== 'string') {
553 return false
554 }
555
556 try {
557 passphraseUtil.toBytes32(passPhrase)
558 } catch (ex) {
559 this.memo('isValidPassPhrase', ex.toString())
560 return false
561 }
562
563 return true
564}
565
566Client.prototype.recoverWallet = function (recoveryId, passPhrase, callback) {
567 const self = this
568 let path, keypair
569
570 try {
571 keypair = this.recoverKeypair(passPhrase)
572 } catch (ex) {
573 return callback(ex)
574 }
575
576 path = '/v2/wallet?publicKey=' + braveCrypto.uint8ToHex(keypair.publicKey)
577 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
578 if (err) return callback(err)
579
580 self._log('recoverWallet', body)
581
582 if (!body.paymentId) return callback(new Error('invalid response'))
583
584 recoveryId = body.paymentId
585
586 path = '/v2/wallet/' + recoveryId
587 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
588 self._log('recoverWallet', { method: 'GET', path: '/v2/wallet/...', errP: !!err })
589 if (err) return callback(err)
590
591 self._log('recoverWallet', body)
592
593 if (!body.addresses) return callback(new Error('invalid response'))
594
595 // yuck
596 const walletInfo = (self.state.properties && self.state.properties.wallet) || { }
597
598 self.state.properties.wallet = underscore.extend(walletInfo, { paymentId: recoveryId }, underscore.pick(body, [ 'addresses', 'altcurrency' ]))
599
600 return callback(null, self.state, underscore.omit(body, [ 'addresses', 'altcurrency' ]))
601 })
602 })
603}
604
605Client.prototype.busyP = function () {
606 const self = this
607
608 const then = new Date().getTime() - (15 * msecs.day)
609 let busyP = false
610
611 self.state.ballots.forEach((ballot) => {
612 const transaction = underscore.find(self.state.transactions, (transaction) => {
613 return (transaction.viewingId === ballot.viewingId)
614 })
615
616 if ((!transaction) || (!transaction.submissionStamp) || (!transaction.submissionStamp > then)) return
617
618 busyP = true
619 self._log('busyP', underscore.extend({ submissionStamp: transaction.submissionStamp }, ballot))
620 })
621
622 return busyP
623}
624
625Client.prototype.transitioned = function (oldState) {
626 return underscore.defaults(this.state, oldState)
627}
628
629Client.prototype.publisherTimestamp = function (callback) {
630 const self = this
631
632 let path
633
634 if (self.options.version === 'v1') return
635
636 path = '/v3/publisher/timestamp'
637 self._retryTrip(self, { path: path, method: 'GET', useProxy: true }, function (err, response, body) {
638 self._log('publisherInfo', { method: 'GET', path: path, errP: !!err })
639 if (err) return callback(err)
640
641 callback(null, body)
642 })
643}
644
645Client.prototype.publisherInfo = function (publisher, callback) {
646 const self = this
647
648 let path
649
650 if (self.options.version === 'v1') return
651
652 path = '/v3/publisher/identity?' + querystring.stringify({ publisher: publisher })
653 self._retryTrip(self, { path: path, method: 'GET', useProxy: true }, function (err, response, body) {
654 self._log('publisherInfo', { method: 'GET', path: path, errP: !!err })
655 if (err) return callback(err, null, response)
656
657 callback(null, body)
658 })
659}
660
661// batched interface... now a single callback invocation
662
663Client.prototype.publishersInfo = function (publishers, callback) {
664 if (this.options.version === 'v1') return
665
666 // initial version is still serialized, future versions will use a new API call.
667
668 if (!Array.isArray(publishers)) publishers = [ publishers ]
669 if (publishers.length === 0) return
670
671 if (typeof this.batches === 'undefined') this.batches = 0
672 if (!this.publishers) this.publishers = { requests: [], results: [], uniques: [] }
673 publishers.forEach((publisher) => {
674 if (this.publishers.uniques.indexOf(publisher) !== -1) return
675
676 this.publishers.requests.push(publisher)
677 this.publishers.uniques.push(publisher)
678 })
679 this._publishersInfo(callback)
680}
681
682Client.prototype.memo = function (who, args) {
683 let what
684
685 if (!this.state.memos) this.state.memos = []
686 if (this.state.memos.length > 10) this.state.memos.splice(0, this.state.memos.length - 10)
687 if (typeof args !== 'object') {
688 what = {reason: args}
689 } else {
690 what = args
691 }
692
693 this.state.memos.push(JSON.stringify({ who: who, what: what || {}, when: underscore.now() }))
694 this._log(who, args)
695}
696
697Client.prototype._publishersInfo = function (callback) {
698 const self = this
699
700 const publisher = underscore.first(self.publishers.requests)
701 let results
702
703 if (self.batches > 3) return
704
705 if (!publisher) {
706 if (self.batches > 0) return
707
708 results = self.publishers.results
709 self.publishers.results = []
710 self.publishers.uniques = []
711 if (results.length) callback(null, results)
712 return
713 }
714
715 self.publishers.requests = underscore.rest(self.publishers.requests)
716
717 self.batches++
718 self.publisherInfo(publisher, (err, result, response) => {
719 if ((err) && (response) && (response.statusCode === 429)) {
720 return setTimeout(() => {
721 self.batches--
722 self.publishersInfo(publisher, callback)
723 }, random.randomInt({ min: 1 * msecs.minute, max: 2 * msecs.minute }))
724 }
725
726 self.batches--
727 self.publishers.results.push(((!err) && (result) ? result : { publisher: publisher, err: err }))
728
729 self._publishersInfo.bind(self)(callback)
730 })
731
732 setTimeout(() => self._publishersInfo.bind(self)(callback), random.randomInt({ min: 250, max: 500 }))
733}
734
735Client.prototype.getPromotion = function (lang, forPaymentId, callback) {
736 const self = this
737
738 let path, params, paymentId
739 params = {}
740
741 if (self.options.version === 'v1') return
742
743 if (!callback) {
744 callback = lang
745 lang = null
746 if (typeof callback !== 'function') throw new Error('getPromotion missing callback parameter')
747 }
748
749 path = '/v1/grants'
750 if (lang) params.lang = lang
751 if (forPaymentId) {
752 params.paymentId = paymentId = forPaymentId
753 } else {
754 paymentId = self.state && self.state.properties && self.state.properties.wallet && self.state.properties.wallet.paymentId
755 if (paymentId) params.paymentId = paymentId
756 }
757 if ((lang) || (paymentId)) path += '?' + querystring.stringify(params)
758 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
759 self._log('getPromotion', { method: 'GET', path: path, errP: !!err })
760 if (err) return callback(err)
761
762 callback(null, body)
763 })
764}
765
766Client.prototype.setPromotion = function (promotionId, callback) {
767 const self = this
768
769 let path
770
771 if (self.options.version === 'v1') return
772
773 path = '/v1/grants/' + self.state.properties.wallet.paymentId
774 self._retryTrip(self, { path: path, method: 'PUT', payload: { promotionId: promotionId } }, function (err, response, body) {
775 self._log('publisherInfo', { method: 'PUT', path: path, errP: !!err })
776 if (err) return callback(err, null, response)
777
778 callback(null, body)
779 })
780}
781
782/*
783 *
784 * internal functions
785 *
786 */
787
788Client.prototype._registerPersona = function (callback) {
789 const self = this
790
791 const prefix = self.options.prefix + '/registrar/persona'
792 let path
793
794 path = prefix
795 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
796 let credential
797 let personaId = self.state.personaId || uuid.v4().toLowerCase()
798
799 self._log('_registerPersona', { method: 'GET', path: path, errP: !!err })
800 if (err) return callback(err)
801
802 credential = new anonize.Credential(personaId, body.registrarVK)
803
804 self.credentialRequest(credential, function (err, result) {
805 let body, keychains, keypair, octets, payload
806
807 if (err) return callback(err)
808
809 if (result.credential) credential = new anonize.Credential(result.credential)
810
811 if (self.options.version === 'v2') {
812 keypair = self.generateKeypair()
813 body = {
814 label: uuid.v4().toLowerCase(),
815 currency: 'BAT',
816 publicKey: braveCrypto.uint8ToHex(keypair.publicKey)
817 }
818 octets = stringify(body)
819 var headers = {
820 digest: 'SHA-256=' + crypto.createHash('sha256').update(octets).digest('base64')
821 }
822 headers['signature'] = sign({
823 headers: headers,
824 keyId: 'primary',
825 secretKey: braveCrypto.uint8ToHex(keypair.secretKey)
826 }, { algorithm: 'ed25519' })
827 payload = {
828 requestType: 'httpSignature',
829 request: {
830 headers: headers,
831 body: body,
832 octets: octets
833 }
834 }
835 }
836 payload.proof = result.proof
837
838 path = prefix + '/' + credential.parameters.userId
839 self._retryTrip(self, { path: path, method: 'POST', payload: payload }, function (err, response, body) {
840 let configuration, currency, days, fee
841
842 self._log('_registerPersona', { method: 'POST', path: prefix + '/...', errP: !!err })
843 if (err) return callback(err)
844
845 self.credentialFinalize(credential, body.verification, function (err, result) {
846 if (err) return callback(err)
847
848 self.credentials.persona = new anonize.Credential(result.credential)
849 self.state.persona = result.credential
850
851 configuration = body.payload && body.payload.adFree
852 if (!configuration) {
853 self._log('_registerPersona', { error: 'persona registration missing adFree configuration' })
854 return callback(new Error('persona registration missing adFree configuration'))
855 }
856
857 currency = configuration.currency || 'USD'
858 days = configuration.days || 30
859 if (!configuration.fee[currency]) {
860 if (currency === 'USD') {
861 self._log('_registerPersona', { error: 'USD is not supported by the ledger' })
862 return callback(new Error('USD is not supported by the ledger'))
863 }
864 if (!configuration.fee.USD) {
865 self._log('_registerPersona', { error: 'neither ' + currency + ' nor USD are supported by the ledger' })
866 return callback(new Error('neither ' + currency + ' nor USD are supported by the ledger'))
867 }
868 currency = 'USD'
869 }
870 fee = { currency: currency, amount: configuration.fee[currency] }
871
872 // yuck
873 const walletInfo = (self.state.properties && self.state.properties.wallet) || { }
874
875 self.state.personaId = personaId
876 self.state.properties = { setting: 'adFree',
877 fee: fee,
878 days: days,
879 configuration: body.contributions,
880 wallet: underscore.extend(walletInfo, body.wallet, { keychains: keychains })
881 }
882 self.state.bootStamp = underscore.now()
883 if (self.options.verboseP) self.state.bootDate = new Date(self.state.bootStamp)
884 self.state.reconcileStamp = self.state.bootStamp + (self.state.properties.days * msecs.day)
885 if (self.options.verboseP) self.state.reconcileDate = new Date(self.state.reconcileStamp)
886
887 self._log('_registerPersona', { personaId: personaId, delayTime: msecs.minute })
888 callback(null, self.state, msecs.minute)
889 })
890 })
891 })
892 })
893}
894
895Client.prototype._currentReconcile = function (callback) {
896 const self = this
897
898 const amount = self.state.properties.fee.amount
899 const currency = self.state.properties.fee.currency
900 const prefix = self.options.prefix + '/wallet/'
901 const surveyorInfo = self.state.currentReconcile.surveyorInfo
902 const viewingId = self.state.currentReconcile.viewingId
903 let fee, path, rates, suffix
904
905 suffix = '?' + self._getWalletParams({ amount: amount, currency: currency })
906 path = prefix + self.state.properties.wallet.paymentId + suffix
907 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
908 let alt, delayTime, keypair, octets, payload
909
910 self._log('_currentReconcile', { method: 'GET', path: prefix + '...?' + suffix, errP: !!err })
911 if (err) return callback(err)
912
913 if (!body.unsignedTx) {
914 if (body.rates[currency]) {
915 alt = (amount / body.rates[currency]).toFixed(4)
916 } else {
917 self._log('reconcile', { error: currency + ' no longer supported by the ledger' })
918 }
919
920 self.state.paymentInfo = underscore.extend(underscore.pick(body, [
921 'balance', 'buyURL', 'recurringURL', 'satoshis', 'altcurrency', 'probi'
922 ]),
923 {
924 address: self.state.properties.wallet.addresses && self.state.properties.wallet.addresses.BAT
925 ? self.state.properties.wallet.addresses.BAT : self.state.properties.wallet.address,
926 addresses: self.state.properties.wallet.addresses,
927 amount: amount,
928 currency: currency
929 })
930 self.state.paymentInfo[body.altcurrency ? body.altcurrency : 'btc'] = alt
931
932 delayTime = random.randomInt({ min: msecs.second, max: (self.options.debugP ? 1 : 10) * msecs.minute })
933 self._log('_currentReconcile', { reason: 'balance < btc', balance: body.balance, alt: alt, delayTime: delayTime })
934 return callback(null, self.state, delayTime)
935 }
936
937 const reconcile = (params) => {
938 path = prefix + self.state.properties.wallet.paymentId
939 payload = underscore.extend({ viewingId: viewingId, surveyorId: surveyorInfo.surveyorId }, params)
940 self._retryTrip(self, { path: path, method: 'PUT', payload: payload }, function (err, response, body) {
941 let transaction
942
943 self._log('_currentReconcile', { method: 'PUT', path: prefix + '...', errP: !!err })
944
945 delete self.state.currentReconcile
946
947 if (err) return callback(err)
948
949 transaction = { viewingId: viewingId,
950 surveyorId: surveyorInfo.surveyorId,
951 contribution: {
952 fiat: { amount: amount, currency: currency },
953 rates: rates,
954 satoshis: body.satoshis,
955 altcurrency: body.altcurrency,
956 probi: body.probi,
957 fee: fee
958 },
959 submissionStamp: body.paymentStamp,
960 submissionDate: self.options.verboseP ? new Date(body.paymentStamp) : undefined,
961 submissionId: body.hash
962 }
963 self.state.transactions.push(transaction)
964
965 self.state.reconcileStamp = underscore.now() + (self.state.properties.days * msecs.day)
966 if (self.options.verboseP) self.state.reconcileDate = new Date(self.state.reconcileStamp)
967
968 self._updateRules(function (err) {
969 if (err) self._log('_updateRules', { message: err.toString() })
970
971 self._log('_currentReconcile', { delayTime: msecs.minute })
972 callback(null, self.state, msecs.minute)
973 })
974 })
975 }
976
977 fee = body.unsignedTx.fee
978 rates = body.rates
979 if (body.altcurrency) {
980 keypair = self.getKeypair()
981 octets = stringify(body.unsignedTx)
982 var headers = {
983 digest: 'SHA-256=' + crypto.createHash('sha256').update(octets).digest('base64')
984 }
985 headers['signature'] = sign({
986 headers: headers,
987 keyId: 'primary',
988 secretKey: braveCrypto.uint8ToHex(keypair.secretKey)
989 }, { algorithm: 'ed25519' })
990 payload = {
991 requestType: 'httpSignature',
992 signedTx: {
993 headers: headers,
994 body: body.unsignedTx,
995 octets: octets
996 }
997 }
998
999 return reconcile(payload)
1000 }
1001 })
1002}
1003
1004Client.prototype._registerViewing = function (viewingId, callback) {
1005 const self = this
1006
1007 const prefix = self.options.prefix + '/registrar/viewing'
1008 let path = prefix
1009
1010 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, body) {
1011 let credential
1012
1013 self._log('_registerViewing', { method: 'GET', path: path, errP: !!err })
1014 if (err) return callback(err)
1015
1016 credential = new anonize.Credential(viewingId, body.registrarVK)
1017
1018 self.credentialRequest(credential, function (err, result) {
1019 if (err) return callback(err)
1020
1021 if (result.credential) credential = new anonize.Credential(result.credential)
1022
1023 path = prefix + '/' + credential.parameters.userId
1024 self._retryTrip(self, { path: path, method: 'POST', payload: { proof: result.proof } }, function (err, response, body) {
1025 let i
1026
1027 self._log('_registerViewing', { method: 'POST', path: prefix + '/...', errP: !!err })
1028 if (err) return callback(err)
1029
1030 self.credentialFinalize(credential, body.verification, function (err, result) {
1031 if (err) return callback(err)
1032
1033 for (i = self.state.transactions.length - 1; i >= 0; i--) {
1034 if (self.state.transactions[i].viewingId !== viewingId) continue
1035
1036 // NB: use of `underscore.extend` requires that the parameter be `self.state.transactions[i]`
1037 underscore.extend(self.state.transactions[i],
1038 { credential: result.credential,
1039 surveyorIds: body.surveyorIds,
1040 count: body.surveyorIds.length,
1041 satoshis: body.satoshis,
1042 altcurrency: body.altcurrency,
1043 probi: body.probi,
1044 votes: 0
1045 })
1046 self._log('_registerViewing', { delayTime: msecs.minute })
1047 return callback(null, self.state, msecs.minute)
1048 }
1049
1050 callback(new Error('viewingId ' + viewingId + ' not found in transaction list'))
1051 })
1052 })
1053 })
1054 })
1055}
1056
1057Client.prototype._prepareBallot = function (ballot, transaction, callback) {
1058 const self = this
1059
1060 const credential = new anonize.Credential(transaction.credential)
1061 const prefix = self.options.prefix + '/surveyor/voting/'
1062 let path
1063
1064 path = prefix + encodeURIComponent(ballot.surveyorId) + '/' + credential.parameters.userId
1065 self._retryTrip(self, { path: path, method: 'GET', useProxy: true }, function (err, response, body) {
1066 let delayTime, now
1067
1068 self._log('_prepareBallot', { method: 'GET', path: prefix + '...', errP: !!err })
1069 if (err) return callback(transaction.err = err)
1070
1071 ballot.prepareBallot = underscore.defaults(body, { server: self.options.server })
1072
1073 now = underscore.now()
1074 delayTime = random.randomInt({ min: 10 * msecs.second, max: (self.options.debugP ? 1 : 5) * msecs.minute })
1075 ballot.delayStamp = now + delayTime
1076 if (self.options.verboseP) ballot.delayDate = new Date(ballot.delayStamp)
1077
1078 if (delayTime > msecs.minute) delayTime = msecs.minute
1079 self._log('_prepareBallot', { delayTime: delayTime })
1080 callback(null, self.state, delayTime)
1081 })
1082}
1083
1084Client.prototype._commitBallot = function (ballot, transaction, callback) {
1085 const self = this
1086
1087 const credential = new anonize.Credential(transaction.credential)
1088 const prefix = self.options.prefix + '/surveyor/voting/'
1089 const surveyor = new anonize.Surveyor(ballot.prepareBallot)
1090 let path
1091
1092 path = prefix + encodeURIComponent(surveyor.parameters.surveyorId)
1093
1094 self.credentialSubmit(credential, surveyor, { publisher: ballot.publisher }, function (err, result) {
1095 if (err) return callback(err)
1096
1097 self._retryTrip(self, { path: path, method: 'PUT', useProxy: true, payload: result.payload }, function (err, response, body) {
1098 let i
1099
1100 self._log('_commitBallot', { method: 'PUT', path: prefix + '...', errP: !!err })
1101 if (err) return callback(transaction.err = err)
1102
1103 if (!transaction.ballots) transaction.ballots = {}
1104 if (!transaction.ballots[ballot.publisher]) transaction.ballots[ballot.publisher] = 0
1105 transaction.ballots[ballot.publisher]++
1106
1107 for (i = self.state.ballots.length - 1; i >= 0; i--) {
1108 if (self.state.ballots[i].surveyorId !== ballot.surveyorId) continue
1109
1110 self.state.ballots.splice(i, 1)
1111 break
1112 }
1113 if (i < 0) console.log('\n\nunable to find ballot surveyorId=' + ballot.surveyorId)
1114
1115 self._log('_commitBallot', { delayTime: msecs.minute })
1116 callback(null, self.state, msecs.minute)
1117 })
1118 })
1119}
1120
1121Client.prototype._getWalletParams = function (params) {
1122 let result = 'refresh=true'
1123
1124 if (params.amount) result += '&amount=' + params.amount
1125 if (params.currency) result += '&' + (params.currency === 'BAT' ? 'alt' : '') + 'currency=' + params.currency
1126
1127 return result
1128}
1129
1130Client.prototype._log = function (who, args) {
1131 const debugP = this.options ? this.options.debugP : false
1132 const loggingP = this.options ? this.options.loggingP : false
1133
1134 if (debugP) console.log(JSON.stringify({ who: who, what: args || {}, when: underscore.now() }, null, 2))
1135 if (loggingP) this.logging.push({ who: who, what: args || {}, when: underscore.now() })
1136}
1137
1138Client.prototype._updateRules = function (callback) {
1139 const self = this
1140
1141 let path
1142
1143 self.state.updatesStamp = underscore.now() + msecs.hour
1144 if (self.options.verboseP) self.state.updatesDate = new Date(self.state.updatesStamp)
1145
1146 path = '/v1/publisher/ruleset?consequential=true'
1147 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, ruleset) {
1148 let validity
1149
1150 self._log('_updateRules', { method: 'GET', path: '/v1/publisher/ruleset', errP: !!err })
1151 if (err) return callback(err)
1152
1153 validity = Joi.validate(ruleset, batPublisher.schema)
1154 if (validity.error) {
1155 self._log('_updateRules', { error: validity.error })
1156 return callback(new Error(validity.error))
1157 }
1158
1159 if (!underscore.isEqual(self.state.ruleset || [], ruleset)) {
1160 self.state.ruleset = ruleset
1161
1162 batPublisher.rules = ruleset
1163 }
1164
1165 self._updateRulesV2(callback)
1166 })
1167}
1168
1169Client.prototype._updateRulesV2 = function (callback) {
1170 const self = this
1171
1172 let path
1173
1174 self.state.updatesStamp = underscore.now() + msecs.hour
1175 if (self.options.verboseP) self.state.updatesDate = new Date(self.state.updatesStamp)
1176
1177 path = '/v2/publisher/ruleset?limit=512&excludedOnly=false'
1178 if (self.state.rulesV2Stamp) path += '&timestamp=' + self.state.rulesV2Stamp
1179 self._retryTrip(self, { path: path, method: 'GET' }, function (err, response, ruleset) {
1180 let c, i, rule, ts
1181
1182 self._log('_updateRules', { method: 'GET', path: '/v2/publisher/ruleset', errP: !!err })
1183 if (err) return callback(err)
1184
1185 if (ruleset.length === 0) return callback()
1186
1187 if (!self.state.rulesetV2) self.state.rulesetV2 = []
1188 self.state.rulesetV2 = self.state.rulesetV2.concat(ruleset)
1189 rule = underscore.last(ruleset)
1190
1191 if (ruleset.length < 512) {
1192 ts = rule.timestamp.split('')
1193 for (i = ts.length - 1; i >= 0; i--) {
1194 c = ts[i]
1195 if (c < '9') {
1196 ts[i] = String.fromCharCode(ts[i].charCodeAt(0) + 1)
1197 break
1198 }
1199 ts[i] = '0'
1200 }
1201
1202 self.state.rulesV2Stamp = ts.join('')
1203 } else {
1204 self.state.rulesV2Stamp = rule.timestamp
1205 }
1206
1207 setTimeout(function () { self._updateRulesV2.bind(self)(callback) }, 3 * msecs.second)
1208 })
1209}
1210
1211// round-trip to the ledger with retries!
1212Client.prototype._retryTrip = (self, params, callback, retry) => {
1213 let method
1214
1215 const loser = (reason) => { setTimeout(() => { callback(new Error(reason)) }, 0) }
1216 const rangeP = (n, min, max) => { return ((min <= n) && (n <= max) && (n === parseInt(n, 10))) }
1217
1218 if (!retry) {
1219 retry = underscore.defaults(params.backoff || {}, {
1220 algorithm: 'binaryExponential', delay: 5 * 1000, retries: 3, tries: 0
1221 })
1222 if (!rangeP(retry.delay, 1, 30 * 1000)) return loser('invalid backoff delay')
1223 if (!rangeP(retry.retries, 0, 10)) return loser('invalid backoff retries')
1224 if (!rangeP(retry.tries, 0, retry.retries - 1)) return loser('invalid backoff tries')
1225 }
1226 method = retry.method || backoff[retry.algorithm]
1227 if (typeof method !== 'function') return loser('invalid backoff algorithm')
1228 method = method(retry.delay)
1229
1230 self.roundtrip(params, (err, response, payload) => {
1231 const code = response && Math.floor(response.statusCode / 100)
1232
1233 if ((!err) || (code !== 5) || (retry.retries-- < 0)) return callback(err, response, payload)
1234
1235 return setTimeout(() => { self._retryTrip(self, params, callback, retry) }, method(++retry.tries))
1236 })
1237}
1238
1239Client.prototype._roundTrip = function (params, callback) {
1240 const self = this
1241
1242 const server = self.options.server
1243 const client = server.protocol === 'https:' ? https : http
1244 let request, timeoutP
1245
1246 params = underscore.extend(underscore.pick(self.options.server, [ 'protocol', 'hostname', 'port' ]), params)
1247 params.headers = underscore.defaults(params.headers || {},
1248 { 'content-type': 'application/json; charset=utf-8', 'accept-encoding': '' })
1249
1250 request = client.request(underscore.omit(params, [ 'useProxy', 'payload' ]), function (response) {
1251 let body = ''
1252
1253 if (timeoutP) return
1254 response.on('data', function (chunk) {
1255 body += chunk.toString()
1256 }).on('end', function () {
1257 let payload
1258
1259 if (params.timeout) request.setTimeout(0)
1260
1261 if (self.options.verboseP) {
1262 console.log('[ response for ' + params.method + ' ' + server.protocol + '//' + server.hostname + params.path + ' ]')
1263 console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode +
1264 ' ' + (response.statusMessage || ''))
1265 underscore.keys(response.headers).forEach(function (header) {
1266 console.log('>>> ' + header + ': ' + response.headers[header])
1267 })
1268 console.log('>>>')
1269 console.log('>>> ' + body.split('\n').join('\n>>> '))
1270 }
1271 if (Math.floor(response.statusCode / 100) !== 2) {
1272 self._log('_roundTrip', { error: 'HTTP response ' + response.statusCode })
1273 return callback(new Error('HTTP response ' + response.statusCode), response, null)
1274 }
1275
1276 try {
1277 payload = (response.statusCode !== 204) ? JSON.parse(body) : null
1278 } catch (err) {
1279 return callback(err, response, null)
1280 }
1281
1282 try {
1283 callback(null, response, payload)
1284 } catch (err0) {
1285 if (self.options.verboseP) console.log('callback: ' + err0.toString() + '\n' + err0.stack)
1286 }
1287 }).setEncoding('utf8')
1288 }).on('error', function (err) {
1289 callback(err)
1290 }).on('timeout', function () {
1291 timeoutP = true
1292 callback(new Error('timeout'))
1293 })
1294 if (params.payload) request.write(JSON.stringify(params.payload))
1295 request.end()
1296
1297 if (!self.options.verboseP) return
1298
1299 console.log('<<< ' + params.method + ' ' + params.protocol + '//' + params.hostname + params.path)
1300 underscore.keys(params.headers).forEach(function (header) { console.log('<<< ' + header + ': ' + params.headers[header]) })
1301 console.log('<<<')
1302 if (params.payload) console.log('<<< ' + JSON.stringify(params.payload, null, 2).split('\n').join('\n<<< '))
1303}
1304
1305/*
1306 * anonize2 helper
1307 */
1308
1309Client.prototype.initializeHelper = function () {
1310/*
1311 const self = this
1312
1313 this.helper = require('child_process').fork(require('path').join(__dirname, 'helper.js')).on('message', function (response) {
1314 const state = self.callbacks[response.msgno]
1315
1316 if (!state) return console.log('! >>> not expecting msgno=' + response.msgno)
1317
1318 delete self.callbacks[response.msgno]
1319 if (state.verboseP) console.log('! >>> ' + JSON.stringify(response, null, 2))
1320 state.callback(response.err, response.result)
1321 }).on('close', function (code, signal) {
1322 self.helper = null
1323 console.log('! >>> close ' + JSON.stringify({ code: code, signal: signal }))
1324 }).on('disconnect', function () {
1325 self.helper = null
1326 console.log('! >>> disconnect')
1327 }).on('error', function (err) {
1328 self.helper = null
1329 console.log('! >>> error ' + err.toString())
1330 }).on('exit', function (code, signal) {
1331 self.helper = null
1332 console.log('! >>> exit ' + JSON.stringify({ code: code, signal: signal }))
1333 })
1334 */
1335}
1336
1337Client.prototype.credentialWorker = function (operation, payload, callback) {
1338 const self = this
1339
1340 const msgno = self.seqno++
1341 const request = { msgno: msgno, operation: operation, payload: payload }
1342 let worker
1343
1344 self.callbacks[msgno] = { verboseP: self.options.verboseP, callback: callback }
1345
1346 worker = self.options.createWorker('bat-client/worker.js')
1347 worker.onmessage = function (evt) {
1348 const response = evt.data
1349 const state = self.callbacks[response.msgno]
1350
1351 if (!state) return console.log('! >>> not expecting msgno=' + response.msgno)
1352
1353 delete self.callbacks[response.msgno]
1354 if (state.verboseP) console.log('! >>> ' + JSON.stringify(response, null, 2))
1355 state.callback(response.err, response.result)
1356 worker.terminate()
1357 }
1358 worker.onerror = function (message, stack) {
1359 console.log('! >>> worker error: ' + message)
1360 console.log(stack)
1361 try { worker.terminate() } catch (ex) { }
1362 }
1363
1364 worker.on('start', function () {
1365 if (self.options.verboseP) console.log('! <<< ' + JSON.stringify(request, null, 2))
1366 worker.postMessage(request)
1367 })
1368 worker.start()
1369}
1370
1371Client.prototype.credentialRoundTrip = function (operation, payload, callback) {
1372 const msgno = this.seqno++
1373 const request = { msgno: msgno, operation: operation, payload: payload }
1374
1375 this.callbacks[msgno] = { verboseP: this.options.verboseP, callback: callback }
1376 if (this.options.verboseP) console.log('! <<< ' + JSON.stringify(request, null, 2))
1377 this.helper.send(request)
1378}
1379
1380Client.prototype.credentialRequest = function (credential, callback) {
1381 let proof
1382
1383 if (this.options.createWorker) return this.credentialWorker('request', { credential: JSON.stringify(credential) }, callback)
1384
1385 if (this.helper) this.credentialRoundTrip('request', { credential: JSON.stringify(credential) }, callback)
1386
1387 try { proof = credential.request() } catch (ex) { return callback(ex) }
1388 callback(null, { proof: proof })
1389}
1390
1391Client.prototype.credentialFinalize = function (credential, verification, callback) {
1392 if (this.options.createWorker) {
1393 return this.credentialWorker('finalize', { credential: JSON.stringify(credential), verification: verification }, callback)
1394 }
1395
1396 if (this.helper) {
1397 return this.credentialRoundTrip('finalize', { credential: JSON.stringify(credential), verification: verification },
1398 callback)
1399 }
1400
1401 try { credential.finalize(verification) } catch (ex) { return callback(ex) }
1402 callback(null, { credential: JSON.stringify(credential) })
1403}
1404
1405Client.prototype.credentialSubmit = function (credential, surveyor, data, callback) {
1406 let payload
1407
1408 if (this.options.createWorker) {
1409 return this.credentialWorker('submit',
1410 { credential: JSON.stringify(credential), surveyor: JSON.stringify(surveyor), data: data },
1411 callback)
1412 }
1413
1414 if (this.helper) {
1415 return this.credentialRoundTrip('submit',
1416 { credential: JSON.stringify(credential), surveyor: JSON.stringify(surveyor), data: data },
1417 callback)
1418 }
1419
1420 try { payload = { proof: credential.submit(surveyor, data) } } catch (ex) { return callback(ex) }
1421 return callback(null, { payload: payload })
1422}
1423
1424Client.prototype._fuzzing = function (synopsis) {
1425 let duration = 0
1426 let remaining = this.state.reconcileStamp - underscore.now()
1427 let advance, memo, ratio, window
1428
1429 if ((!synopsis) ||
1430 (remaining > (3 * msecs.day)) ||
1431 (this.options && this.options.noFuzzing) ||
1432 (this.boolion(process.env.LEDGER_NO_FUZZING))) return
1433
1434 synopsis.prune()
1435 underscore.keys(synopsis.publishers).forEach((publisher) => {
1436 duration += synopsis.publishers[publisher].duration
1437 })
1438
1439 // at the moment hard-wired to 30 minutes every 30 days
1440 ratio = duration / (30 * msecs.minute)
1441 memo = { duration: duration, ratio1: ratio, numFrames: synopsis.options.numFrames, frameSize: synopsis.options.frameSize }
1442 window = synopsis.options.numFrames * synopsis.options.frameSize
1443 if (window > 0) ratio *= (30 * msecs.day) / window
1444 if (ratio >= 1.0) return
1445
1446 memo.window = window
1447 memo.ratio2 = ratio
1448
1449 advance = Math.round((this.state.properties.days * msecs.day) * (1.0 - ratio))
1450 memo.advance1 = advance
1451 if (advance > (3 * msecs.day)) advance = 3 * msecs.day
1452 memo.advance2 = advance
1453 if (advance) this.state.reconcileStamp += advance
1454 memo.reconcileStamp = this.state.reconcileStamp
1455 memo.reconcileDate = new Date(memo.reconcileStamp)
1456
1457 this.memo('_fuzzing', memo)
1458}
1459/*
1460 *
1461 * utilities
1462 *
1463 */
1464
1465Client.prototype.boolion = function (value) {
1466 const f = {
1467 undefined: function () {
1468 return false
1469 },
1470
1471 boolean: function () {
1472 return value
1473 },
1474
1475 // handles `Infinity` and `NaN`
1476 number: function () {
1477 return (!!value)
1478 },
1479
1480 string: function () {
1481 return ([ 'n', 'no', 'false', '0' ].indexOf(value.toLowerCase()) === -1)
1482 },
1483
1484 // handles `null`
1485 object: function () {
1486 return (!!value)
1487 }
1488 }[typeof value] || function () { return false }
1489
1490 return f()
1491}
1492
1493Client.prototype.numbion = function (value) {
1494 if (typeof value === 'string') value = parseInt(value, 10)
1495
1496 const f = {
1497 undefined: function () {
1498 return 0
1499 },
1500
1501 boolean: function () {
1502 return (value ? 1 : 0)
1503 },
1504
1505 // handles `Infinity` and `NaN`
1506 number: function () {
1507 return (Number.isFinite(value) ? value : 0)
1508 },
1509
1510 object: function () {
1511 return (value ? 1 : 0)
1512 }
1513 }[typeof value] || function () { return 0 }
1514
1515 return f()
1516}
1517
1518module.exports = Client
1519
\No newline at end of file