UNPKG

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