UNPKG

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