· 6 years ago · Mar 26, 2020, 03:32 PM
1// Untitled Dice v0.0.8
2
3// Customize these configuration settings:
4
5var config = {
6 // - Your app's id on moneypot.com
7 app_id: 838, // <----------------------------- EDIT ME!
8 // - Displayed in the navbar
9 app_name: 'BitWarz Dice',
10 // - For your faucet to work, you must register your site at Recaptcha
11 // - https://www.google.com/recaptcha/intro/index.html
12 recaptcha_sitekey: '6LepDBQTAAAAALmtECCYgTV5p0-isqFHLagQaNa-', // <----- EDIT ME!
13 redirect_uri: 'dice.bitwarz.co.uk/index.html',
14 mp_browser_uri: 'https://www.moneypot.com',
15 mp_api_uri: 'https://api.moneypot.com',
16 chat_uri: '//socket.moneypot.com',
17 // - Show debug output only if running on localhost
18 debug: isRunningLocally(),
19 // - Set this to true if you want users that come to http:// to be redirected
20 // to https://
21 // force_https_redirect: !isRunningLocally(),
22 // - Configure the house edge (default is 1%)
23 // Must be between 0.0 (0%) and 1.0 (100%)
24 house_edge: 0.01,
25 chat_buffer_size: 250,
26 // - The amount of bets to show on screen in each tab
27 bet_buffer_size: 25
28};
29
30////////////////////////////////////////////////////////////
31// You shouldn't have to edit anything below this line
32////////////////////////////////////////////////////////////
33
34// Validate the configured house edge
35(function() {
36 var errString;
37
38 if (config.house_edge <= 0.0) {
39 errString = 'House edge must be > 0.0 (0%)';
40 } else if (config.house_edge >= 100.0) {
41 errString = 'House edge must be < 1.0 (100%)';
42 }
43
44 if (errString) {
45 alert(errString);
46 throw new Error(errString);
47 }
48
49 // Sanity check: Print house edge
50 console.log('House Edge:', (config.house_edge * 100).toString() + '%');
51})();
52
53////////////////////////////////////////////////////////////
54
55if (config.force_https_redirect && window.location.protocol !== "https:") {
56 window.location.href = "https:" + window.location.href.substring(window.location.protocol.length);
57}
58
59// Hoist it. It's impl'd at bottom of page.
60var socket;
61
62// :: Bool
63function isRunningLocally() {
64 return /^localhost/.test(window.location.host);
65}
66
67var el = React.DOM;
68
69// Generates UUID for uniquely tagging components
70var genUuid = function() {
71 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
72 var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
73 return v.toString(16);
74 });
75};
76
77var helpers = {};
78
79// For displaying HH:MM timestamp in chat
80//
81// String (Date JSON) -> String
82helpers.formatDateToTime = function(dateJson) {
83 var date = new Date(dateJson);
84 return _.padLeft(date.getHours().toString(), 2, '0') +
85 ':' +
86 _.padLeft(date.getMinutes().toString(), 2, '0');
87};
88
89// Number -> Number in range (0, 1)
90helpers.multiplierToWinProb = function(multiplier) {
91 console.assert(typeof multiplier === 'number');
92 console.assert(multiplier > 0);
93
94 // For example, n is 0.99 when house edge is 1%
95 var n = 1.0 - config.house_edge;
96
97 return n / multiplier;
98};
99
100helpers.calcNumber = function(cond, winProb) {
101 console.assert(cond === '<' || cond === '>');
102 console.assert(typeof winProb === 'number');
103
104 if (cond === '<') {
105 return winProb * 100;
106 } else {
107 return 99.99 - (winProb * 100);
108 }
109};
110
111helpers.roleToLabelElement = function(role) {
112 switch(role) {
113 case 'ADMIN':
114 return el.span({className: 'label label-danger'}, 'MP Staff');
115 case 'MOD':
116 return el.span({className: 'label label-info'}, 'Mod');
117 case 'OWNER':
118 return el.span({className: 'label label-primary'}, 'Owner');
119 default:
120 return '';
121 }
122};
123
124// -> Object
125helpers.getHashParams = function() {
126 var hashParams = {};
127 var e,
128 a = /\+/g, // Regex for replacing addition symbol with a space
129 r = /([^&;=]+)=?([^&;]*)/g,
130 d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
131 q = window.location.hash.substring(1);
132 while (e = r.exec(q))
133 hashParams[d(e[1])] = d(e[2]);
134 return hashParams;
135};
136
137// getPrecision('1') -> 0
138// getPrecision('.05') -> 2
139// getPrecision('25e-100') -> 100
140// getPrecision('2.5e-99') -> 100
141helpers.getPrecision = function(num) {
142 var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
143 if (!match) { return 0; }
144 return Math.max(
145 0,
146 // Number of digits right of decimal point.
147 (match[1] ? match[1].length : 0) -
148 // Adjust for scientific notation.
149 (match[2] ? +match[2] : 0));
150};
151
152/**
153 * Decimal adjustment of a number.
154 *
155 * @param {String} type The type of adjustment.
156 * @param {Number} value The number.
157 * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base).
158 * @returns {Number} The adjusted value.
159 */
160helpers.decimalAdjust = function(type, value, exp) {
161 // If the exp is undefined or zero...
162 if (typeof exp === 'undefined' || +exp === 0) {
163 return Math[type](value);
164 }
165 value = +value;
166 exp = +exp;
167 // If the value is not a number or the exp is not an integer...
168 if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
169 return NaN;
170 }
171 // Shift
172 value = value.toString().split('e');
173 value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
174 // Shift back
175 value = value.toString().split('e');
176 return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
177}
178
179helpers.round10 = function(value, exp) {
180 return helpers.decimalAdjust('round', value, exp);
181};
182
183helpers.floor10 = function(value, exp) {
184 return helpers.decimalAdjust('floor', value, exp);
185};
186
187helpers.ceil10 = function(value, exp) {
188 return helpers.decimalAdjust('ceil', value, exp);
189};
190
191////////////////////////////////////////////////////////////
192
193// A weak Moneypot API abstraction
194//
195// Moneypot's API docs: https://www.moneypot.com/api-docs
196var MoneyPot = (function() {
197
198 var o = {};
199
200 o.apiVersion = 'v1';
201
202 // method: 'GET' | 'POST' | ...
203 // endpoint: '/tokens/abcd-efgh-...'
204 var noop = function() {};
205 var makeMPRequest = function(method, bodyParams, endpoint, callbacks, overrideOpts) {
206
207 if (!worldStore.state.accessToken)
208 throw new Error('Must have accessToken set to call MoneyPot API');
209
210 var url = config.mp_api_uri + '/' + o.apiVersion + endpoint;
211
212 if (worldStore.state.accessToken) {
213 url = url + '?access_token=' + worldStore.state.accessToken;
214 }
215
216 var ajaxOpts = {
217 url: url,
218 dataType: 'json', // data type of response
219 method: method,
220 data: bodyParams ? JSON.stringify(bodyParams) : undefined,
221 // By using text/plain, even though this is a JSON request,
222 // we avoid preflight request. (Moneypot explicitly supports this)
223 headers: {
224 'Content-Type': 'text/plain'
225 },
226 // Callbacks
227 success: callbacks.success || noop,
228 error: callbacks.error || noop,
229 complete: callbacks.complete || noop
230 };
231
232 $.ajax(_.merge({}, ajaxOpts, overrideOpts || {}));
233 };
234
235 o.listBets = function(callbacks) {
236 var endpoint = '/list-bets';
237 makeMPRequest('GET', undefined, endpoint, callbacks, {
238 data: {
239 app_id: config.app_id,
240 limit: config.bet_buffer_size
241 }
242 });
243 };
244
245 o.getTokenInfo = function(callbacks) {
246 var endpoint = '/token';
247 makeMPRequest('GET', undefined, endpoint, callbacks);
248 };
249
250 o.generateBetHash = function(callbacks) {
251 var endpoint = '/hashes';
252 makeMPRequest('POST', undefined, endpoint, callbacks);
253 };
254
255 o.getDepositAddress = function(callbacks) {
256 var endpoint = '/deposit-address';
257 makeMPRequest('GET', undefined, endpoint, callbacks);
258 };
259
260 // gRecaptchaResponse is string response from google server
261 // `callbacks.success` signature is fn({ claim_id: Int, amoutn: Satoshis })
262 o.claimFaucet = function(gRecaptchaResponse, callbacks) {
263 console.log('Hitting POST /claim-faucet');
264 var endpoint = '/claim-faucet';
265 var body = { response: gRecaptchaResponse };
266 makeMPRequest('POST', body, endpoint, callbacks);
267 };
268
269 // bodyParams is an object:
270 // - wager: Int in satoshis
271 // - client_seed: Int in range [0, 0^32)
272 // - hash: BetHash
273 // - cond: '<' | '>'
274 // - number: Int in range [0, 99.99] that cond applies to
275 // - payout: how many satoshis to pay out total on win (wager * multiplier)
276 o.placeSimpleDiceBet = function(bodyParams, callbacks) {
277 var endpoint = '/bets/simple-dice';
278 makeMPRequest('POST', bodyParams, endpoint, callbacks);
279 };
280
281 return o;
282})();
283
284////////////////////////////////////////////////////////////
285
286var Dispatcher = new (function() {
287 // Map of actionName -> [Callback]
288 this.callbacks = {};
289
290 var self = this;
291
292 // Hook up a store's callback to receive dispatched actions from dispatcher
293 //
294 // Ex: Dispatcher.registerCallback('NEW_MESSAGE', function(message) {
295 // console.log('store received new message');
296 // self.state.messages.push(message);
297 // self.emitter.emit('change', self.state);
298 // });
299 this.registerCallback = function(actionName, cb) {
300 console.log('[Dispatcher] registering callback for:', actionName);
301
302 if (!self.callbacks[actionName]) {
303 self.callbacks[actionName] = [cb];
304 } else {
305 self.callbacks[actionName].push(cb);
306 }
307 };
308
309 this.sendAction = function(actionName, payload) {
310 console.log('[Dispatcher] received action:', actionName, payload);
311
312 // Ensure this action has 1+ registered callbacks
313 if (!self.callbacks[actionName]) {
314 throw new Error('Unsupported actionName: ' + actionName);
315 }
316
317 // Dispatch payload to each registered callback for this action
318 self.callbacks[actionName].forEach(function(cb) {
319 cb(payload);
320 });
321 };
322});
323
324////////////////////////////////////////////////////////////
325
326var Store = function(storeName, initState, initCallback) {
327
328 this.state = initState;
329 this.emitter = new EventEmitter();
330
331 // Execute callback immediately once store (above state) is setup
332 // This callback should be used by the store to register its callbacks
333 // to the dispatcher upon initialization
334 initCallback.call(this);
335
336 var self = this;
337
338 // Allow components to listen to store events (i.e. its 'change' event)
339 this.on = function(eventName, cb) {
340 self.emitter.on(eventName, cb);
341 };
342
343 this.off = function(eventName, cb) {
344 self.emitter.off(eventName, cb);
345 };
346};
347
348////////////////////////////////////////////////////////////
349
350// Manage access_token //////////////////////////////////////
351//
352// - If access_token is in url, save it into localStorage.
353// `expires_in` (seconds until expiration) will also exist in url
354// so turn it into a date that we can compare
355
356var access_token, expires_in, expires_at;
357
358if (helpers.getHashParams().access_token) {
359 console.log('[token manager] access_token in hash params');
360 access_token = helpers.getHashParams().access_token;
361 expires_in = helpers.getHashParams().expires_in;
362 expires_at = new Date(Date.now() + (expires_in * 1000));
363
364 localStorage.setItem('access_token', access_token);
365 localStorage.setItem('expires_at', expires_at);
366} else if (localStorage.access_token) {
367 console.log('[token manager] access_token in localStorage');
368 expires_at = localStorage.expires_at;
369 // Only get access_token from localStorage if it expires
370 // in a week or more. access_tokens are valid for two weeks
371 if (expires_at && new Date(expires_at) > new Date(Date.now() + (1000 * 60 * 60 * 24 * 7))) {
372 access_token = localStorage.access_token;
373 } else {
374 localStorage.removeItem('expires_at');
375 localStorage.removeItem('access_token');
376 }
377} else {
378 console.log('[token manager] no access token');
379}
380
381// Scrub fragment params from url.
382if (window.history && window.history.replaceState) {
383 window.history.replaceState({}, document.title, "/");
384} else {
385 // For browsers that don't support html5 history api, just do it the old
386 // fashioned way that leaves a trailing '#' in the url
387 window.location.hash = '#';
388}
389
390////////////////////////////////////////////////////////////
391
392var chatStore = new Store('chat', {
393 messages: new CBuffer(config.chat_buffer_size),
394 waitingForServer: false,
395 userList: {},
396 showUserList: false,
397 loadingInitialMessages: true
398}, function() {
399 var self = this;
400
401 // `data` is object received from socket auth
402 Dispatcher.registerCallback('INIT_CHAT', function(data) {
403 console.log('[ChatStore] received INIT_CHAT');
404 // Give each one unique id
405 var messages = data.chat.messages.map(function(message) {
406 message.id = genUuid();
407 return message;
408 });
409
410 // Reset the CBuffer since this event may fire multiple times,
411 // e.g. upon every reconnection to chat-server.
412 self.state.messages.empty();
413
414 self.state.messages.push.apply(self.state.messages, messages);
415
416 // Indicate that we're done with initial fetch
417 self.state.loadingInitialMessages = false;
418
419 // Load userList
420 self.state.userList = data.chat.userlist;
421 self.emitter.emit('change', self.state);
422 self.emitter.emit('init');
423 });
424
425 Dispatcher.registerCallback('NEW_MESSAGE', function(message) {
426 console.log('[ChatStore] received NEW_MESSAGE');
427 message.id = genUuid();
428 self.state.messages.push(message);
429
430 self.emitter.emit('change', self.state);
431 self.emitter.emit('new_message');
432 });
433
434 Dispatcher.registerCallback('TOGGLE_CHAT_USERLIST', function() {
435 console.log('[ChatStore] received TOGGLE_CHAT_USERLIST');
436 self.state.showUserList = !self.state.showUserList;
437 self.emitter.emit('change', self.state);
438 });
439
440 // user is { id: Int, uname: String, role: 'admin' | 'mod' | 'owner' | 'member' }
441 Dispatcher.registerCallback('USER_JOINED', function(user) {
442 console.log('[ChatStore] received USER_JOINED:', user);
443 self.state.userList[user.uname] = user;
444 self.emitter.emit('change', self.state);
445 });
446
447 // user is { id: Int, uname: String, role: 'admin' | 'mod' | 'owner' | 'member' }
448 Dispatcher.registerCallback('USER_LEFT', function(user) {
449 console.log('[ChatStore] received USER_LEFT:', user);
450 delete self.state.userList[user.uname];
451 self.emitter.emit('change', self.state);
452 });
453
454 // Message is { text: String }
455 Dispatcher.registerCallback('SEND_MESSAGE', function(text) {
456 console.log('[ChatStore] received SEND_MESSAGE');
457 self.state.waitingForServer = true;
458 self.emitter.emit('change', self.state);
459 socket.emit('new_message', { text: text }, function(err) {
460 if (err) {
461 alert('Chat Error: ' + err);
462 }
463 });
464 });
465});
466
467var betStore = new Store('bet', {
468 nextHash: undefined,
469 wager: {
470 str: '1',
471 num: 1,
472 error: undefined
473 },
474 multiplier: {
475 str: '2.00',
476 num: 2.00,
477 error: undefined
478 },
479 hotkeysEnabled: false
480}, function() {
481 var self = this;
482
483 Dispatcher.registerCallback('SET_NEXT_HASH', function(hexString) {
484 self.state.nextHash = hexString;
485 self.emitter.emit('change', self.state);
486 });
487
488 Dispatcher.registerCallback('UPDATE_WAGER', function(newWager) {
489 self.state.wager = _.merge({}, self.state.wager, newWager);
490
491 var n = parseInt(self.state.wager.str, 10);
492
493 // If n is a number, ensure it's at least 1 bit
494 if (isFinite(n)) {
495 n = Math.max(n, 1);
496 self.state.wager.str = n.toString();
497 }
498
499 // Ensure wagerString is a number
500 if (isNaN(n) || /[^\d]/.test(n.toString())) {
501 self.state.wager.error = 'INVALID_WAGER';
502 // Ensure user can afford balance
503 } else if (n * 100 > worldStore.state.user.balance) {
504 self.state.wager.error = 'CANNOT_AFFORD_WAGER';
505 self.state.wager.num = n;
506 } else {
507 // wagerString is valid
508 self.state.wager.error = null;
509 self.state.wager.str = n.toString();
510 self.state.wager.num = n;
511 }
512
513 self.emitter.emit('change', self.state);
514 });
515
516 Dispatcher.registerCallback('UPDATE_MULTIPLIER', function(newMult) {
517 self.state.multiplier = _.merge({}, self.state.multiplier, newMult);
518 self.emitter.emit('change', self.state);
519 });
520});
521
522// The general store that holds all things until they are separated
523// into smaller stores for performance.
524var worldStore = new Store('world', {
525 isLoading: true,
526 user: undefined,
527 accessToken: access_token,
528 isRefreshingUser: false,
529 hotkeysEnabled: false,
530 currTab: 'ALL_BETS',
531 // TODO: Turn this into myBets or something
532 bets: new CBuffer(config.bet_buffer_size),
533 // TODO: Fetch list on load alongside socket subscription
534 allBets: new CBuffer(config.bet_buffer_size),
535 grecaptcha: undefined
536}, function() {
537 var self = this;
538
539 // TODO: Consider making these emit events unique to each callback
540 // for more granular reaction.
541
542 // data is object, note, assumes user is already an object
543 Dispatcher.registerCallback('UPDATE_USER', function(data) {
544 self.state.user = _.merge({}, self.state.user, data);
545 self.emitter.emit('change', self.state);
546 });
547
548 // deprecate in favor of SET_USER
549 Dispatcher.registerCallback('USER_LOGIN', function(user) {
550 self.state.user = user;
551 self.emitter.emit('change', self.state);
552 self.emitter.emit('user_update');
553 });
554
555 // Replace with CLEAR_USER
556 Dispatcher.registerCallback('USER_LOGOUT', function() {
557 self.state.user = undefined;
558 self.state.accessToken = undefined;
559 localStorage.removeItem('expires_at');
560 localStorage.removeItem('access_token');
561 self.state.bets.empty();
562 self.emitter.emit('change', self.state);
563 });
564
565 Dispatcher.registerCallback('START_LOADING', function() {
566 self.state.isLoading = true;
567 self.emitter.emit('change', self.state);
568 });
569
570 Dispatcher.registerCallback('STOP_LOADING', function() {
571 self.state.isLoading = false;
572 self.emitter.emit('change', self.state);
573 });
574
575 Dispatcher.registerCallback('CHANGE_TAB', function(tabName) {
576 console.assert(typeof tabName === 'string');
577 self.state.currTab = tabName;
578 self.emitter.emit('change', self.state);
579 });
580
581 // This is only for my bets? Then change to 'NEW_MY_BET'
582 Dispatcher.registerCallback('NEW_BET', function(bet) {
583 console.assert(typeof bet === 'object');
584 self.state.bets.push(bet);
585 self.emitter.emit('change', self.state);
586 });
587
588 Dispatcher.registerCallback('NEW_ALL_BET', function(bet) {
589 self.state.allBets.push(bet);
590 self.emitter.emit('change', self.state);
591 });
592
593 Dispatcher.registerCallback('INIT_ALL_BETS', function(bets) {
594 console.assert(_.isArray(bets));
595 self.state.allBets.push.apply(self.state.allBets, bets);
596 self.emitter.emit('change', self.state);
597 });
598
599 Dispatcher.registerCallback('TOGGLE_HOTKEYS', function() {
600 self.state.hotkeysEnabled = !self.state.hotkeysEnabled;
601 self.emitter.emit('change', self.state);
602 });
603
604 Dispatcher.registerCallback('DISABLE_HOTKEYS', function() {
605 self.state.hotkeysEnabled = false;
606 self.emitter.emit('change', self.state);
607 });
608
609 Dispatcher.registerCallback('START_REFRESHING_USER', function() {
610 self.state.isRefreshingUser = true;
611 self.emitter.emit('change', self.state);
612 MoneyPot.getTokenInfo({
613 success: function(data) {
614 console.log('Successfully loaded user from tokens endpoint', data);
615 var user = data.auth.user;
616 self.state.user = user;
617 self.emitter.emit('change', self.state);
618 self.emitter.emit('user_update');
619 },
620 error: function(err) {
621 console.log('Error:', err);
622 },
623 complete: function() {
624 Dispatcher.sendAction('STOP_REFRESHING_USER');
625 }
626 });
627 });
628
629 Dispatcher.registerCallback('STOP_REFRESHING_USER', function() {
630 self.state.isRefreshingUser = false;
631 self.emitter.emit('change', self.state);
632 });
633
634 Dispatcher.registerCallback('GRECAPTCHA_LOADED', function(_grecaptcha) {
635 self.state.grecaptcha = _grecaptcha;
636 self.emitter.emit('grecaptcha_loaded');
637 });
638
639});
640
641////////////////////////////////////////////////////////////
642
643
644////////////////////////////////////////////////////////////
645
646var UserBox = React.createClass({
647 displayName: 'UserBox',
648 _onStoreChange: function() {
649 this.forceUpdate();
650 },
651 componentDidMount: function() {
652 worldStore.on('change', this._onStoreChange);
653 betStore.on('change', this._onStoreChange);
654 },
655 componentWillUnount: function() {
656 worldStore.off('change', this._onStoreChange);
657 betStore.off('change', this._onStoreChange);
658 },
659 _onLogout: function() {
660 Dispatcher.sendAction('USER_LOGOUT');
661 },
662 _onRefreshUser: function() {
663 Dispatcher.sendAction('START_REFRESHING_USER');
664 },
665 _openWithdrawPopup: function() {
666 var windowUrl = config.mp_browser_uri + '/dialog/withdraw?app_id=' + config.app_id;
667 var windowName = 'manage-auth';
668 var windowOpts = [
669 'width=420',
670 'height=350',
671 'left=100',
672 'top=100'
673 ].join(',');
674 var windowRef = window.open(windowUrl, windowName, windowOpts);
675 windowRef.focus();
676 return false;
677 },
678 _openDepositPopup: function() {
679 var windowUrl = config.mp_browser_uri + '/dialog/deposit?app_id=' + config.app_id;
680 var windowName = 'manage-auth';
681 var windowOpts = [
682 'width=420',
683 'height=350',
684 'left=100',
685 'top=100'
686 ].join(',');
687 var windowRef = window.open(windowUrl, windowName, windowOpts);
688 windowRef.focus();
689 return false;
690 },
691 render: function() {
692
693 var innerNode;
694 if (worldStore.state.isLoading) {
695 innerNode = el.p(
696 {className: 'navbar-text'},
697 'Loading...'
698 );
699 } else if (worldStore.state.user) {
700 innerNode = el.div(
701 null,
702 // Deposit/Withdraw popup buttons
703 el.div(
704 {className: 'btn-group navbar-left btn-group-xs'},
705 el.button(
706 {
707 type: 'button',
708 className: 'btn navbar-btn btn-xs ' + (betStore.state.wager.error === 'CANNOT_AFFORD_WAGER' ? 'btn-success' : 'btn-default'),
709 onClick: this._openDepositPopup
710 },
711 'Deposit'
712 ),
713 el.button(
714 {
715 type: 'button',
716 className: 'btn btn-default navbar-btn btn-xs',
717 onClick: this._openWithdrawPopup
718 },
719 'Withdraw'
720 )
721 ),
722 // Balance
723 el.span(
724 {
725 className: 'navbar-text',
726 style: {marginRight: '5px'}
727 },
728 (worldStore.state.user.balance / 100) + ' bits',
729 !worldStore.state.user.unconfirmed_balance ?
730 '' :
731 el.span(
732 {style: { color: '#e67e22'}},
733 ' + ' + (worldStore.state.user.unconfirmed_balance / 100) + ' bits pending'
734 )
735 ),
736 // Refresh button
737 el.button(
738 {
739 className: 'btn btn-link navbar-btn navbar-left ' + (worldStore.state.isRefreshingUser ? ' rotate' : ''),
740 title: 'Refresh Balance',
741 disabled: worldStore.state.isRefreshingUser,
742 onClick: this._onRefreshUser,
743 style: {
744 paddingLeft: 0,
745 paddingRight: 0,
746 marginRight: '10px'
747 }
748 },
749 el.span({className: 'glyphicon glyphicon-refresh'})
750 ),
751 // Logged in as...
752 el.span(
753 {className: 'navbar-text'},
754 'Logged in as ',
755 el.code(null, worldStore.state.user.uname)
756 ),
757 // Logout button
758 el.button(
759 {
760 type: 'button',
761 onClick: this._onLogout,
762 className: 'navbar-btn btn btn-default'
763 },
764 'Logout'
765 )
766 );
767 } else {
768 // User needs to login
769 innerNode = el.p(
770 {className: 'navbar-text'},
771 el.a(
772 {
773 href: config.mp_browser_uri + '/oauth/authorize' +
774 '?app_id=' + config.app_id +
775 '&redirect_uri=' + config.redirect_uri,
776 className: 'btn btn-default'
777 },
778 'Login with Moneypot'
779 )
780 );
781 }
782
783 return el.div(
784 {className: 'navbar-right'},
785 innerNode
786 );
787 }
788});
789
790var Navbar = React.createClass({
791 displayName: 'Navbar',
792 render: function() {
793 return el.div(
794 {className: 'navbar'},
795 el.div(
796 {className: 'container-fluid'},
797 el.div(
798 {className: 'navbar-header'},
799 el.a({className: 'navbar-brand', href:'/'}, config.app_name)
800 ),
801 // Links
802 el.ul(
803 {className: 'nav navbar-nav'},
804 el.li(
805 null,
806 el.a(
807 {
808 href: config.mp_browser_uri + '/apps/' + config.app_id,
809 target: '_blank'
810 },
811 'View on Moneypot ',
812 // External site glyphicon
813 el.span(
814 {className: 'glyphicon glyphicon-new-window'}
815 )
816 )
817 )
818 ),
819 // Userbox
820 React.createElement(UserBox, null)
821 )
822 );
823 }
824});
825
826var ChatBoxInput = React.createClass({
827 displayName: 'ChatBoxInput',
828 _onStoreChange: function() {
829 this.forceUpdate();
830 },
831 componentDidMount: function() {
832 chatStore.on('change', this._onStoreChange);
833 worldStore.on('change', this._onStoreChange);
834 },
835 componentWillUnmount: function() {
836 chatStore.off('change', this._onStoreChange);
837 worldStore.off('change', this._onStoreChange);
838 },
839 //
840 getInitialState: function() {
841 return { text: '' };
842 },
843 // Whenever input changes
844 _onChange: function(e) {
845 this.setState({ text: e.target.value });
846 },
847 // When input contents are submitted to chat server
848 _onSend: function() {
849 var self = this;
850 Dispatcher.sendAction('SEND_MESSAGE', this.state.text);
851 this.setState({ text: '' });
852 },
853 _onFocus: function() {
854 // When users click the chat input, turn off bet hotkeys so they
855 // don't accidentally bet
856 if (worldStore.state.hotkeysEnabled) {
857 Dispatcher.sendAction('DISABLE_HOTKEYS');
858 }
859 },
860 _onKeyPress: function(e) {
861 var ENTER = 13;
862 if (e.which === ENTER) {
863 if (this.state.text.trim().length > 0) {
864 this._onSend();
865 }
866 }
867 },
868 render: function() {
869 return (
870 el.div(
871 {className: 'row'},
872 el.div(
873 {className: 'col-md-9'},
874 chatStore.state.loadingInitialMessages ?
875 el.div(
876 {
877 style: {marginTop: '7px'},
878 className: 'text-muted'
879 },
880 el.span(
881 {className: 'glyphicon glyphicon-refresh rotate'}
882 ),
883 ' Loading...'
884 )
885 :
886 el.input(
887 {
888 id: 'chat-input',
889 className: 'form-control',
890 type: 'text',
891 value: this.state.text,
892 placeholder: worldStore.state.user ?
893 'Click here and begin typing...' :
894 'Login to chat',
895 onChange: this._onChange,
896 onKeyPress: this._onKeyPress,
897 onFocus: this._onFocus,
898 ref: 'input',
899 // TODO: disable while fetching messages
900 disabled: !worldStore.state.user || chatStore.state.loadingInitialMessages
901 }
902 )
903 ),
904 el.div(
905 {className: 'col-md-3'},
906 el.button(
907 {
908 type: 'button',
909 className: 'btn btn-default btn-block',
910 disabled: !worldStore.state.user ||
911 chatStore.state.waitingForServer ||
912 this.state.text.trim().length === 0,
913 onClick: this._onSend
914 },
915 'Send'
916 )
917 )
918 )
919 );
920 }
921});
922
923var ChatUserList = React.createClass({
924 displayName: 'ChatUserList',
925 render: function() {
926 return (
927 el.div(
928 {className: 'panel panel-default'},
929 el.div(
930 {className: 'panel-heading'},
931 'UserList'
932 ),
933 el.div(
934 {className: 'panel-body'},
935 el.ul(
936 {},
937 _.values(chatStore.state.userList).map(function(u) {
938 return el.li(
939 {
940 key: u.uname
941 },
942 helpers.roleToLabelElement(u.role),
943 ' ' + u.uname
944 );
945 })
946 )
947 )
948 )
949 );
950 }
951});
952
953var ChatBox = React.createClass({
954 displayName: 'ChatBox',
955 _onStoreChange: function() {
956 this.forceUpdate();
957 },
958 // New messages should only force scroll if user is scrolled near the bottom
959 // already. This allows users to scroll back to earlier convo without being
960 // forced to scroll to bottom when new messages arrive
961 _onNewMessage: function() {
962 var node = this.refs.chatListRef.getDOMNode();
963
964 // Only scroll if user is within 100 pixels of last message
965 var shouldScroll = function() {
966 var distanceFromBottom = node.scrollHeight - ($(node).scrollTop() + $(node).innerHeight());
967 console.log('DistanceFromBottom:', distanceFromBottom);
968 return distanceFromBottom <= 100;
969 };
970
971 if (shouldScroll()) {
972 this._scrollChat();
973 }
974 },
975 _scrollChat: function() {
976 var node = this.refs.chatListRef.getDOMNode();
977 $(node).scrollTop(node.scrollHeight);
978 },
979 componentDidMount: function() {
980 chatStore.on('change', this._onStoreChange);
981 chatStore.on('new_message', this._onNewMessage);
982 chatStore.on('init', this._scrollChat);
983 },
984 componentWillUnmount: function() {
985 chatStore.off('change', this._onStoreChange);
986 chatStore.off('new_message', this._onNewMessage);
987 chatStore.off('init', this._scrollChat);
988 },
989 //
990 _onUserListToggle: function() {
991 Dispatcher.sendAction('TOGGLE_CHAT_USERLIST');
992 },
993 render: function() {
994 return el.div(
995 {id: 'chat-box'},
996 el.div(
997 {className: 'panel panel-default'},
998 el.div(
999 {className: 'panel-body'},
1000 el.ul(
1001 {className: 'chat-list list-unstyled', ref: 'chatListRef'},
1002 chatStore.state.messages.toArray().map(function(m) {
1003 return el.li(
1004 {
1005 // Use message id as unique key
1006 key: m.id
1007 },
1008 el.span(
1009 {
1010 style: {
1011 fontFamily: 'monospace'
1012 }
1013 },
1014 helpers.formatDateToTime(m.created_at),
1015 ' '
1016 ),
1017 m.user ? helpers.roleToLabelElement(m.user.role) : '',
1018 m.user ? ' ' : '',
1019 el.code(
1020 null,
1021 m.user ?
1022 // If chat message:
1023 m.user.uname :
1024 // If system message:
1025 'SYSTEM :: ' + m.text
1026 ),
1027 m.user ?
1028 // If chat message
1029 el.span(null, ' ' + m.text) :
1030 // If system message
1031 ''
1032 );
1033 })
1034 )
1035 ),
1036 el.div(
1037 {className: 'panel-footer'},
1038 React.createElement(ChatBoxInput, null)
1039 )
1040 ),
1041 // After the chatbox panel
1042 el.p(
1043 {
1044 className: 'text-right text-muted',
1045 style: { marginTop: '-15px' }
1046 },
1047 'Users online: ' + Object.keys(chatStore.state.userList).length + ' ',
1048 // Show/Hide userlist button
1049 el.button(
1050 {
1051 className: 'btn btn-default btn-xs',
1052 onClick: this._onUserListToggle
1053 },
1054 chatStore.state.showUserList ? 'Hide' : 'Show'
1055 )
1056 ),
1057 // Show userlist
1058 chatStore.state.showUserList ? React.createElement(ChatUserList, null) : ''
1059 );
1060 }
1061});
1062
1063var BetBoxChance = React.createClass({
1064 displayName: 'BetBoxChance',
1065 // Hookup to stores
1066 _onStoreChange: function() {
1067 this.forceUpdate();
1068 },
1069 componentDidMount: function() {
1070 betStore.on('change', this._onStoreChange);
1071 worldStore.on('change', this._onStoreChange);
1072 },
1073 componentWillUnmount: function() {
1074 betStore.off('change', this._onStoreChange);
1075 worldStore.off('change', this._onStoreChange);
1076 },
1077 //
1078 render: function() {
1079 // 0.00 to 1.00
1080 var winProb = helpers.multiplierToWinProb(betStore.state.multiplier.num);
1081
1082 var isError = betStore.state.multiplier.error || betStore.state.wager.error;
1083
1084 // Just show '--' if chance can't be calculated
1085 var innerNode;
1086 if (isError) {
1087 innerNode = el.span(
1088 {className: 'lead'},
1089 ' --'
1090 );
1091 } else {
1092 innerNode = el.span(
1093 {className: 'lead'},
1094 ' ' + (winProb * 100).toFixed(2).toString() + '%'
1095 );
1096 }
1097
1098 return el.div(
1099 {},
1100 el.span(
1101 {className: 'lead', style: { fontWeight: 'bold' }},
1102 'Chance:'
1103 ),
1104 innerNode
1105 );
1106 }
1107});
1108
1109var BetBoxProfit = React.createClass({
1110 displayName: 'BetBoxProfit',
1111 // Hookup to stores
1112 _onStoreChange: function() {
1113 this.forceUpdate();
1114 },
1115 componentDidMount: function() {
1116 betStore.on('change', this._onStoreChange);
1117 worldStore.on('change', this._onStoreChange);
1118 },
1119 componentWillUnmount: function() {
1120 betStore.off('change', this._onStoreChange);
1121 worldStore.off('change', this._onStoreChange);
1122 },
1123 //
1124 render: function() {
1125 var profit = betStore.state.wager.num * (betStore.state.multiplier.num - 1);
1126
1127 var innerNode;
1128 if (betStore.state.multiplier.error || betStore.state.wager.error) {
1129 innerNode = el.span(
1130 {className: 'lead'},
1131 '--'
1132 );
1133 } else {
1134 innerNode = el.span(
1135 {
1136 className: 'lead',
1137 style: { color: '#39b54a' }
1138 },
1139 '+' + profit.toFixed(2)
1140 );
1141 }
1142
1143 return el.div(
1144 null,
1145 el.span(
1146 {className: 'lead', style: { fontWeight: 'bold' }},
1147 'Profit: '
1148 ),
1149 innerNode
1150 );
1151 }
1152});
1153
1154var BetBoxMultiplier = React.createClass({
1155 displayName: 'BetBoxMultiplier',
1156 // Hookup to stores
1157 _onStoreChange: function() {
1158 this.forceUpdate();
1159 },
1160 componentDidMount: function() {
1161 betStore.on('change', this._onStoreChange);
1162 worldStore.on('change', this._onStoreChange);
1163 },
1164 componentWillUnmount: function() {
1165 betStore.off('change', this._onStoreChange);
1166 worldStore.off('change', this._onStoreChange);
1167 },
1168 //
1169 _validateMultiplier: function(newStr) {
1170 var num = parseFloat(newStr, 10);
1171
1172 // If num is a number, ensure it's at least 0.01x
1173 // if (Number.isFinite(num)) {
1174 // num = Math.max(num, 0.01);
1175 // this.props.currBet.setIn(['multiplier', 'str'], num.toString());
1176 // }
1177
1178 var isFloatRegexp = /^(\d*\.)?\d+$/;
1179
1180 // Ensure str is a number
1181 if (isNaN(num) || !isFloatRegexp.test(newStr)) {
1182 Dispatcher.sendAction('UPDATE_MULTIPLIER', { error: 'INVALID_MULTIPLIER' });
1183 // Ensure multiplier is >= 1.00x
1184 } else if (num < 1.01) {
1185 Dispatcher.sendAction('UPDATE_MULTIPLIER', { error: 'MULTIPLIER_TOO_LOW' });
1186 // Ensure multiplier is <= max allowed multiplier (100x for now)
1187 } else if (num > 9900) {
1188 Dispatcher.sendAction('UPDATE_MULTIPLIER', { error: 'MULTIPLIER_TOO_HIGH' });
1189 // Ensure no more than 2 decimal places of precision
1190 } else if (helpers.getPrecision(num) > 2) {
1191 Dispatcher.sendAction('UPDATE_MULTIPLIER', { error: 'MULTIPLIER_TOO_PRECISE' });
1192 // multiplier str is valid
1193 } else {
1194 Dispatcher.sendAction('UPDATE_MULTIPLIER', {
1195 num: num,
1196 error: null
1197 });
1198 }
1199 },
1200 _onMultiplierChange: function(e) {
1201 console.log('Multiplier changed');
1202 var str = e.target.value;
1203 console.log('You entered', str, 'as your multiplier');
1204 Dispatcher.sendAction('UPDATE_MULTIPLIER', { str: str });
1205 this._validateMultiplier(str);
1206 },
1207 render: function() {
1208 return el.div(
1209 {className: 'form-group'},
1210 el.p(
1211 {className: 'lead'},
1212 el.strong(
1213 {
1214 style: betStore.state.multiplier.error ? { color: 'red' } : {}
1215 },
1216 'Multiplier:')
1217 ),
1218 el.div(
1219 {className: 'input-group'},
1220 el.input(
1221 {
1222 type: 'text',
1223 value: betStore.state.multiplier.str,
1224 className: 'form-control input-lg',
1225 onChange: this._onMultiplierChange,
1226 disabled: !!worldStore.state.isLoading
1227 }
1228 ),
1229 el.span(
1230 {className: 'input-group-addon'},
1231 'x'
1232 )
1233 )
1234 );
1235 }
1236});
1237
1238var BetBoxWager = React.createClass({
1239 displayName: 'BetBoxWager',
1240 // Hookup to stores
1241 _onStoreChange: function() {
1242 this.forceUpdate();
1243 },
1244 _onBalanceChange: function() {
1245 // Force validation when user logs in
1246 // TODO: Re-force it when user refreshes
1247 Dispatcher.sendAction('UPDATE_WAGER', {});
1248 },
1249 componentDidMount: function() {
1250 betStore.on('change', this._onStoreChange);
1251 worldStore.on('change', this._onStoreChange);
1252 worldStore.on('user_update', this._onBalanceChange);
1253 },
1254 componentWillUnmount: function() {
1255 betStore.off('change', this._onStoreChange);
1256 worldStore.off('change', this._onStoreChange);
1257 worldStore.off('user_update', this._onBalanceChange);
1258 },
1259 _onWagerChange: function(e) {
1260 var str = e.target.value;
1261 Dispatcher.sendAction('UPDATE_WAGER', { str: str });
1262 },
1263 _onHalveWager: function() {
1264 var newWager = Math.round(betStore.state.wager.num / 2);
1265 Dispatcher.sendAction('UPDATE_WAGER', { str: newWager.toString() });
1266 },
1267 _onDoubleWager: function() {
1268 var n = betStore.state.wager.num * 2;
1269 Dispatcher.sendAction('UPDATE_WAGER', { str: n.toString() });
1270
1271 },
1272 _onMaxWager: function() {
1273 // If user is logged in, use their balance as max wager
1274 var balanceBits;
1275 if (worldStore.state.user) {
1276 balanceBits = Math.floor(worldStore.state.user.balance / 100);
1277 } else {
1278 balanceBits = 42000;
1279 }
1280 Dispatcher.sendAction('UPDATE_WAGER', { str: balanceBits.toString() });
1281 },
1282 //
1283 render: function() {
1284 var style1 = { borderBottomLeftRadius: '0', borderBottomRightRadius: '0' };
1285 var style2 = { borderTopLeftRadius: '0' };
1286 var style3 = { borderTopRightRadius: '0' };
1287 return el.div(
1288 {className: 'form-group'},
1289 el.p(
1290 {className: 'lead'},
1291 el.strong(
1292 // If wagerError, make the label red
1293 betStore.state.wager.error ? { style: {color: 'red'} } : null,
1294 'Wager:')
1295 ),
1296 el.input(
1297 {
1298 value: betStore.state.wager.str,
1299 type: 'text',
1300 className: 'form-control input-lg',
1301 style: style1,
1302 onChange: this._onWagerChange,
1303 disabled: !!worldStore.state.isLoading,
1304 placeholder: 'Bits'
1305 }
1306 ),
1307 el.div(
1308 {className: 'btn-group btn-group-justified'},
1309 el.div(
1310 {className: 'btn-group'},
1311 el.button(
1312 {
1313 className: 'btn btn-default btn-md',
1314 type: 'button',
1315 style: style2,
1316 onClick: this._onHalveWager
1317 },
1318 '1/2x ', worldStore.state.hotkeysEnabled ? el.kbd(null, 'X') : ''
1319 )
1320 ),
1321 el.div(
1322 {className: 'btn-group'},
1323 el.button(
1324 {
1325 className: 'btn btn-default btn-md',
1326 type: 'button',
1327 onClick: this._onDoubleWager
1328 },
1329 '2x ', worldStore.state.hotkeysEnabled ? el.kbd(null, 'C') : ''
1330 )
1331 ),
1332 el.div(
1333 {className: 'btn-group'},
1334 el.button(
1335 {
1336 className: 'btn btn-default btn-md',
1337 type: 'button',
1338 style: style3,
1339 onClick: this._onMaxWager
1340 },
1341 'Max'
1342 )
1343 )
1344 )
1345 );
1346 }
1347});
1348
1349var BetBoxButton = React.createClass({
1350 displayName: 'BetBoxButton',
1351 _onStoreChange: function() {
1352 this.forceUpdate();
1353 },
1354 componentDidMount: function() {
1355 worldStore.on('change', this._onStoreChange);
1356 betStore.on('change', this._onStoreChange);
1357 },
1358 componentWillUnmount: function() {
1359 worldStore.off('change', this._onStoreChange);
1360 betStore.off('change', this._onStoreChange);
1361 },
1362 getInitialState: function() {
1363 return { waitingForServer: false };
1364 },
1365 // cond is '>' or '<'
1366 _makeBetHandler: function(cond) {
1367 var self = this;
1368
1369 console.assert(cond === '<' || cond === '>');
1370
1371 return function(e) {
1372 console.log('Placing bet...');
1373
1374 // Indicate that we are waiting for server response
1375 self.setState({ waitingForServer: true });
1376
1377 var hash = betStore.state.nextHash;
1378 console.assert(typeof hash === 'string');
1379
1380 var wagerSatoshis = betStore.state.wager.num * 100;
1381 var multiplier = betStore.state.multiplier.num;
1382 var payoutSatoshis = wagerSatoshis * multiplier;
1383
1384 var number = helpers.calcNumber(
1385 cond, helpers.multiplierToWinProb(multiplier)
1386 );
1387
1388 var params = {
1389 wager: wagerSatoshis,
1390 client_seed: 0, // TODO
1391 hash: hash,
1392 cond: cond,
1393 target: number,
1394 payout: payoutSatoshis
1395 };
1396
1397 MoneyPot.placeSimpleDiceBet(params, {
1398 success: function(bet) {
1399 console.log('Successfully placed bet:', bet);
1400 // Append to bet list
1401
1402 // We don't get this info from the API, so assoc it for our use
1403 bet.meta = {
1404 cond: cond,
1405 number: number,
1406 hash: hash,
1407 isFair: CryptoJS.SHA256(bet.secret + '|' + bet.salt).toString() === hash
1408 };
1409
1410 // Sync up with the bets we get from socket
1411 bet.wager = wagerSatoshis;
1412 bet.uname = worldStore.state.user.uname;
1413
1414 Dispatcher.sendAction('NEW_BET', bet);
1415
1416 // Update next bet hash
1417 Dispatcher.sendAction('SET_NEXT_HASH', bet.next_hash);
1418
1419 // Update user balance
1420 Dispatcher.sendAction('UPDATE_USER', {
1421 balance: worldStore.state.user.balance + bet.profit
1422 });
1423 },
1424 error: function(xhr) {
1425 console.log('Error');
1426 if (xhr.responseJSON && xhr.responseJSON) {
1427 alert(xhr.responseJSON.error);
1428 } else {
1429 alert('Internal Error');
1430 }
1431 },
1432 complete: function() {
1433 self.setState({ waitingForServer: false });
1434 // Force re-validation of wager
1435 Dispatcher.sendAction('UPDATE_WAGER', {
1436 str: betStore.state.wager.str
1437 });
1438 }
1439 });
1440 };
1441 },
1442 render: function() {
1443 var innerNode;
1444
1445 // TODO: Create error prop for each input
1446 var error = betStore.state.wager.error || betStore.state.multiplier.error;
1447
1448 if (worldStore.state.isLoading) {
1449 // If app is loading, then just disable button until state change
1450 innerNode = el.button(
1451 {type: 'button', disabled: true, className: 'btn btn-lg btn-block btn-default'},
1452 'Loading...'
1453 );
1454 } else if (error) {
1455 // If there's a betbox error, then render button in error state
1456
1457 var errorTranslations = {
1458 'CANNOT_AFFORD_WAGER': 'You cannot afford wager',
1459 'INVALID_WAGER': 'Invalid wager',
1460 'INVALID_MULTIPLIER': 'Invalid multiplier',
1461 'MULTIPLIER_TOO_PRECISE': 'Multiplier too precise',
1462 'MULTIPLIER_TOO_HIGH': 'Multiplier too high',
1463 'MULTIPLIER_TOO_LOW': 'Multiplier too low'
1464 };
1465
1466 innerNode = el.button(
1467 {type: 'button',
1468 disabled: true,
1469 className: 'btn btn-lg btn-block btn-danger'},
1470 errorTranslations[error] || 'Invalid bet'
1471 );
1472 } else if (worldStore.state.user) {
1473 // If user is logged in, let them submit bet
1474 innerNode =
1475 el.div(
1476 {className: 'row'},
1477 // bet hi
1478 el.div(
1479 {className: 'col-xs-6'},
1480 el.button(
1481 {
1482 id: 'bet-hi',
1483 type: 'button',
1484 className: 'btn btn-lg btn-primary btn-block',
1485 onClick: this._makeBetHandler('>'),
1486 disabled: !!this.state.waitingForServer
1487 },
1488 'Bet Hi ', worldStore.state.hotkeysEnabled ? el.kbd(null, 'H') : ''
1489 )
1490 ),
1491 // bet lo
1492 el.div(
1493 {className: 'col-xs-6'},
1494 el.button(
1495 {
1496 id: 'bet-lo',
1497 type: 'button',
1498 className: 'btn btn-lg btn-primary btn-block',
1499 onClick: this._makeBetHandler('<'),
1500 disabled: !!this.state.waitingForServer
1501 },
1502 'Bet Lo ', worldStore.state.hotkeysEnabled ? el.kbd(null, 'L') : ''
1503 )
1504 )
1505 );
1506 } else {
1507 // If user isn't logged in, give them link to /oauth/authorize
1508 innerNode = el.a(
1509 {
1510 href: config.mp_browser_uri + '/oauth/authorize' +
1511 '?app_id=' + config.app_id +
1512 '&redirect_uri=' + config.redirect_uri,
1513 className: 'btn btn-lg btn-block btn-success'
1514 },
1515 'Login with MoneyPot'
1516 );
1517 }
1518
1519 return el.div(
1520 null,
1521 el.div(
1522 {className: 'col-md-2',},
1523 (this.state.waitingForServer) ?
1524 el.span(
1525 {
1526 className: 'glyphicon glyphicon-refresh rotate',
1527 style: { marginTop: '15px' }
1528 }
1529 ) : ''
1530 ),
1531 el.div(
1532 {className: 'col-md-8'},
1533 innerNode
1534 )
1535 );
1536 }
1537});
1538
1539var HotkeyToggle = React.createClass({
1540 displayName: 'HotkeyToggle',
1541 _onClick: function() {
1542 Dispatcher.sendAction('TOGGLE_HOTKEYS');
1543 },
1544 render: function() {
1545 return (
1546 el.div(
1547 {className: 'text-center'},
1548 el.button(
1549 {
1550 type: 'button',
1551 className: 'btn btn-default btn-sm',
1552 onClick: this._onClick,
1553 style: { marginTop: '-15px' }
1554 },
1555 'Hotkeys: ',
1556 worldStore.state.hotkeysEnabled ?
1557 el.span({className: 'label label-success'}, 'ON') :
1558 el.span({className: 'label label-default'}, 'OFF')
1559 )
1560 )
1561 );
1562 }
1563});
1564
1565var BetBox = React.createClass({
1566 displayName: 'BetBox',
1567 _onStoreChange: function() {
1568 this.forceUpdate();
1569 },
1570 componentDidMount: function() {
1571 worldStore.on('change', this._onStoreChange);
1572 },
1573 componentWillUnmount: function() {
1574 worldStore.off('change', this._onStoreChange);
1575 },
1576 render: function() {
1577 return el.div(
1578 null,
1579 el.div(
1580 {className: 'panel panel-default'},
1581 el.div(
1582 {className: 'panel-body'},
1583 el.div(
1584 {className: 'row'},
1585 el.div(
1586 {className: 'col-xs-6'},
1587 React.createElement(BetBoxWager, null)
1588 ),
1589 el.div(
1590 {className: 'col-xs-6'},
1591 React.createElement(BetBoxMultiplier, null)
1592 ),
1593 // HR
1594 el.div(
1595 {className: 'row'},
1596 el.div(
1597 {className: 'col-xs-12'},
1598 el.hr(null)
1599 )
1600 ),
1601 // Bet info bar
1602 el.div(
1603 null,
1604 el.div(
1605 {className: 'col-sm-6'},
1606 React.createElement(BetBoxProfit, null)
1607 ),
1608 el.div(
1609 {className: 'col-sm-6'},
1610 React.createElement(BetBoxChance, null)
1611 )
1612 )
1613 )
1614 ),
1615 el.div(
1616 {className: 'panel-footer clearfix'},
1617 React.createElement(BetBoxButton, null)
1618 )
1619 ),
1620 React.createElement(HotkeyToggle, null)
1621 );
1622 }
1623});
1624
1625var Tabs = React.createClass({
1626 displayName: 'Tabs',
1627 _onStoreChange: function() {
1628 this.forceUpdate();
1629 },
1630 componentDidMount: function() {
1631 worldStore.on('change', this._onStoreChange);
1632 },
1633 componentWillUnmount: function() {
1634 worldStore.off('change', this._onStoreChange);
1635 },
1636 _makeTabChangeHandler: function(tabName) {
1637 var self = this;
1638 return function() {
1639 Dispatcher.sendAction('CHANGE_TAB', tabName);
1640 };
1641 },
1642 render: function() {
1643 return el.ul(
1644 {className: 'nav nav-tabs'},
1645 el.li(
1646 {className: worldStore.state.currTab === 'ALL_BETS' ? 'active' : ''},
1647 el.a(
1648 {
1649 href: 'javascript:void(0)',
1650 onClick: this._makeTabChangeHandler('ALL_BETS')
1651 },
1652 'All Bets'
1653 )
1654 ),
1655 // Only show MY BETS tab if user is logged in
1656 !worldStore.state.user ? '' :
1657 el.li(
1658 {className: worldStore.state.currTab === 'MY_BETS' ? 'active' : ''},
1659 el.a(
1660 {
1661 href: 'javascript:void(0)',
1662 onClick: this._makeTabChangeHandler('MY_BETS')
1663 },
1664 'My Bets'
1665 )
1666 ),
1667 // Display faucet tab even to guests so that they're aware that
1668 // this casino has one.
1669 !config.recaptcha_sitekey ? '' :
1670 el.li(
1671 {className: worldStore.state.currTab === 'FAUCET' ? 'active' : ''},
1672 el.a(
1673 {
1674 href: 'javascript:void(0)',
1675 onClick: this._makeTabChangeHandler('FAUCET')
1676 },
1677 el.span(null, 'Faucet ')
1678 )
1679 )
1680 );
1681 }
1682});
1683
1684var MyBetsTabContent = React.createClass({
1685 displayName: 'MyBetsTabContent',
1686 _onStoreChange: function() {
1687 this.forceUpdate();
1688 },
1689 componentDidMount: function() {
1690 worldStore.on('change', this._onStoreChange);
1691 },
1692 componentWillUnmount: function() {
1693 worldStore.off('change', this._onStoreChange);
1694 },
1695 render: function() {
1696 return el.div(
1697 null,
1698 el.table(
1699 {className: 'table'},
1700 el.thead(
1701 null,
1702 el.tr(
1703 null,
1704 el.th(null, 'ID'),
1705 el.th(null, 'Time'),
1706 el.th(null, 'User'),
1707 el.th(null, 'Wager'),
1708 el.th(null, 'Target'),
1709 el.th(null, 'Roll'),
1710 el.th(null, 'Profit')
1711 )
1712 ),
1713 el.tbody(
1714 null,
1715 worldStore.state.bets.toArray().map(function(bet) {
1716 return el.tr(
1717 {
1718 key: bet.bet_id || bet.id
1719 },
1720 // bet id
1721 el.td(
1722 null,
1723 el.a(
1724 {
1725 href: config.mp_browser_uri + '/bets/' + (bet.bet_id || bet.id),
1726 target: '_blank'
1727 },
1728 bet.bet_id || bet.id
1729 )
1730 ),
1731 // Time
1732 el.td(
1733 null,
1734 helpers.formatDateToTime(bet.created_at)
1735 ),
1736 // User
1737 el.td(
1738 null,
1739 el.a(
1740 {
1741 href: config.mp_browser_uri + '/users/' + bet.uname,
1742 target: '_blank'
1743 },
1744 bet.uname
1745 )
1746 ),
1747 // wager
1748 el.td(
1749 null,
1750 helpers.round10(bet.wager/100, -2),
1751 ' bits'
1752 ),
1753 // target
1754 el.td(
1755 null,
1756 bet.meta.cond + ' ' + bet.meta.number.toFixed(2)
1757 ),
1758 // roll
1759 el.td(
1760 null,
1761 bet.outcome + ' ',
1762 bet.meta.isFair ?
1763 el.span(
1764 {className: 'label label-success'}, 'Verified') : ''
1765 ),
1766 // profit
1767 el.td(
1768 {style: {color: bet.profit > 0 ? 'green' : 'red'}},
1769 bet.profit > 0 ?
1770 '+' + helpers.round10(bet.profit/100, -2) :
1771 helpers.round10(bet.profit/100, -2),
1772 ' bits'
1773 )
1774 );
1775 }).reverse()
1776 )
1777 )
1778 );
1779 }
1780});
1781
1782var FaucetTabContent = React.createClass({
1783 displayName: 'FaucetTabContent',
1784 getInitialState: function() {
1785 return {
1786 // SHOW_RECAPTCHA | SUCCESSFULLY_CLAIM | ALREADY_CLAIMED | WAITING_FOR_SERVER
1787 faucetState: 'SHOW_RECAPTCHA',
1788 // :: Integer that's updated after the claim from the server so we
1789 // can show user how much the claim was worth without hardcoding it
1790 // - It will be in satoshis
1791 claimAmount: undefined
1792 };
1793 },
1794 // This function is extracted so that we can call it on update and mount
1795 // when the window.grecaptcha instance loads
1796 _renderRecaptcha: function() {
1797 worldStore.state.grecaptcha.render(
1798 'recaptcha-target',
1799 {
1800 sitekey: config.recaptcha_sitekey,
1801 callback: this._onRecaptchaSubmit
1802 }
1803 );
1804 },
1805 // `response` is the g-recaptcha-response returned from google
1806 _onRecaptchaSubmit: function(response) {
1807 var self = this;
1808 console.log('recaptcha submitted: ', response);
1809
1810 self.setState({ faucetState: 'WAITING_FOR_SERVER' });
1811
1812 MoneyPot.claimFaucet(response, {
1813 // `data` is { claim_id: Int, amount: Satoshis }
1814 success: function(data) {
1815 Dispatcher.sendAction('UPDATE_USER', {
1816 balance: worldStore.state.user.balance + data.amount
1817 });
1818 self.setState({
1819 faucetState: 'SUCCESSFULLY_CLAIMED',
1820 claimAmount: data.amount
1821 });
1822 // self.props.faucetClaimedAt.update(function() {
1823 // return new Date();
1824 // });
1825 },
1826 error: function(xhr, textStatus, errorThrown) {
1827 if (xhr.responseJSON && xhr.responseJSON.error === 'FAUCET_ALREADY_CLAIMED') {
1828 self.setState({ faucetState: 'ALREADY_CLAIMED' });
1829 }
1830 }
1831 });
1832 },
1833 // This component will mount before window.grecaptcha is loaded if user
1834 // clicks the Faucet tab before the recaptcha.js script loads, so don't assume
1835 // we have a grecaptcha instance
1836 componentDidMount: function() {
1837 if (worldStore.state.grecaptcha) {
1838 this._renderRecaptcha();
1839 }
1840
1841 worldStore.on('grecaptcha_loaded', this._renderRecaptcha);
1842 },
1843 componentWillUnmount: function() {
1844 worldStore.off('grecaptcha_loaded', this._renderRecaptcha);
1845 },
1846 render: function() {
1847
1848 // If user is not logged in, let them know only logged-in users can claim
1849 if (!worldStore.state.user) {
1850 return el.p(
1851 {className: 'lead'},
1852 'You must login to claim faucet'
1853 );
1854 }
1855
1856 var innerNode;
1857 // SHOW_RECAPTCHA | SUCCESSFULLY_CLAIMED | ALREADY_CLAIMED | WAITING_FOR_SERVER
1858 switch(this.state.faucetState) {
1859 case 'SHOW_RECAPTCHA':
1860 innerNode = el.div(
1861 { id: 'recaptcha-target' },
1862 !!worldStore.state.grecaptcha ? '' : 'Loading...'
1863 );
1864 break;
1865 case 'SUCCESSFULLY_CLAIMED':
1866 innerNode = el.div(
1867 null,
1868 'Successfully claimed ' + this.state.claimAmount/100 + ' bits.' +
1869 // TODO: What's the real interval?
1870 ' You can claim again in 5 minutes.'
1871 );
1872 break;
1873 case 'ALREADY_CLAIMED':
1874 innerNode = el.div(
1875 null,
1876 'ALREADY_CLAIMED'
1877 );
1878 break;
1879 case 'WAITING_FOR_SERVER':
1880 innerNode = el.div(
1881 null,
1882 'WAITING_FOR_SERVER'
1883 );
1884 break;
1885 default:
1886 alert('Unhandled faucet state');
1887 return;
1888 }
1889
1890 return el.div(
1891 null,
1892 innerNode
1893 );
1894 }
1895});
1896
1897// props: { bet: Bet }
1898var BetRow = React.createClass({
1899 displayName: 'BetRow',
1900 render: function() {
1901 var bet = this.props.bet;
1902 return el.tr(
1903 {},
1904 // bet id
1905 el.td(
1906 null,
1907 el.a(
1908 {
1909 href: config.mp_browser_uri + '/bets/' + (bet.bet_id || bet.id),
1910 target: '_blank'
1911 },
1912 bet.bet_id || bet.id
1913 )
1914 ),
1915 // Time
1916 el.td(
1917 null,
1918 helpers.formatDateToTime(bet.created_at)
1919 ),
1920 // User
1921 el.td(
1922 null,
1923 el.a(
1924 {
1925 href: config.mp_browser_uri + '/users/' + bet.uname,
1926 target: '_blank'
1927 },
1928 bet.uname
1929 )
1930 ),
1931 // Wager
1932 el.td(
1933 null,
1934 helpers.round10(bet.wager/100, -2),
1935 ' bits'
1936 ),
1937 // Target
1938 el.td(
1939 {
1940 className: 'text-right',
1941 style: {
1942 fontFamily: 'monospace'
1943 }
1944 },
1945 bet.cond + bet.target.toFixed(2)
1946 ),
1947 // // Roll
1948 // el.td(
1949 // null,
1950 // bet.outcome
1951 // ),
1952 // Visual
1953 el.td(
1954 {
1955 style: {
1956 //position: 'relative'
1957 fontFamily: 'monospace'
1958 }
1959 },
1960 // progress bar container
1961 el.div(
1962 {
1963 className: 'progress',
1964 style: {
1965 minWidth: '100px',
1966 position: 'relative',
1967 marginBottom: 0,
1968 // make it thinner than default prog bar
1969 height: '10px'
1970 }
1971 },
1972 el.div(
1973 {
1974 className: 'progress-bar ' +
1975 (bet.profit >= 0 ?
1976 'progress-bar-success' : 'progress-bar-grey') ,
1977 style: {
1978 float: bet.cond === '<' ? 'left' : 'right',
1979 width: bet.cond === '<' ?
1980 bet.target.toString() + '%' :
1981 (100 - bet.target).toString() + '%'
1982 }
1983 }
1984 ),
1985 el.div(
1986 {
1987 style: {
1988 position: 'absolute',
1989 left: 0,
1990 top: 0,
1991 width: bet.outcome.toString() + '%',
1992 borderRight: '3px solid #333',
1993 height: '100%'
1994 }
1995 }
1996 )
1997 ),
1998 // arrow container
1999 el.div(
2000 {
2001 style: {
2002 position: 'relative',
2003 width: '100%',
2004 height: '15px'
2005 }
2006 },
2007 // arrow
2008 el.div(
2009 {
2010 style: {
2011 position: 'absolute',
2012 top: 0,
2013 left: (bet.outcome - 1).toString() + '%'
2014 }
2015 },
2016 el.div(
2017 {
2018 style: {
2019 width: '5em',
2020 marginLeft: '-10px'
2021 }
2022 },
2023 // el.span(
2024 // //{className: 'glyphicon glyphicon-triangle-top'}
2025 // {className: 'glyphicon glyphicon-arrow-up'}
2026 // ),
2027 el.span(
2028 {style: {fontFamily: 'monospace'}},
2029 '' + bet.outcome
2030 )
2031 )
2032 )
2033 )
2034 ),
2035 // Profit
2036 el.td(
2037 {
2038 style: {
2039 color: bet.profit > 0 ? 'green' : 'red',
2040 paddingLeft: '50px'
2041 }
2042 },
2043 bet.profit > 0 ?
2044 '+' + helpers.round10(bet.profit/100, -2) :
2045 helpers.round10(bet.profit/100, -2),
2046 ' bits'
2047 )
2048 );
2049 }
2050});
2051
2052var AllBetsTabContent = React.createClass({
2053 displayName: 'AllBetsTabContent',
2054 _onStoreChange: function() {
2055 this.forceUpdate();
2056 },
2057 componentDidMount: function() {
2058 worldStore.on('change', this._onStoreChange);
2059 },
2060 componentWillUnmount: function() {
2061 worldStore.off('change', this._onStoreChange);
2062 },
2063 render: function() {
2064 return el.div(
2065 null,
2066 el.table(
2067 {className: 'table'},
2068 el.thead(
2069 null,
2070 el.tr(
2071 null,
2072 el.th(null, 'ID'),
2073 el.th(null, 'Time'),
2074 el.th(null, 'User'),
2075 el.th(null, 'Wager'),
2076 el.th({className: 'text-right'}, 'Target'),
2077 // el.th(null, 'Roll'),
2078 el.th(null, 'Outcome'),
2079 el.th(
2080 {
2081 style: {
2082 paddingLeft: '50px'
2083 }
2084 },
2085 'Profit'
2086 )
2087 )
2088 ),
2089 el.tbody(
2090 null,
2091 worldStore.state.allBets.toArray().map(function(bet) {
2092 return React.createElement(BetRow, { bet: bet, key: bet.bet_id || bet.id });
2093 }).reverse()
2094 )
2095 )
2096 );
2097 }
2098});
2099
2100var TabContent = React.createClass({
2101 displayName: 'TabContent',
2102 _onStoreChange: function() {
2103 this.forceUpdate();
2104 },
2105 componentDidMount: function() {
2106 worldStore.on('change', this._onStoreChange);
2107 },
2108 componentWillUnmount: function() {
2109 worldStore.off('change', this._onStoreChange);
2110 },
2111 render: function() {
2112 switch(worldStore.state.currTab) {
2113 case 'FAUCET':
2114 return React.createElement(FaucetTabContent, null);
2115 case 'MY_BETS':
2116 return React.createElement(MyBetsTabContent, null);
2117 case 'ALL_BETS':
2118 return React.createElement(AllBetsTabContent, null);
2119 default:
2120 alert('Unsupported currTab value: ', worldStore.state.currTab);
2121 break;
2122 }
2123 }
2124});
2125
2126var Footer = React.createClass({
2127 displayName: 'Footer',
2128 render: function() {
2129 return el.div(
2130 {
2131 className: 'text-center text-muted',
2132 style: {
2133 marginTop: '200px'
2134 }
2135 },
2136 'Powered by ',
2137 el.a(
2138 {
2139 href: 'https://www.moneypot.com'
2140 },
2141 'Moneypot'
2142 )
2143 );
2144 }
2145});
2146
2147var App = React.createClass({
2148 displayName: 'App',
2149 render: function() {
2150 return el.div(
2151 {className: 'container'},
2152 // Navbar
2153 React.createElement(Navbar, null),
2154 // BetBox & ChatBox
2155 el.div(
2156 {className: 'row'},
2157 el.div(
2158 {className: 'col-sm-5'},
2159 React.createElement(BetBox, null)
2160 ),
2161 el.div(
2162 {className: 'col-sm-7'},
2163 React.createElement(ChatBox, null)
2164 )
2165 ),
2166 // Tabs
2167 el.div(
2168 {style: {marginTop: '15px'}},
2169 React.createElement(Tabs, null)
2170 ),
2171 // Tab Contents
2172 React.createElement(TabContent, null),
2173 // Footer
2174 React.createElement(Footer, null)
2175 );
2176 }
2177});
2178
2179React.render(
2180 React.createElement(App, null),
2181 document.getElementById('app')
2182);
2183
2184// If not accessToken,
2185// If accessToken, then
2186if (!worldStore.state.accessToken) {
2187 Dispatcher.sendAction('STOP_LOADING');
2188 connectToChatServer();
2189} else {
2190 // Load user from accessToken
2191 MoneyPot.getTokenInfo({
2192 success: function(data) {
2193 console.log('Successfully loaded user from tokens endpoint', data);
2194 var user = data.auth.user;
2195 Dispatcher.sendAction('USER_LOGIN', user);
2196 },
2197 error: function(err) {
2198 console.log('Error:', err);
2199 },
2200 complete: function() {
2201 Dispatcher.sendAction('STOP_LOADING');
2202 connectToChatServer();
2203 }
2204 });
2205 // Get next bet hash
2206 MoneyPot.generateBetHash({
2207 success: function(data) {
2208 Dispatcher.sendAction('SET_NEXT_HASH', data.hash);
2209 }
2210 });
2211 // Fetch latest all-bets to populate the all-bets tab
2212 MoneyPot.listBets({
2213 success: function(bets) {
2214 console.log('[MoneyPot.listBets]:', bets);
2215 Dispatcher.sendAction('INIT_ALL_BETS', bets.reverse());
2216 },
2217 error: function(err) {
2218 console.error('[MoneyPot.listBets] Error:', err);
2219 }
2220 });
2221}
2222
2223////////////////////////////////////////////////////////////
2224// Hook up to chat server
2225
2226function connectToChatServer() {
2227 console.log('Connecting to chat server. AccessToken:',
2228 worldStore.state.accessToken);
2229
2230 socket = io(config.chat_uri);
2231
2232 socket.on('connect', function() {
2233 console.log('[socket] Connected');
2234
2235 socket.on('disconnect', function() {
2236 console.log('[socket] Disconnected');
2237 });
2238
2239 // When subscribed to DEPOSITS:
2240
2241 socket.on('unconfirmed_balance_change', function(payload) {
2242 console.log('[socket] unconfirmed_balance_change:', payload);
2243 Dispatcher.sendAction('UPDATE_USER', {
2244 unconfirmed_balance: payload.balance
2245 });
2246 });
2247
2248 socket.on('balance_change', function(payload) {
2249 console.log('[socket] (confirmed) balance_change:', payload);
2250 Dispatcher.sendAction('UPDATE_USER', {
2251 balance: payload.balance
2252 });
2253 });
2254
2255 // message is { text: String, user: { role: String, uname: String} }
2256 socket.on('new_message', function(message) {
2257 console.log('[socket] Received chat message:', message);
2258 Dispatcher.sendAction('NEW_MESSAGE', message);
2259 });
2260
2261 socket.on('user_joined', function(user) {
2262 console.log('[socket] User joined:', user);
2263 Dispatcher.sendAction('USER_JOINED', user);
2264 });
2265
2266 // `user` is object { uname: String }
2267 socket.on('user_left', function(user) {
2268 console.log('[socket] User left:', user);
2269 Dispatcher.sendAction('USER_LEFT', user);
2270 });
2271
2272 socket.on('new_bet', function(bet) {
2273 console.log('[socket] New bet:', bet);
2274
2275 // Ignore bets that aren't of kind "simple_dice".
2276 if (bet.kind !== 'simple_dice') {
2277 console.log('[weird] received bet from socket that was NOT a simple_dice bet');
2278 return;
2279 }
2280
2281 Dispatcher.sendAction('NEW_ALL_BET', bet);
2282 });
2283
2284 // Received when your client doesn't comply with chat-server api
2285 socket.on('client_error', function(text) {
2286 console.warn('[socket] Client error:', text);
2287 });
2288
2289 // Once we connect to chat server, we send an auth message to join
2290 // this app's lobby channel.
2291
2292 var authPayload = {
2293 app_id: config.app_id,
2294 access_token: worldStore.state.accessToken,
2295 subscriptions: ['CHAT', 'DEPOSITS', 'BETS']
2296 };
2297
2298 socket.emit('auth', authPayload, function(err, data) {
2299 if (err) {
2300 console.log('[socket] Auth failure:', err);
2301 return;
2302 }
2303 console.log('[socket] Auth success:', data);
2304 Dispatcher.sendAction('INIT_CHAT', data);
2305 });
2306 });
2307}
2308
2309// This function is passed to the recaptcha.js script and called when
2310// the script loads and exposes the window.grecaptcha object. We pass it
2311// as a prop into the faucet component so that the faucet can update when
2312// when grecaptcha is loaded.
2313function onRecaptchaLoad() {
2314 Dispatcher.sendAction('GRECAPTCHA_LOADED', grecaptcha);
2315}
2316
2317$(document).on('keydown', function(e) {
2318 var H = 72, L = 76, C = 67, X = 88, keyCode = e.which;
2319
2320 // Bail is hotkeys aren't currently enabled to prevent accidental bets
2321 if (!worldStore.state.hotkeysEnabled) {
2322 return;
2323 }
2324
2325 // Bail if it's not a key we care about
2326 if (keyCode !== H && keyCode !== L && keyCode !== X && keyCode !== C) {
2327 return;
2328 }
2329
2330 // TODO: Remind self which one I need and what they do ^_^;;
2331 e.stopPropagation();
2332 e.preventDefault();
2333
2334 switch(keyCode) {
2335 case C: // Increase wager
2336 var upWager = betStore.state.wager.num * 2;
2337 Dispatcher.sendAction('UPDATE_WAGER', {
2338 num: upWager,
2339 str: upWager.toString()
2340 });
2341 break;
2342 case X: // Decrease wager
2343 var downWager = Math.floor(betStore.state.wager.num / 2);
2344 Dispatcher.sendAction('UPDATE_WAGER', {
2345 num: downWager,
2346 str: downWager.toString()
2347 });
2348
2349 break;
2350 case L: // Bet lo
2351 $('#bet-lo').click();
2352 break;
2353 case H: // Bet hi
2354 $('#bet-hi').click();
2355 break;
2356 default:
2357 return;
2358 }
2359});
2360
2361window.addEventListener('message', function(event) {
2362 if (event.origin === config.mp_browser_uri && event.data === 'UPDATE_BALANCE') {
2363 Dispatcher.sendAction('START_REFRESHING_USER');
2364 }
2365}, false);