UNPKG

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