├── .gitignore ├── LICENSE ├── README.md ├── data ├── bitcoin128.png ├── bitcoin16.png ├── bitcoin19.png ├── bitcoin38.png ├── bitcoin48.png ├── css │ ├── ajax-loader.gif │ ├── bootstrap.min.css │ ├── hoverpopup.css │ ├── index.css │ └── paypopup.css ├── fonts │ └── glyphicons-halflings-regular.woff ├── hoverpopup.html ├── index.html ├── js │ ├── background.js │ ├── context-menu.js │ ├── currency-manager.js │ ├── hoverpopup.js │ ├── index.js │ ├── libs │ │ ├── bitcoinjs-lib.min.js │ │ ├── bootstrap.min.js │ │ ├── cryptojs.min.js │ │ ├── jquery.min.js │ │ ├── promise.min.js │ │ └── qrcode.js │ ├── paypopup.js │ ├── preferences.js │ ├── util.js │ └── wallet.js └── paypopup.html ├── lib └── main.js ├── manifest.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Andrew Toth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BitBrowser Bitcoin Wallet 2 | ============= 3 | 4 | Bitcoin wallet in the browser. Send and receive instantly on any web page. 5 | 6 | Check out a video demonstration here. Screenshots here. 7 | 8 | You can download the extension here, or roll your own by simply downloading this repo with the button on the right, renaming .zip to .crx and drag-and-dropping it into Chrome. 9 | 10 | Security 11 | -------- 12 | 13 | The private key is stored in the browser only. Transactions are signed in the browser and are pushed to blockchain.info. The private key will only leave the browser to be synced with other Chrome browsers that you are signed into. Encrypting the private key ensures that nobody will know the private key without the password, not even this extension. 14 | -------------------------------------------------------------------------------- /data/bitcoin128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/bitcoin128.png -------------------------------------------------------------------------------- /data/bitcoin16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/bitcoin16.png -------------------------------------------------------------------------------- /data/bitcoin19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/bitcoin19.png -------------------------------------------------------------------------------- /data/bitcoin38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/bitcoin38.png -------------------------------------------------------------------------------- /data/bitcoin48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/bitcoin48.png -------------------------------------------------------------------------------- /data/css/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/css/ajax-loader.gif -------------------------------------------------------------------------------- /data/css/hoverpopup.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color : transparent; 3 | } 4 | 5 | .popover.top { 6 | width: 210px; 7 | height: 55px; 8 | margin-left: 5px; 9 | display: block; 10 | margin-top: 0; 11 | } 12 | 13 | .popover-content { 14 | padding: 5px; 15 | } 16 | 17 | .progress { 18 | width: 170px; 19 | margin-bottom: 0; 20 | height: 41px; 21 | } 22 | 23 | .progress-bar { 24 | width: 100%; 25 | } 26 | 27 | #infoButton { 28 | position: absolute; 29 | bottom: 6px; 30 | right: 6px; 31 | width: 20px; 32 | height: 20px; 33 | padding: 0; 34 | } 35 | 36 | #infoIcon { 37 | width: 20px; 38 | height: 20px; 39 | vertical-align: top; 40 | } 41 | 42 | #closeButton { 43 | position: absolute; 44 | top: 0; 45 | right: 6px; 46 | } 47 | 48 | .pull-right { 49 | margin-right: 28px; 50 | } 51 | 52 | h6 { 53 | margin-top: 4px; 54 | margin-bottom: 6px; 55 | } -------------------------------------------------------------------------------- /data/css/index.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-left: 0px; 3 | width: 360px; 4 | } 5 | 6 | a:hover { 7 | color: #428bca; 8 | text-decoration: none !important; 9 | } 10 | 11 | body .modal-dialog { 12 | margin: 0px; 13 | width: 360px; 14 | overflow: hidden; 15 | } 16 | 17 | #privateKeyQRCode { 18 | margin-left: 77px; 19 | } 20 | 21 | #qrcode { 22 | margin-left: 86px; 23 | } 24 | 25 | #settingsButton { 26 | margin-left: 219px; 27 | } 28 | 29 | #settingsMenu { 30 | margin-left: 76px; 31 | } 32 | 33 | .no-padding { 34 | margin: 0px; 35 | } 36 | 37 | #sendConfirmationText, 38 | #privateKeyText { 39 | word-wrap: break-word; 40 | } 41 | 42 | .panel { 43 | margin-bottom: 0px; 44 | } 45 | 46 | #cover { 47 | background: url("ajax-loader.gif") no-repeat scroll center center rgba(255,255,255,0.5); 48 | z-index: 2147483647; 49 | position: absolute; 50 | height: 100%; 51 | width: 100%; 52 | } -------------------------------------------------------------------------------- /data/css/paypopup.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color : transparent; 3 | } 4 | 5 | .alert { 6 | margin-bottom: 5px; 7 | padding: 5px; 8 | font-size: 12px; 9 | width: 156px; 10 | } 11 | 12 | .alert-success { 13 | margin-bottom: 0px; 14 | } 15 | 16 | .form-group { 17 | margin-bottom: 5px; 18 | } 19 | 20 | .btn-sm { 21 | width: 156px; 22 | } 23 | 24 | .popover { 25 | margin-left: 5px; 26 | display: block; 27 | } 28 | 29 | .popover-content { 30 | padding: 5px; 31 | } 32 | 33 | .progress { 34 | width: 156px; 35 | margin-bottom: 0px; 36 | } 37 | 38 | .progress-bar { 39 | width: 100%; 40 | } -------------------------------------------------------------------------------- /data/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewtoth/BitcoinWallet/d3194981ebea90fbd07bc592a4a3f2cc712589ce/data/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /data/hoverpopup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bitcoin Wallet Extension 5 | 6 | 7 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bitcoin Wallet Extension 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | Send BTC 27 |
28 | 31 | 42 |
43 |
44 |
45 | 49 | 53 |
54 | 55 | 58 | 59 |
60 | 61 | 64 |
65 | 66 | BTC 67 |
68 | 69 |
70 |
71 |
72 | 78 |
79 |
80 | 81 | 82 | 99 | 100 | 101 | 125 | 126 | 127 | 168 | 169 | 170 | 181 | 182 | 183 | 212 | 213 | 214 | 243 | 244 | 245 | 270 | 271 | 272 | 303 | 304 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /data/js/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * background.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Background script for Chrome extension 9 | */ 10 | 11 | (function () { 12 | 13 | // Save port to communicate with content scripts 14 | var responsePort = null; 15 | chrome.runtime.onConnect.addListener(function(port) { 16 | responsePort = port; 17 | }); 18 | 19 | // Create context menus 20 | chrome.contextMenus.create({'title': 'Pay %s', 'contexts': ['selection'], 'onclick': menuOnClick}); 21 | chrome.contextMenus.create({'title': 'Send BTC', 'contexts': ['page'], 'onclick': menuOnClick}); 22 | function menuOnClick(info) { 23 | if (info.selectionText) { 24 | responsePort.postMessage({'address': info.selectionText}); 25 | } else { 26 | responsePort.postMessage({}); 27 | } 28 | }; 29 | 30 | // Open new tabs 31 | chrome.runtime.onMessage.addListener(function (request) { 32 | if (request.address) { 33 | chrome.tabs.create({url: 'https://blockchain.info/address/' + request.address}); 34 | } 35 | }); 36 | 37 | })(); -------------------------------------------------------------------------------- /data/js/context-menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * context-menu.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Context menu event listeners for Firefox add-on 9 | */ 10 | 11 | (function () { 12 | 13 | var address = null, 14 | rect = null; 15 | 16 | // Event received before context menu is rendered 17 | self.on('context', function (node) { 18 | address = null; 19 | rect = null; 20 | // Send null address to reset label to 'Send BTC' 21 | self.postMessage({address:address}); 22 | // If we're a text node we check if it contains a bitcoin address 23 | if (node.children.length == 0 && node['textContent']) { 24 | var text = node['textContent']; 25 | var matches = text.match(/[13][1-9A-HJ-NP-Za-km-z]{26,33}/); 26 | if (matches) { 27 | try { 28 | new Bitcoin.Address(matches[0]); 29 | // If we get here we have a valid bitcoin address somewhere in the right clicked node 30 | address = matches[0]; 31 | // Sending the address back changes text to 'Pay
' 32 | self.postMessage({address:address}); 33 | // Wrap address with a unique span so we can determine the exact position 34 | text = text.replace(address, '' + address + ''); 35 | var replacementNode = document.createElement('span'); 36 | replacementNode.innerHTML = text; 37 | node.parentNode.insertBefore(replacementNode, node); 38 | node.parentNode.removeChild(node); 39 | // Get the rect of the address 40 | rect = document.getElementById('bitcoin-address-' + address).getBoundingClientRect(); 41 | // Put everything back to where it was 42 | replacementNode.parentNode.insertBefore(node, replacementNode); 43 | replacementNode.parentNode.removeChild(replacementNode); 44 | } catch (e) {} 45 | } 46 | } 47 | // Return true will always show the item in the context menu 48 | return true; 49 | }); 50 | 51 | // Event received when the context menu item is clicked 52 | self.on('click', function () { 53 | var object = {clicked:true, address:address}; 54 | // If an address was clicked, rect will be the position of the address so send it 55 | if (rect) { 56 | object.left = rect.left; 57 | object.right = rect.right; 58 | object.top = rect.top; 59 | object.bottom = rect.bottom; 60 | } 61 | self.postMessage(object); 62 | }); 63 | 64 | })(); -------------------------------------------------------------------------------- /data/js/currency-manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * currency-manager.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Currency manager handles the exchange rate of the currency 9 | * and the proper formatting of the currency value 10 | */ 11 | 12 | (function (window) { 13 | var currencyManager = function () {}; 14 | currencyManager.prototype = { 15 | 16 | updateExchangeRate: function () { 17 | return preferences.getCurrency().then(function (currency) { 18 | return util.getJSON('https://api.bitcoinaverage.com/ticker/' + currency); 19 | }).then(function (response) { 20 | return preferences.setExchangeRate(response['24h_avg']); 21 | }); 22 | }, 23 | 24 | getSymbol: function () { 25 | return preferences.getCurrency().then(function (currency) { 26 | switch (currency) { 27 | case 'AUD': 28 | case 'CAD': 29 | case 'NZD': 30 | case 'SGD': 31 | case 'USD': 32 | return(['$', 'before']); 33 | case 'BRL': 34 | return(['R$', 'before']); 35 | case 'CHF': 36 | return([' Fr.', 'after']); 37 | case 'CNY': 38 | case 'JPY': 39 | return(['¥', 'before']); 40 | case 'CZK': 41 | return([' Kč', 'after']); 42 | case 'EUR': 43 | return(['€', 'before']); 44 | case 'GBP': 45 | return(['£', 'before']); 46 | case 'ILS': 47 | return(['₪', 'before']); 48 | case 'NOK': 49 | case 'SEK': 50 | return([' kr', 'after']); 51 | case 'PLN': 52 | return(['zł', 'after']); 53 | case 'RUB': 54 | return([' RUB', 'after']); 55 | case 'ZAR': 56 | return([' R', 'after']); 57 | default: 58 | return(['$', 'before']); 59 | } 60 | }); 61 | }, 62 | 63 | getAvailableCurrencies: function () { 64 | return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'EUR', 'GBP', 'ILS', 'JPY', 'NOK', 'NZD', 'PLN', 'RUB', 'SEK', 'SGD', 'USD', 'ZAR']; 65 | }, 66 | 67 | formatAmount: function (value) { 68 | return Promise.all([preferences.getExchangeRate(), this.getSymbol()]).then(function (values) { 69 | var rate = values[0], 70 | symbol = values[1][0], 71 | beforeOrAfter = values[1][1], 72 | SATOSHIS = 100000000, 73 | text = (value / SATOSHIS * rate).formatMoney(2); 74 | if (beforeOrAfter === 'before') { 75 | text = symbol + text; 76 | } else { 77 | text += symbol; 78 | } 79 | return text; 80 | }); 81 | } 82 | }; 83 | 84 | Number.prototype.formatMoney = function(c, d, t){ 85 | var n = this, 86 | c = isNaN(c = Math.abs(c)) ? 2 : c, 87 | d = d == undefined ? "." : d, 88 | t = t == undefined ? "," : t, 89 | s = n < 0 ? "-" : "", 90 | i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", 91 | j = (j = i.length) > 3 ? j % 3 : 0; 92 | return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ""); 93 | }; 94 | 95 | var ret = new currencyManager(); 96 | ret.updateExchangeRate(); 97 | window.currencyManager = ret; 98 | 99 | })(window); -------------------------------------------------------------------------------- /data/js/hoverpopup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * hoverpopup.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Controls hoverpopup.html, the popup that appears hovering over addresses 9 | */ 10 | 11 | $(document).ready(function () { 12 | // Recursively walk through the childs, and push text nodes in the list 13 | var textNodes = []; 14 | (function recursiveWalk(node) { 15 | if (node) { 16 | node = node.firstChild; 17 | while (node != null) { 18 | if (node.nodeType == 3) { 19 | textNodes.push(node); 20 | } else if (node.nodeType == 1) { 21 | recursiveWalk(node); 22 | } 23 | node = node.nextSibling; 24 | } 25 | } 26 | })(document.body); 27 | 28 | // Check all text nodes for addresses 29 | for (var i = textNodes.length-1; i>=0; i--) { 30 | var matches = textNodes[i]['textContent'].match(/[13][1-9A-HJ-NP-Za-km-z]{26,33}/g); 31 | if (matches) { 32 | var text = textNodes[i]['textContent'], 33 | hasMatch = false; 34 | for (var j = 0; j < matches.length; j++) { 35 | try { 36 | new Bitcoin.Address(matches[j]); 37 | // If we're here, then this node has a valid address 38 | hasMatch = true; 39 | // Wrap the address in the 'bitcoin-address' class 40 | text = text.replace(matches[j], '' + matches[j] + ''); 41 | } catch (e) {} 42 | } 43 | if (hasMatch) { 44 | // Replace the node with the wrapped bitcoin addresses HTML 45 | var replacementNode = document.createElement('span'), 46 | node = textNodes[i]; 47 | replacementNode.innerHTML = text; 48 | node.parentNode.insertBefore(replacementNode, node); 49 | node.parentNode.removeChild(node); 50 | } 51 | } 52 | } 53 | 54 | // We only want to open the popup once per address to not annoy the user, 55 | // so cache the addresses 56 | var openPopups = {}; 57 | // Open the address when we hover on a 'bitcoin-address' wrapped element 58 | $('.bitcoin-address').hover(function () { 59 | var address = $(this).text(); 60 | var rect = this.getBoundingClientRect(); 61 | if (!openPopups[address]) { 62 | // Cache the address 63 | openPopups[address] = {}; 64 | util.iframe('hoverpopup.html').then(function (iframe) { 65 | var height = 66; 66 | iframe.style.height = height + 'px'; 67 | iframe.style.width = '240px'; 68 | iframe.style.left = Number(rect.left) + Number(window.pageXOffset) + Number(rect.right - rect.left)/2 - 105 + 'px'; 69 | iframe.style.top = Number(rect.top) + Number(window.pageYOffset) - height + 'px'; 70 | var $iframe = $(iframe.contentWindow.document); 71 | $iframe.find('#main').fadeIn('fast'); 72 | util.getJSON('https://blockchain.info/address/' + address + '?format=json&limit=0').then(function (json) { 73 | return Promise.all([currencyManager.formatAmount(json.total_received), currencyManager.formatAmount(json.final_balance)]); 74 | }).then(function (amounts) { 75 | $iframe.find('#progress').fadeOut('fast', function () { 76 | $iframe.find('#received').fadeIn('fast').html('Total received: ' + amounts[0] + ''); 77 | $iframe.find('#balance').fadeIn('fast').html('Final balance: ' + amounts[1] + ''); 78 | }); 79 | }); 80 | $iframe.find('#infoButton').click(function () { 81 | // Different APIs to open tabs in Chrome and Firefox 82 | if (typeof chrome !== 'undefined') { 83 | chrome.runtime.sendMessage({address: address}) 84 | } else { 85 | self.port.emit('openTab', 'https://blockchain.info/address/' + address); 86 | } 87 | }); 88 | $iframe.find('#closeButton').click(function () { 89 | $(iframe).fadeOut('fast', function () { 90 | $(this).remove(); 91 | }); 92 | }); 93 | }); 94 | } 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /data/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Controls index.html, the main wallet Chrome popover/Firefox panel 9 | */ 10 | 11 | $(document).ready(function () { 12 | // Setup the wallet, page values and callbacks 13 | var val = '', 14 | address = '', 15 | SATOSHIS = 100000000, 16 | FEE = SATOSHIS * .0001, 17 | BTCUnits = 'BTC', 18 | BTCMultiplier = SATOSHIS; 19 | function setupWallet() { 20 | wallet.restoreAddress().then(setQRCodes, 21 | function () { 22 | return wallet.generateAddress(); 23 | }).then(setQRCodes, 24 | function () { 25 | alert('Failed to generate wallet. Refresh and try again.'); 26 | }); 27 | 28 | function setQRCodes() { 29 | $('#qrcode').html(createQRCodeCanvas(wallet.getAddress())); 30 | $('#textAddress').text(wallet.getAddress()); 31 | } 32 | } 33 | wallet.setBalanceListener(function (balance) { 34 | setBalance(balance); 35 | }); 36 | setupWallet(); 37 | 38 | $('#amount').on('keyup change', function () { 39 | val = Math.floor(Number($(this).val() * BTCMultiplier)); 40 | if (val > 0) { 41 | currencyManager.formatAmount(val).then(function (formattedMoney) { 42 | var text = 'Amount: ' + formattedMoney; 43 | $('#amountLabel').text(text); 44 | }); 45 | } else { 46 | $('#amountLabel').text('Amount:'); 47 | } 48 | }); 49 | 50 | function setBTCUnits(units) { 51 | BTCUnits = units; 52 | if (units === 'µBTC') { 53 | BTCMultiplier = SATOSHIS / 1000000; 54 | } else if (units === 'mBTC') { 55 | BTCMultiplier = SATOSHIS / 1000; 56 | } else { 57 | BTCMultiplier = SATOSHIS; 58 | } 59 | 60 | setBalance(wallet.getBalance()); 61 | $('#sendUnit').html(BTCUnits); 62 | $('#amount').attr('placeholder', '(Plus ' + FEE / BTCMultiplier + ' ' + BTCUnits + ' fee)').attr('step', 100000 / BTCMultiplier).val(null); 63 | $('#amountLabel').text('Amount:'); 64 | } 65 | preferences.getBTCUnits().then(setBTCUnits); 66 | 67 | function setBalance(balance) { 68 | if (Number(balance) < 0 || isNaN(balance)) { 69 | balance = 0; 70 | } 71 | $('#balance').text(balance / BTCMultiplier + ' ' + BTCUnits); 72 | } 73 | 74 | $('#successAlertClose').click(function () { 75 | $('#successAlert').fadeOut(); 76 | if (typeof chrome === 'undefined') { 77 | addon.port.emit('resize', 278); 78 | } 79 | }); 80 | 81 | $('#unkownErrorAlertClose').click(function () { 82 | $('#unknownErrorAlert').fadeOut(); 83 | }); 84 | 85 | if (typeof chrome === 'undefined') { 86 | addon.port.on('show', setupWallet); 87 | } 88 | 89 | /* 90 | * Send BTC 91 | */ 92 | $('#sendButton').click(function () { 93 | val = Math.floor(Number($('#amount').val() * BTCMultiplier)); 94 | address = $('#sendAddress').val(); 95 | var balance = wallet.getBalance(); 96 | var validAmount = true; 97 | if (val <= 0) { 98 | validAmount = false; 99 | } else if (val + FEE > balance) { 100 | validAmount = false; 101 | } 102 | if (validAmount) { 103 | $('#amountAlert').slideUp(); 104 | } else { 105 | $('#amountAlert').slideDown(); 106 | } 107 | 108 | var regex = /^[13][1-9A-HJ-NP-Za-km-z]{26,33}$/; 109 | var validAddress = true; 110 | if (!regex.test(String(address))) { 111 | validAddress = false; 112 | } else { 113 | try { 114 | new Bitcoin.Address(address); 115 | } catch (e) { 116 | validAddress = false; 117 | } 118 | } 119 | 120 | if (validAddress) { 121 | $('#addressAlert').slideUp(); 122 | } else { 123 | $('#addressAlert').slideDown(); 124 | } 125 | 126 | if (validAddress && validAmount) { 127 | if (wallet.isEncrypted()) { 128 | currencyManager.formatAmount(val).then(function (formattedMoney) { 129 | var text = 'Are you sure you want to send
' + val / BTCMultiplier + ' ' + BTCUnits + ' (' + formattedMoney + ')
to ' + address + ' ?'; 130 | $('#sendConfirmationText').html(text); 131 | $('#sendConfirmationPassword').val(null); 132 | $('#sendConfirmationPasswordIncorrect').hide(); 133 | $('#sendConfirmationModal').modal().show(); 134 | }); 135 | } else { 136 | confirmSend(); 137 | } 138 | } 139 | }); 140 | 141 | $('#confirmSendButton').click(function () { 142 | confirmSend(); 143 | }); 144 | 145 | function confirmSend() { 146 | $('#cover').show(); 147 | var password = $('#sendConfirmationPassword').val(); 148 | wallet.send(address, val, FEE, password).then(function () { 149 | $('#amount').val(null); 150 | $('#sendAddress').val(null); 151 | $('#amountLabel').text('Amount:'); 152 | var text = 'Sent ' + val / BTCMultiplier + ' ' + BTCUnits + ' to ' + address + '.'; 153 | $('#successAlertLabel').text(text); 154 | $('#successAlert').slideDown(); 155 | $('#sendConfirmationModal').modal('hide'); 156 | $('#cover').fadeOut('slow'); 157 | }, function (e) { 158 | if (wallet.isEncrypted()) { 159 | $('#sendConfirmationPasswordIncorrect').text(e.message).slideDown(); 160 | } else { 161 | $('#unknownErrorAlertLabel').text(e.message); 162 | $('#unknownErrorAlert').slideDown(); 163 | } 164 | $('#cover').hide(); 165 | }); 166 | } 167 | 168 | /* 169 | * Settings Menu 170 | */ 171 | 172 | /* 173 | * Set Password 174 | */ 175 | $('#setPassword').click(function () { 176 | $('#passwordMismatch').hide(); 177 | $('#setPasswordIncorrect').hide(); 178 | $('#setPasswordBlank').hide(); 179 | if (wallet.isEncrypted()) { 180 | $('#removePasswordDiv').show(); 181 | $('#setPasswordPassword').show().val(null); 182 | } else { 183 | $('#removePasswordDiv').hide(); 184 | $('#setPasswordPassword').hide().val(null); 185 | } 186 | $('#newPassword').show().val(null); 187 | $('#confirmNewPassword').show().val(null); 188 | $('#removePassword').attr('checked', false); 189 | $('#setPasswordModal').modal().show(); 190 | }); 191 | 192 | $('#removePassword').click(function () { 193 | if (this.checked) { 194 | $('#newPassword').val(null).slideUp(); 195 | $('#confirmNewPassword').val(null).slideUp(); 196 | } else { 197 | $('#newPassword').slideDown(); 198 | $('#confirmNewPassword').slideDown(); 199 | } 200 | }); 201 | 202 | $('#confirmSetPassword').click(function () { 203 | var password = $('#setPasswordPassword').val(), 204 | newPassword = $('#newPassword').val(), 205 | confirmNewPassword = $('#confirmNewPassword').val(); 206 | var validInput = true; 207 | if ((wallet.isEncrypted() && !password) || (!$('#removePassword').is(':checked') && (!newPassword || !confirmNewPassword))) { 208 | validInput = false; 209 | $('#setPasswordBlank').slideDown(); 210 | } else { 211 | $('#setPasswordBlank').slideUp(); 212 | } 213 | 214 | if (validInput && newPassword !== confirmNewPassword) { 215 | validInput = false; 216 | $('#passwordMismatch').slideDown(); 217 | } else { 218 | $('#passwordMismatch').slideUp(); 219 | } 220 | 221 | if (validInput && wallet.isEncrypted() && !wallet.validatePassword(password)) { 222 | validInput = false; 223 | $('#setPasswordIncorrect').slideDown(); 224 | } else { 225 | $('#setPasswordIncorrect').slideUp(); 226 | } 227 | 228 | if (validInput) { 229 | wallet.updatePassword(String(password), String(newPassword)).then(function () { 230 | $('#successAlertLabel').text('New password set.'); 231 | $('#successAlert').show(); 232 | $('#setPasswordModal').modal('hide'); 233 | }); 234 | } 235 | 236 | }); 237 | 238 | /* 239 | * Currency selection 240 | */ 241 | $('#setCurrency').click(function () { 242 | preferences.getCurrency().then(function (currency) { 243 | var currencies = currencyManager.getAvailableCurrencies(); 244 | var tableBody = ''; 245 | for (var i = 0; i < currencies.length/3; i++) { 246 | tableBody += ''; 247 | for (var j = i; j <= i+12; j+=6) { 248 | tableBody += '
'; 253 | } 254 | tableBody += ''; 255 | } 256 | $('#tableBody').html(tableBody); 257 | $('#setCurrencyModal').modal().show(); 258 | $('.radio').click(function () { 259 | var currency = $.trim($(this).text()); 260 | $('input:radio[name=' + currency + ']').attr('checked', 'checked'); 261 | preferences.setCurrency(currency).then(function () { 262 | $('#amountLabel').text('Amount:'); 263 | $('#successAlertLabel').text('Currency set to ' + currency + '.'); 264 | $('#successAlert').show(); 265 | $('#setCurrencyModal').modal('hide'); 266 | }); 267 | }); 268 | }); 269 | }); 270 | 271 | /* 272 | * Units selection 273 | */ 274 | $('#setUnits').click(function () { 275 | preferences.getBTCUnits().then(function (units) { 276 | var availableUnits = ['BTC', 'mBTC', 'µBTC']; 277 | var tableBody = ''; 278 | for (var i = 0; i < availableUnits.length; i++) { 279 | tableBody += '
'; 284 | } 285 | tableBody += ''; 286 | $('#tableBody').html(tableBody); 287 | $('#setCurrencyModal').modal().show(); 288 | $('.radio').click(function () { 289 | var units = $.trim($(this).text()); 290 | $('input:radio[name=' + units + ']').attr('checked', 'checked'); 291 | setBTCUnits(units); 292 | preferences.setBTCUnits(units).then(function () { 293 | $('#successAlertLabel').text('Units set to ' + units + '.'); 294 | $('#successAlert').show(); 295 | $('#setCurrencyModal').modal('hide'); 296 | }); 297 | }); 298 | }); 299 | }); 300 | 301 | /* 302 | * Show Private Key 303 | */ 304 | $('#showPrivateKey').click(function () { 305 | $('#showPrivateKeyPasswordIncorrect').hide(); 306 | if (wallet.isEncrypted()) { 307 | $('#showPrivateKeyPassword').val(null).show(); 308 | } else { 309 | $('#showPrivateKeyPassword').hide(); 310 | } 311 | $('#privateKey').hide(); 312 | $('#showPrivateKeyModal').modal().show(); 313 | }); 314 | 315 | $('#showPrivateKeyConfirm').click(function () { 316 | var password = $('#showPrivateKeyPassword').val(); 317 | if (wallet.isEncrypted() && !wallet.validatePassword(password)) { 318 | $('#showPrivateKeyPasswordIncorrect').slideDown(); 319 | } else { 320 | $('#showPrivateKeyPasswordIncorrect').slideUp(); 321 | var privateKey = wallet.getDecryptedPrivateKey(password); 322 | $('#privateKeyQRCode').html(createQRCodeCanvas(privateKey)); 323 | $('#privateKeyText').text(privateKey); 324 | $('#privateKey').slideDown(function () { 325 | $('#main').height($('#showPrivateKeyModal').find('.modal-dialog').height()); 326 | }); 327 | } 328 | }); 329 | 330 | /* 331 | * Import Private Key 332 | */ 333 | $('#importPrivateKey').click(function () { 334 | $('#importPrivateKeyPasswordIncorrect').hide(); 335 | $('#importPrivateKeyBadPrivateKey').hide(); 336 | if (wallet.isEncrypted()) { 337 | $('#importPrivateKeyPassword').val(null).show(); 338 | } else { 339 | $('#importPrivateKeyPassword').hide(); 340 | } 341 | $('#importPrivateKeyPrivateKey').val(null); 342 | $('#importPrivateKeyModal').modal().show(); 343 | }); 344 | 345 | $('#importPrivateKeyConfirm').click(function () { 346 | var privateKey = $('#importPrivateKeyPrivateKey').val(); 347 | try { 348 | new Bitcoin.ECKey(privateKey).getExportedPrivateKey(); 349 | } catch (e) { 350 | $('#importPrivateKeyBadPrivateKey').slideDown(); 351 | return; 352 | } 353 | wallet.importAddress($('#importPrivateKeyPassword').val(), privateKey).then(function () { 354 | setupWallet(); 355 | $('#successAlertLabel').text('Private key imported successfully.'); 356 | $('#successAlert').show(); 357 | $('#importPrivateKeyModal').modal('hide'); 358 | }, function (e) { 359 | if (e.message === 'Incorrect password') { 360 | $('#importPrivateKeyBadPrivateKey').slideUp(); 361 | $('#importPrivateKeyPasswordIncorrect').slideDown(); 362 | } else { 363 | $('#importPrivateKeyPasswordIncorrect').slideUp(); 364 | $('#importPrivateKeyBadPrivateKey').slideDown(); 365 | } 366 | }); 367 | }); 368 | 369 | /* 370 | * Generate New Wallet 371 | */ 372 | $('#generateNewWallet').click(function () { 373 | $('#generateNewWalletPasswordIncorrect').hide(); 374 | if (wallet.isEncrypted()) { 375 | $('#generateNewWalletPassword').show().val(null); 376 | } else { 377 | $('#generateNewWalletPassword').hide(); 378 | } 379 | $('#generateNewWalletModal').modal().show(); 380 | }); 381 | 382 | $('#generateNewWalletConfirm').click(function () { 383 | wallet.generateAddress($('#generateNewWalletPassword').val()).then(function () { 384 | setupWallet(); 385 | $('#successAlertLabel').text('New wallet generated.'); 386 | $('#successAlert').show(); 387 | $('#generateNewWalletModal').modal('hide'); 388 | }, function () { 389 | $('#generateNewWalletPasswordIncorrect').slideDown(); 390 | }); 391 | }); 392 | 393 | /* 394 | * About 395 | */ 396 | 397 | if (typeof chrome !== 'undefined') { 398 | $('#version').text(chrome.runtime.getManifest().version); 399 | } else { 400 | addon.port.on('version', function (version) { 401 | $('#version').text(version); 402 | }); 403 | } 404 | 405 | $('#aboutModal').on('click', 'a', function () { 406 | if (typeof chrome !== 'undefined') { 407 | chrome.tabs.create({url: $(this).attr('href')}); 408 | } else { 409 | addon.port.emit('openTab', $(this).attr('href')); 410 | } 411 | return false; 412 | }); 413 | 414 | /* 415 | * Resizing 416 | */ 417 | 418 | $('.modal').on('shown.bs.modal', function() { 419 | var $main = $('#main'); 420 | var height = $main.height(); 421 | var modalHeight = $(this).find('.modal-dialog').height(); 422 | if (modalHeight > height) { 423 | $main.height(modalHeight); 424 | if (typeof chrome === 'undefined') { 425 | addon.port.emit('resize', modalHeight+2); 426 | } 427 | } 428 | }).on('hidden.bs.modal', function () { 429 | $('#main').height('auto'); 430 | if (typeof chrome === 'undefined') { 431 | if ($('#successAlert').is(':visible')) { 432 | var height = 350; 433 | } else { 434 | var height = 278; 435 | } 436 | addon.port.emit('resize', height); 437 | } 438 | }); 439 | 440 | function createQRCodeCanvas(text) { 441 | var sizeMultiplier = 4; 442 | var typeNumber; 443 | var lengthCalculation = text.length * 8 + 12; 444 | if (lengthCalculation < 72) { typeNumber = 1; } 445 | else if (lengthCalculation < 128) { typeNumber = 2; } 446 | else if (lengthCalculation < 208) { typeNumber = 3; } 447 | else if (lengthCalculation < 288) { typeNumber = 4; } 448 | else if (lengthCalculation < 368) { typeNumber = 5; } 449 | else if (lengthCalculation < 480) { typeNumber = 6; } 450 | else if (lengthCalculation < 528) { typeNumber = 7; } 451 | else if (lengthCalculation < 688) { typeNumber = 8; } 452 | else if (lengthCalculation < 800) { typeNumber = 9; } 453 | else if (lengthCalculation < 976) { typeNumber = 10; } 454 | var qrcode = new QRCode(typeNumber, QRCode.ErrorCorrectLevel.H); 455 | qrcode.addData(text); 456 | qrcode.make(); 457 | var width = qrcode.getModuleCount() * sizeMultiplier; 458 | var height = qrcode.getModuleCount() * sizeMultiplier; 459 | // create canvas element 460 | var canvas = document.createElement('canvas'); 461 | var scale = 10.0; 462 | canvas.width = width * scale; 463 | canvas.height = height * scale; 464 | canvas.style.width = width + 'px'; 465 | canvas.style.height = height + 'px'; 466 | var ctx = canvas.getContext('2d'); 467 | ctx.scale(scale, scale); 468 | // compute tileW/tileH based on width/height 469 | var tileW = width / qrcode.getModuleCount(); 470 | var tileH = height / qrcode.getModuleCount(); 471 | // draw in the canvas 472 | for (var row = 0; row < qrcode.getModuleCount(); row++) { 473 | for (var col = 0; col < qrcode.getModuleCount(); col++) { 474 | ctx.fillStyle = qrcode.isDark(row, col) ? "#000000" : "#ffffff"; 475 | ctx.fillRect(col * tileW, row * tileH, tileW, tileH); 476 | } 477 | } 478 | return canvas; 479 | } 480 | }); -------------------------------------------------------------------------------- /data/js/libs/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /data/js/libs/cryptojs.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | CryptoJS v3.1.2 3 | code.google.com/p/crypto-js 4 | (c) 2009-2013 by Jeff Mott. All rights reserved. 5 | code.google.com/p/crypto-js/wiki/License 6 | */ 7 | var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 8 | r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 9 | 32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, 10 | 2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, 11 | q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< 15 | l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); 16 | (function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, 17 | _doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), 18 | f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, 19 | m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, 20 | E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ 21 | 4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); 22 | (function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, 28 | this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, 29 | 1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, 30 | decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, 31 | b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); 32 | (function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, 33 | 16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> 34 | 8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= 35 | d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})(); -------------------------------------------------------------------------------- /data/js/libs/promise.min.js: -------------------------------------------------------------------------------- 1 | !function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j= 7) { 76 | this.setupTypeNumber(test); 77 | } 78 | 79 | if (this.dataCache == null) { 80 | this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); 81 | } 82 | 83 | this.mapData(this.dataCache, maskPattern); 84 | }, 85 | 86 | setupPositionProbePattern: function (row, col) { 87 | 88 | for (var r = -1; r <= 7; r++) { 89 | 90 | if (row + r <= -1 || this.moduleCount <= row + r) continue; 91 | 92 | for (var c = -1; c <= 7; c++) { 93 | 94 | if (col + c <= -1 || this.moduleCount <= col + c) continue; 95 | 96 | if ((0 <= r && r <= 6 && (c == 0 || c == 6)) 97 | || (0 <= c && c <= 6 && (r == 0 || r == 6)) 98 | || (2 <= r && r <= 4 && 2 <= c && c <= 4)) { 99 | this.modules[row + r][col + c] = true; 100 | } else { 101 | this.modules[row + r][col + c] = false; 102 | } 103 | } 104 | } 105 | }, 106 | 107 | getBestMaskPattern: function () { 108 | 109 | var minLostPoint = 0; 110 | var pattern = 0; 111 | 112 | for (var i = 0; i < 8; i++) { 113 | 114 | this.makeImpl(true, i); 115 | 116 | var lostPoint = QRCode.Util.getLostPoint(this); 117 | 118 | if (i == 0 || minLostPoint > lostPoint) { 119 | minLostPoint = lostPoint; 120 | pattern = i; 121 | } 122 | } 123 | 124 | return pattern; 125 | }, 126 | 127 | createMovieClip: function (target_mc, instance_name, depth) { 128 | 129 | var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth); 130 | var cs = 1; 131 | 132 | this.make(); 133 | 134 | for (var row = 0; row < this.modules.length; row++) { 135 | 136 | var y = row * cs; 137 | 138 | for (var col = 0; col < this.modules[row].length; col++) { 139 | 140 | var x = col * cs; 141 | var dark = this.modules[row][col]; 142 | 143 | if (dark) { 144 | qr_mc.beginFill(0, 100); 145 | qr_mc.moveTo(x, y); 146 | qr_mc.lineTo(x + cs, y); 147 | qr_mc.lineTo(x + cs, y + cs); 148 | qr_mc.lineTo(x, y + cs); 149 | qr_mc.endFill(); 150 | } 151 | } 152 | } 153 | 154 | return qr_mc; 155 | }, 156 | 157 | setupTimingPattern: function () { 158 | 159 | for (var r = 8; r < this.moduleCount - 8; r++) { 160 | if (this.modules[r][6] != null) { 161 | continue; 162 | } 163 | this.modules[r][6] = (r % 2 == 0); 164 | } 165 | 166 | for (var c = 8; c < this.moduleCount - 8; c++) { 167 | if (this.modules[6][c] != null) { 168 | continue; 169 | } 170 | this.modules[6][c] = (c % 2 == 0); 171 | } 172 | }, 173 | 174 | setupPositionAdjustPattern: function () { 175 | 176 | var pos = QRCode.Util.getPatternPosition(this.typeNumber); 177 | 178 | for (var i = 0; i < pos.length; i++) { 179 | 180 | for (var j = 0; j < pos.length; j++) { 181 | 182 | var row = pos[i]; 183 | var col = pos[j]; 184 | 185 | if (this.modules[row][col] != null) { 186 | continue; 187 | } 188 | 189 | for (var r = -2; r <= 2; r++) { 190 | 191 | for (var c = -2; c <= 2; c++) { 192 | 193 | if (r == -2 || r == 2 || c == -2 || c == 2 194 | || (r == 0 && c == 0)) { 195 | this.modules[row + r][col + c] = true; 196 | } else { 197 | this.modules[row + r][col + c] = false; 198 | } 199 | } 200 | } 201 | } 202 | } 203 | }, 204 | 205 | setupTypeNumber: function (test) { 206 | 207 | var bits = QRCode.Util.getBCHTypeNumber(this.typeNumber); 208 | 209 | for (var i = 0; i < 18; i++) { 210 | var mod = (!test && ((bits >> i) & 1) == 1); 211 | this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; 212 | } 213 | 214 | for (var i = 0; i < 18; i++) { 215 | var mod = (!test && ((bits >> i) & 1) == 1); 216 | this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; 217 | } 218 | }, 219 | 220 | setupTypeInfo: function (test, maskPattern) { 221 | 222 | var data = (this.errorCorrectLevel << 3) | maskPattern; 223 | var bits = QRCode.Util.getBCHTypeInfo(data); 224 | 225 | // vertical 226 | for (var i = 0; i < 15; i++) { 227 | 228 | var mod = (!test && ((bits >> i) & 1) == 1); 229 | 230 | if (i < 6) { 231 | this.modules[i][8] = mod; 232 | } else if (i < 8) { 233 | this.modules[i + 1][8] = mod; 234 | } else { 235 | this.modules[this.moduleCount - 15 + i][8] = mod; 236 | } 237 | } 238 | 239 | // horizontal 240 | for (var i = 0; i < 15; i++) { 241 | 242 | var mod = (!test && ((bits >> i) & 1) == 1); 243 | 244 | if (i < 8) { 245 | this.modules[8][this.moduleCount - i - 1] = mod; 246 | } else if (i < 9) { 247 | this.modules[8][15 - i - 1 + 1] = mod; 248 | } else { 249 | this.modules[8][15 - i - 1] = mod; 250 | } 251 | } 252 | 253 | // fixed module 254 | this.modules[this.moduleCount - 8][8] = (!test); 255 | 256 | }, 257 | 258 | mapData: function (data, maskPattern) { 259 | 260 | var inc = -1; 261 | var row = this.moduleCount - 1; 262 | var bitIndex = 7; 263 | var byteIndex = 0; 264 | 265 | for (var col = this.moduleCount - 1; col > 0; col -= 2) { 266 | 267 | if (col == 6) col--; 268 | 269 | while (true) { 270 | 271 | for (var c = 0; c < 2; c++) { 272 | 273 | if (this.modules[row][col - c] == null) { 274 | 275 | var dark = false; 276 | 277 | if (byteIndex < data.length) { 278 | dark = (((data[byteIndex] >>> bitIndex) & 1) == 1); 279 | } 280 | 281 | var mask = QRCode.Util.getMask(maskPattern, row, col - c); 282 | 283 | if (mask) { 284 | dark = !dark; 285 | } 286 | 287 | this.modules[row][col - c] = dark; 288 | bitIndex--; 289 | 290 | if (bitIndex == -1) { 291 | byteIndex++; 292 | bitIndex = 7; 293 | } 294 | } 295 | } 296 | 297 | row += inc; 298 | 299 | if (row < 0 || this.moduleCount <= row) { 300 | row -= inc; 301 | inc = -inc; 302 | break; 303 | } 304 | } 305 | } 306 | 307 | } 308 | 309 | }; 310 | 311 | QRCode.PAD0 = 0xEC; 312 | QRCode.PAD1 = 0x11; 313 | 314 | QRCode.createData = function (typeNumber, errorCorrectLevel, dataList) { 315 | 316 | var rsBlocks = QRCode.RSBlock.getRSBlocks(typeNumber, errorCorrectLevel); 317 | 318 | var buffer = new QRCode.BitBuffer(); 319 | 320 | for (var i = 0; i < dataList.length; i++) { 321 | var data = dataList[i]; 322 | buffer.put(data.mode, 4); 323 | buffer.put(data.getLength(), QRCode.Util.getLengthInBits(data.mode, typeNumber)); 324 | data.write(buffer); 325 | } 326 | 327 | // calc num max data. 328 | var totalDataCount = 0; 329 | for (var i = 0; i < rsBlocks.length; i++) { 330 | totalDataCount += rsBlocks[i].dataCount; 331 | } 332 | 333 | if (buffer.getLengthInBits() > totalDataCount * 8) { 334 | throw new Error("code length overflow. (" 335 | + buffer.getLengthInBits() 336 | + ">" 337 | + totalDataCount * 8 338 | + ")"); 339 | } 340 | 341 | // end code 342 | if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { 343 | buffer.put(0, 4); 344 | } 345 | 346 | // padding 347 | while (buffer.getLengthInBits() % 8 != 0) { 348 | buffer.putBit(false); 349 | } 350 | 351 | // padding 352 | while (true) { 353 | 354 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 355 | break; 356 | } 357 | buffer.put(QRCode.PAD0, 8); 358 | 359 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 360 | break; 361 | } 362 | buffer.put(QRCode.PAD1, 8); 363 | } 364 | 365 | return QRCode.createBytes(buffer, rsBlocks); 366 | }; 367 | 368 | QRCode.createBytes = function (buffer, rsBlocks) { 369 | 370 | var offset = 0; 371 | 372 | var maxDcCount = 0; 373 | var maxEcCount = 0; 374 | 375 | var dcdata = new Array(rsBlocks.length); 376 | var ecdata = new Array(rsBlocks.length); 377 | 378 | for (var r = 0; r < rsBlocks.length; r++) { 379 | 380 | var dcCount = rsBlocks[r].dataCount; 381 | var ecCount = rsBlocks[r].totalCount - dcCount; 382 | 383 | maxDcCount = Math.max(maxDcCount, dcCount); 384 | maxEcCount = Math.max(maxEcCount, ecCount); 385 | 386 | dcdata[r] = new Array(dcCount); 387 | 388 | for (var i = 0; i < dcdata[r].length; i++) { 389 | dcdata[r][i] = 0xff & buffer.buffer[i + offset]; 390 | } 391 | offset += dcCount; 392 | 393 | var rsPoly = QRCode.Util.getErrorCorrectPolynomial(ecCount); 394 | var rawPoly = new QRCode.Polynomial(dcdata[r], rsPoly.getLength() - 1); 395 | 396 | var modPoly = rawPoly.mod(rsPoly); 397 | ecdata[r] = new Array(rsPoly.getLength() - 1); 398 | for (var i = 0; i < ecdata[r].length; i++) { 399 | var modIndex = i + modPoly.getLength() - ecdata[r].length; 400 | ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0; 401 | } 402 | 403 | } 404 | 405 | var totalCodeCount = 0; 406 | for (var i = 0; i < rsBlocks.length; i++) { 407 | totalCodeCount += rsBlocks[i].totalCount; 408 | } 409 | 410 | var data = new Array(totalCodeCount); 411 | var index = 0; 412 | 413 | for (var i = 0; i < maxDcCount; i++) { 414 | for (var r = 0; r < rsBlocks.length; r++) { 415 | if (i < dcdata[r].length) { 416 | data[index++] = dcdata[r][i]; 417 | } 418 | } 419 | } 420 | 421 | for (var i = 0; i < maxEcCount; i++) { 422 | for (var r = 0; r < rsBlocks.length; r++) { 423 | if (i < ecdata[r].length) { 424 | data[index++] = ecdata[r][i]; 425 | } 426 | } 427 | } 428 | 429 | return data; 430 | 431 | }; 432 | 433 | //--------------------------------------------------------------------- 434 | // QR8bitByte 435 | //--------------------------------------------------------------------- 436 | QRCode.QR8bitByte = function (data) { 437 | this.mode = QRCode.Mode.MODE_8BIT_BYTE; 438 | this.data = data; 439 | } 440 | 441 | QRCode.QR8bitByte.prototype = { 442 | getLength: function (buffer) { 443 | return this.data.length; 444 | }, 445 | 446 | write: function (buffer) { 447 | for (var i = 0; i < this.data.length; i++) { 448 | // not JIS ... 449 | buffer.put(this.data.charCodeAt(i), 8); 450 | } 451 | } 452 | }; 453 | 454 | 455 | //--------------------------------------------------------------------- 456 | // QRMode 457 | //--------------------------------------------------------------------- 458 | QRCode.Mode = { 459 | MODE_NUMBER: 1 << 0, 460 | MODE_ALPHA_NUM: 1 << 1, 461 | MODE_8BIT_BYTE: 1 << 2, 462 | MODE_KANJI: 1 << 3 463 | }; 464 | 465 | //--------------------------------------------------------------------- 466 | // QRErrorCorrectLevel 467 | //--------------------------------------------------------------------- 468 | QRCode.ErrorCorrectLevel = { 469 | L: 1, 470 | M: 0, 471 | Q: 3, 472 | H: 2 473 | }; 474 | 475 | 476 | //--------------------------------------------------------------------- 477 | // QRMaskPattern 478 | //--------------------------------------------------------------------- 479 | QRCode.MaskPattern = { 480 | PATTERN000: 0, 481 | PATTERN001: 1, 482 | PATTERN010: 2, 483 | PATTERN011: 3, 484 | PATTERN100: 4, 485 | PATTERN101: 5, 486 | PATTERN110: 6, 487 | PATTERN111: 7 488 | }; 489 | 490 | //--------------------------------------------------------------------- 491 | // QRUtil 492 | //--------------------------------------------------------------------- 493 | 494 | QRCode.Util = { 495 | 496 | PATTERN_POSITION_TABLE: [ 497 | [], 498 | [6, 18], 499 | [6, 22], 500 | [6, 26], 501 | [6, 30], 502 | [6, 34], 503 | [6, 22, 38], 504 | [6, 24, 42], 505 | [6, 26, 46], 506 | [6, 28, 50], 507 | [6, 30, 54], 508 | [6, 32, 58], 509 | [6, 34, 62], 510 | [6, 26, 46, 66], 511 | [6, 26, 48, 70], 512 | [6, 26, 50, 74], 513 | [6, 30, 54, 78], 514 | [6, 30, 56, 82], 515 | [6, 30, 58, 86], 516 | [6, 34, 62, 90], 517 | [6, 28, 50, 72, 94], 518 | [6, 26, 50, 74, 98], 519 | [6, 30, 54, 78, 102], 520 | [6, 28, 54, 80, 106], 521 | [6, 32, 58, 84, 110], 522 | [6, 30, 58, 86, 114], 523 | [6, 34, 62, 90, 118], 524 | [6, 26, 50, 74, 98, 122], 525 | [6, 30, 54, 78, 102, 126], 526 | [6, 26, 52, 78, 104, 130], 527 | [6, 30, 56, 82, 108, 134], 528 | [6, 34, 60, 86, 112, 138], 529 | [6, 30, 58, 86, 114, 142], 530 | [6, 34, 62, 90, 118, 146], 531 | [6, 30, 54, 78, 102, 126, 150], 532 | [6, 24, 50, 76, 102, 128, 154], 533 | [6, 28, 54, 80, 106, 132, 158], 534 | [6, 32, 58, 84, 110, 136, 162], 535 | [6, 26, 54, 82, 110, 138, 166], 536 | [6, 30, 58, 86, 114, 142, 170] 537 | ], 538 | 539 | G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), 540 | G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), 541 | G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), 542 | 543 | getBCHTypeInfo: function (data) { 544 | var d = data << 10; 545 | while (QRCode.Util.getBCHDigit(d) - QRCode.Util.getBCHDigit(QRCode.Util.G15) >= 0) { 546 | d ^= (QRCode.Util.G15 << (QRCode.Util.getBCHDigit(d) - QRCode.Util.getBCHDigit(QRCode.Util.G15))); 547 | } 548 | return ((data << 10) | d) ^ QRCode.Util.G15_MASK; 549 | }, 550 | 551 | getBCHTypeNumber: function (data) { 552 | var d = data << 12; 553 | while (QRCode.Util.getBCHDigit(d) - QRCode.Util.getBCHDigit(QRCode.Util.G18) >= 0) { 554 | d ^= (QRCode.Util.G18 << (QRCode.Util.getBCHDigit(d) - QRCode.Util.getBCHDigit(QRCode.Util.G18))); 555 | } 556 | return (data << 12) | d; 557 | }, 558 | 559 | getBCHDigit: function (data) { 560 | 561 | var digit = 0; 562 | 563 | while (data != 0) { 564 | digit++; 565 | data >>>= 1; 566 | } 567 | 568 | return digit; 569 | }, 570 | 571 | getPatternPosition: function (typeNumber) { 572 | return QRCode.Util.PATTERN_POSITION_TABLE[typeNumber - 1]; 573 | }, 574 | 575 | getMask: function (maskPattern, i, j) { 576 | 577 | switch (maskPattern) { 578 | 579 | case QRCode.MaskPattern.PATTERN000: return (i + j) % 2 == 0; 580 | case QRCode.MaskPattern.PATTERN001: return i % 2 == 0; 581 | case QRCode.MaskPattern.PATTERN010: return j % 3 == 0; 582 | case QRCode.MaskPattern.PATTERN011: return (i + j) % 3 == 0; 583 | case QRCode.MaskPattern.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; 584 | case QRCode.MaskPattern.PATTERN101: return (i * j) % 2 + (i * j) % 3 == 0; 585 | case QRCode.MaskPattern.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 == 0; 586 | case QRCode.MaskPattern.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 == 0; 587 | 588 | default: 589 | throw new Error("bad maskPattern:" + maskPattern); 590 | } 591 | }, 592 | 593 | getErrorCorrectPolynomial: function (errorCorrectLength) { 594 | 595 | var a = new QRCode.Polynomial([1], 0); 596 | 597 | for (var i = 0; i < errorCorrectLength; i++) { 598 | a = a.multiply(new QRCode.Polynomial([1, QRCode.Math.gexp(i)], 0)); 599 | } 600 | 601 | return a; 602 | }, 603 | 604 | getLengthInBits: function (mode, type) { 605 | 606 | if (1 <= type && type < 10) { 607 | 608 | // 1 - 9 609 | 610 | switch (mode) { 611 | case QRCode.Mode.MODE_NUMBER: return 10; 612 | case QRCode.Mode.MODE_ALPHA_NUM: return 9; 613 | case QRCode.Mode.MODE_8BIT_BYTE: return 8; 614 | case QRCode.Mode.MODE_KANJI: return 8; 615 | default: 616 | throw new Error("mode:" + mode); 617 | } 618 | 619 | } else if (type < 27) { 620 | 621 | // 10 - 26 622 | 623 | switch (mode) { 624 | case QRCode.Mode.MODE_NUMBER: return 12; 625 | case QRCode.Mode.MODE_ALPHA_NUM: return 11; 626 | case QRCode.Mode.MODE_8BIT_BYTE: return 16; 627 | case QRCode.Mode.MODE_KANJI: return 10; 628 | default: 629 | throw new Error("mode:" + mode); 630 | } 631 | 632 | } else if (type < 41) { 633 | 634 | // 27 - 40 635 | 636 | switch (mode) { 637 | case QRCode.Mode.MODE_NUMBER: return 14; 638 | case QRCode.Mode.MODE_ALPHA_NUM: return 13; 639 | case QRCode.Mode.MODE_8BIT_BYTE: return 16; 640 | case QRCode.Mode.MODE_KANJI: return 12; 641 | default: 642 | throw new Error("mode:" + mode); 643 | } 644 | 645 | } else { 646 | throw new Error("type:" + type); 647 | } 648 | }, 649 | 650 | getLostPoint: function (qrCode) { 651 | 652 | var moduleCount = qrCode.getModuleCount(); 653 | 654 | var lostPoint = 0; 655 | 656 | // LEVEL1 657 | 658 | for (var row = 0; row < moduleCount; row++) { 659 | 660 | for (var col = 0; col < moduleCount; col++) { 661 | 662 | var sameCount = 0; 663 | var dark = qrCode.isDark(row, col); 664 | 665 | for (var r = -1; r <= 1; r++) { 666 | 667 | if (row + r < 0 || moduleCount <= row + r) { 668 | continue; 669 | } 670 | 671 | for (var c = -1; c <= 1; c++) { 672 | 673 | if (col + c < 0 || moduleCount <= col + c) { 674 | continue; 675 | } 676 | 677 | if (r == 0 && c == 0) { 678 | continue; 679 | } 680 | 681 | if (dark == qrCode.isDark(row + r, col + c)) { 682 | sameCount++; 683 | } 684 | } 685 | } 686 | 687 | if (sameCount > 5) { 688 | lostPoint += (3 + sameCount - 5); 689 | } 690 | } 691 | } 692 | 693 | // LEVEL2 694 | 695 | for (var row = 0; row < moduleCount - 1; row++) { 696 | for (var col = 0; col < moduleCount - 1; col++) { 697 | var count = 0; 698 | if (qrCode.isDark(row, col)) count++; 699 | if (qrCode.isDark(row + 1, col)) count++; 700 | if (qrCode.isDark(row, col + 1)) count++; 701 | if (qrCode.isDark(row + 1, col + 1)) count++; 702 | if (count == 0 || count == 4) { 703 | lostPoint += 3; 704 | } 705 | } 706 | } 707 | 708 | // LEVEL3 709 | 710 | for (var row = 0; row < moduleCount; row++) { 711 | for (var col = 0; col < moduleCount - 6; col++) { 712 | if (qrCode.isDark(row, col) 713 | && !qrCode.isDark(row, col + 1) 714 | && qrCode.isDark(row, col + 2) 715 | && qrCode.isDark(row, col + 3) 716 | && qrCode.isDark(row, col + 4) 717 | && !qrCode.isDark(row, col + 5) 718 | && qrCode.isDark(row, col + 6)) { 719 | lostPoint += 40; 720 | } 721 | } 722 | } 723 | 724 | for (var col = 0; col < moduleCount; col++) { 725 | for (var row = 0; row < moduleCount - 6; row++) { 726 | if (qrCode.isDark(row, col) 727 | && !qrCode.isDark(row + 1, col) 728 | && qrCode.isDark(row + 2, col) 729 | && qrCode.isDark(row + 3, col) 730 | && qrCode.isDark(row + 4, col) 731 | && !qrCode.isDark(row + 5, col) 732 | && qrCode.isDark(row + 6, col)) { 733 | lostPoint += 40; 734 | } 735 | } 736 | } 737 | 738 | // LEVEL4 739 | 740 | var darkCount = 0; 741 | 742 | for (var col = 0; col < moduleCount; col++) { 743 | for (var row = 0; row < moduleCount; row++) { 744 | if (qrCode.isDark(row, col)) { 745 | darkCount++; 746 | } 747 | } 748 | } 749 | 750 | var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; 751 | lostPoint += ratio * 10; 752 | 753 | return lostPoint; 754 | } 755 | 756 | }; 757 | 758 | 759 | //--------------------------------------------------------------------- 760 | // QRMath 761 | //--------------------------------------------------------------------- 762 | 763 | QRCode.Math = { 764 | 765 | glog: function (n) { 766 | 767 | if (n < 1) { 768 | throw new Error("glog(" + n + ")"); 769 | } 770 | 771 | return QRCode.Math.LOG_TABLE[n]; 772 | }, 773 | 774 | gexp: function (n) { 775 | 776 | while (n < 0) { 777 | n += 255; 778 | } 779 | 780 | while (n >= 256) { 781 | n -= 255; 782 | } 783 | 784 | return QRCode.Math.EXP_TABLE[n]; 785 | }, 786 | 787 | EXP_TABLE: new Array(256), 788 | 789 | LOG_TABLE: new Array(256) 790 | 791 | }; 792 | 793 | for (var i = 0; i < 8; i++) { 794 | QRCode.Math.EXP_TABLE[i] = 1 << i; 795 | } 796 | for (var i = 8; i < 256; i++) { 797 | QRCode.Math.EXP_TABLE[i] = QRCode.Math.EXP_TABLE[i - 4] 798 | ^ QRCode.Math.EXP_TABLE[i - 5] 799 | ^ QRCode.Math.EXP_TABLE[i - 6] 800 | ^ QRCode.Math.EXP_TABLE[i - 8]; 801 | } 802 | for (var i = 0; i < 255; i++) { 803 | QRCode.Math.LOG_TABLE[QRCode.Math.EXP_TABLE[i]] = i; 804 | } 805 | 806 | //--------------------------------------------------------------------- 807 | // QRPolynomial 808 | //--------------------------------------------------------------------- 809 | 810 | QRCode.Polynomial = function (num, shift) { 811 | 812 | if (num.length == undefined) { 813 | throw new Error(num.length + "/" + shift); 814 | } 815 | 816 | var offset = 0; 817 | 818 | while (offset < num.length && num[offset] == 0) { 819 | offset++; 820 | } 821 | 822 | this.num = new Array(num.length - offset + shift); 823 | for (var i = 0; i < num.length - offset; i++) { 824 | this.num[i] = num[i + offset]; 825 | } 826 | } 827 | 828 | QRCode.Polynomial.prototype = { 829 | 830 | get: function (index) { 831 | return this.num[index]; 832 | }, 833 | 834 | getLength: function () { 835 | return this.num.length; 836 | }, 837 | 838 | multiply: function (e) { 839 | 840 | var num = new Array(this.getLength() + e.getLength() - 1); 841 | 842 | for (var i = 0; i < this.getLength(); i++) { 843 | for (var j = 0; j < e.getLength(); j++) { 844 | num[i + j] ^= QRCode.Math.gexp(QRCode.Math.glog(this.get(i)) + QRCode.Math.glog(e.get(j))); 845 | } 846 | } 847 | 848 | return new QRCode.Polynomial(num, 0); 849 | }, 850 | 851 | mod: function (e) { 852 | 853 | if (this.getLength() - e.getLength() < 0) { 854 | return this; 855 | } 856 | 857 | var ratio = QRCode.Math.glog(this.get(0)) - QRCode.Math.glog(e.get(0)); 858 | 859 | var num = new Array(this.getLength()); 860 | 861 | for (var i = 0; i < this.getLength(); i++) { 862 | num[i] = this.get(i); 863 | } 864 | 865 | for (var i = 0; i < e.getLength(); i++) { 866 | num[i] ^= QRCode.Math.gexp(QRCode.Math.glog(e.get(i)) + ratio); 867 | } 868 | 869 | // recursive call 870 | return new QRCode.Polynomial(num, 0).mod(e); 871 | } 872 | }; 873 | 874 | //--------------------------------------------------------------------- 875 | // QRRSBlock 876 | //--------------------------------------------------------------------- 877 | 878 | QRCode.RSBlock = function (totalCount, dataCount) { 879 | this.totalCount = totalCount; 880 | this.dataCount = dataCount; 881 | } 882 | 883 | QRCode.RSBlock.RS_BLOCK_TABLE = [ 884 | 885 | // L 886 | // M 887 | // Q 888 | // H 889 | 890 | // 1 891 | [1, 26, 19], 892 | [1, 26, 16], 893 | [1, 26, 13], 894 | [1, 26, 9], 895 | 896 | // 2 897 | [1, 44, 34], 898 | [1, 44, 28], 899 | [1, 44, 22], 900 | [1, 44, 16], 901 | 902 | // 3 903 | [1, 70, 55], 904 | [1, 70, 44], 905 | [2, 35, 17], 906 | [2, 35, 13], 907 | 908 | // 4 909 | [1, 100, 80], 910 | [2, 50, 32], 911 | [2, 50, 24], 912 | [4, 25, 9], 913 | 914 | // 5 915 | [1, 134, 108], 916 | [2, 67, 43], 917 | [2, 33, 15, 2, 34, 16], 918 | [2, 33, 11, 2, 34, 12], 919 | 920 | // 6 921 | [2, 86, 68], 922 | [4, 43, 27], 923 | [4, 43, 19], 924 | [4, 43, 15], 925 | 926 | // 7 927 | [2, 98, 78], 928 | [4, 49, 31], 929 | [2, 32, 14, 4, 33, 15], 930 | [4, 39, 13, 1, 40, 14], 931 | 932 | // 8 933 | [2, 121, 97], 934 | [2, 60, 38, 2, 61, 39], 935 | [4, 40, 18, 2, 41, 19], 936 | [4, 40, 14, 2, 41, 15], 937 | 938 | // 9 939 | [2, 146, 116], 940 | [3, 58, 36, 2, 59, 37], 941 | [4, 36, 16, 4, 37, 17], 942 | [4, 36, 12, 4, 37, 13], 943 | 944 | // 10 945 | [2, 86, 68, 2, 87, 69], 946 | [4, 69, 43, 1, 70, 44], 947 | [6, 43, 19, 2, 44, 20], 948 | [6, 43, 15, 2, 44, 16] 949 | 950 | ]; 951 | 952 | QRCode.RSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) { 953 | 954 | var rsBlock = QRCode.RSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); 955 | 956 | if (rsBlock == undefined) { 957 | throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel); 958 | } 959 | 960 | var length = rsBlock.length / 3; 961 | 962 | var list = new Array(); 963 | 964 | for (var i = 0; i < length; i++) { 965 | 966 | var count = rsBlock[i * 3 + 0]; 967 | var totalCount = rsBlock[i * 3 + 1]; 968 | var dataCount = rsBlock[i * 3 + 2]; 969 | 970 | for (var j = 0; j < count; j++) { 971 | list.push(new QRCode.RSBlock(totalCount, dataCount)); 972 | } 973 | } 974 | 975 | return list; 976 | }; 977 | 978 | QRCode.RSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) { 979 | 980 | switch (errorCorrectLevel) { 981 | case QRCode.ErrorCorrectLevel.L: 982 | return QRCode.RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; 983 | case QRCode.ErrorCorrectLevel.M: 984 | return QRCode.RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; 985 | case QRCode.ErrorCorrectLevel.Q: 986 | return QRCode.RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; 987 | case QRCode.ErrorCorrectLevel.H: 988 | return QRCode.RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; 989 | default: 990 | return undefined; 991 | } 992 | }; 993 | 994 | //--------------------------------------------------------------------- 995 | // QRBitBuffer 996 | //--------------------------------------------------------------------- 997 | 998 | QRCode.BitBuffer = function () { 999 | this.buffer = new Array(); 1000 | this.length = 0; 1001 | } 1002 | 1003 | QRCode.BitBuffer.prototype = { 1004 | 1005 | get: function (index) { 1006 | var bufIndex = Math.floor(index / 8); 1007 | return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1; 1008 | }, 1009 | 1010 | put: function (num, length) { 1011 | for (var i = 0; i < length; i++) { 1012 | this.putBit(((num >>> (length - i - 1)) & 1) == 1); 1013 | } 1014 | }, 1015 | 1016 | getLengthInBits: function () { 1017 | return this.length; 1018 | }, 1019 | 1020 | putBit: function (bit) { 1021 | 1022 | var bufIndex = Math.floor(this.length / 8); 1023 | if (this.buffer.length <= bufIndex) { 1024 | this.buffer.push(0); 1025 | } 1026 | 1027 | if (bit) { 1028 | this.buffer[bufIndex] |= (0x80 >>> (this.length % 8)); 1029 | } 1030 | 1031 | this.length++; 1032 | } 1033 | }; 1034 | })(); -------------------------------------------------------------------------------- /data/js/paypopup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * paypopup.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Controls paypopup.html, the popup that appears when clicking on bitcoin pay links, 9 | * or by clicking the context menu 10 | */ 11 | 12 | $(document).ready(function () { 13 | var SATOSHIS = 100000000, 14 | FEE = SATOSHIS * .0001, 15 | BTCUnits = 'BTC', 16 | BTCMultiplier = SATOSHIS, 17 | clickX, 18 | clickY, 19 | port = null; 20 | 21 | // Event is broadcast when context menu is opened on the page 22 | $(document).on('contextmenu', function (e) { 23 | // Save the position of the right click to use for positioning the popup 24 | clickX = e.clientX; 25 | clickY = e.clientY; 26 | if (typeof chrome !== 'undefined') { 27 | // In Chrome we open a port with the background script 28 | // to tell us when the menu item is clicked 29 | if (port) { 30 | port.disconnect(); 31 | } 32 | port = chrome.runtime.connect(); 33 | port.onMessage.addListener(function(response) { 34 | var rect = null; 35 | if (response.address) { 36 | // We only have an address in Chrome if it was selected by right clicking, 37 | // so we can get the location of the address by finding the selection 38 | rect = window.getSelection().getRangeAt(0).getBoundingClientRect(); 39 | } 40 | showPopup(response.address, null, rect); 41 | }); 42 | } 43 | }); 44 | 45 | if (typeof chrome === 'undefined') { 46 | // In Firefox we listen for the pay message to be sent 47 | self.port.on('pay', function (message) { 48 | if (message.address) { 49 | // If we have an address, the position of the address is sent as well 50 | var rect = {}; 51 | rect.left = message.left; 52 | rect.right = message.right; 53 | rect.top = message.top; 54 | rect.bottom = message.bottom; 55 | showPopup(message.address, null, rect); 56 | } else { 57 | showPopup(null, null, null); 58 | } 59 | }); 60 | } 61 | 62 | // Intercept all anchor clicks and determine if they are bitcoin pay links 63 | $('body').on('click', 'a', function (e) { 64 | var href = $(this).attr('href'); 65 | // Regex test for bitcoin pay link 66 | if (/^bitcoin:[13][1-9A-HJ-NP-Za-km-z]{26,33}/.test(href)) { 67 | var addresses = href.match(/[13][1-9A-HJ-NP-Za-km-z]{26,33}/); 68 | var address = null; 69 | if (addresses) { 70 | address = addresses[0]; 71 | } 72 | var amounts = href.match(/amount=\d+\.?\d*/); 73 | var amount = null; 74 | if (amounts) { 75 | amount = Number(amounts[0].substring(7)) * SATOSHIS; 76 | } 77 | showPopup(address, amount, this.getBoundingClientRect()); 78 | return false; 79 | } 80 | // Return true if not a bitcoin link so click will work normally 81 | return true; 82 | }); 83 | 84 | function showPopup(address, amount, rect) { 85 | util.iframe('paypopup.html').then(function (iframe) { 86 | 87 | iframe.style.height = '210px'; 88 | iframe.style.width = '210px'; 89 | var offset = {} 90 | if (rect) { 91 | offset.left = Number(rect.left) + Number(window.pageXOffset) + Number(rect.right-rect.left)/2 - 85; 92 | offset.top = Number(rect.bottom) + Number(window.pageYOffset); 93 | } else { 94 | offset.left = Number(clickX) + Number(window.pageXOffset); 95 | offset.top = Number(clickY) + Number(window.pageYOffset); 96 | } 97 | iframe.style.left = offset.left + 'px'; 98 | iframe.style.top = offset.top + 'px'; 99 | 100 | var $iframe = $(iframe.contentWindow.document); 101 | 102 | wallet.restoreAddress().then(function () { 103 | if (wallet.isEncrypted()) { 104 | // Only show password field if the wallet is encrypted 105 | $iframe.find('#password').parent().show(); 106 | } 107 | }, function () { 108 | wallet.generateAddress(); 109 | }); 110 | 111 | preferences.getBTCUnits().then(function (units) { 112 | BTCUnits = units; 113 | if (units === 'µBTC') { 114 | BTCMultiplier = SATOSHIS / 1000000; 115 | } else if (units === 'mBTC') { 116 | BTCMultiplier = SATOSHIS / 1000; 117 | } else { 118 | BTCMultiplier = SATOSHIS; 119 | } 120 | $iframe.find('#amount').attr('placeholder', 'Amount (' + BTCUnits + ')').attr('step', 100000 / BTCMultiplier); 121 | }); 122 | 123 | // Check if the address is actually valid 124 | if (!address || !/^[13][1-9A-HJ-NP-Za-km-z]{26,33}$/.test(String(address))) { 125 | address = null; 126 | } else { 127 | try { 128 | new Bitcoin.Address(address); 129 | } catch (e) { 130 | address = null; 131 | } 132 | } 133 | 134 | // Hide the address field if we have a valid address, 135 | // else hide the arrow pointing to an address 136 | if (address) { 137 | $iframe.find('#address').val(address).parent().hide(); 138 | } else { 139 | $iframe.find('.arrow').hide(); 140 | } 141 | 142 | // Hide the amount field if we have a valid amount 143 | if (amount) { 144 | $iframe.find('#amount').parent().hide(); 145 | updateButton(amount); 146 | } else { 147 | $iframe.find('#amount').on('keyup change', function () { 148 | var value = Math.floor(Number($iframe.find('#amount').val() * BTCMultiplier)); 149 | updateButton(value); 150 | }); 151 | } 152 | 153 | function updateButton(value) { 154 | currencyManager.formatAmount(value).then(function (formattedMoney) { 155 | var text = 'Send'; 156 | if (value > 0) { 157 | text += ' (' + formattedMoney + ')'; 158 | } 159 | $iframe.find('#button').text(text); 160 | }); 161 | } 162 | 163 | $iframe.find('#main').fadeIn('fast'); 164 | 165 | $iframe.find('#button').click(function () { 166 | var validAmount = true, 167 | validAddress = true, 168 | newAmount; 169 | if (!amount) { 170 | newAmount = Math.floor(Number($iframe.find('#amount').val() * BTCMultiplier)); 171 | } else { 172 | newAmount = amount; 173 | } 174 | var balance = wallet.getBalance(); 175 | if (newAmount <= 0) { 176 | validAmount = false; 177 | } else if (newAmount + FEE > balance) { 178 | validAmount = false; 179 | } 180 | 181 | var newAddress; 182 | if (!address) { 183 | newAddress = $iframe.find('#address').val(); 184 | if (!/^[13][1-9A-HJ-NP-Za-km-z]{26,33}$/.test(String(newAddress))) { 185 | validAddress = false; 186 | } else { 187 | try { 188 | new Bitcoin.Address(newAddress); 189 | } catch (e) { 190 | validAddress = false; 191 | } 192 | } 193 | } else { 194 | newAddress = address; 195 | } 196 | 197 | $iframe.find('#amount').parent().removeClass('has-error'); 198 | $iframe.find('#address').parent().removeClass('has-error'); 199 | $iframe.find('#password').parent().removeClass('has-error'); 200 | if (!validAddress) { 201 | $iframe.find('#errorAlert').text('Invalid address').slideDown(); 202 | $iframe.find('#address').parent().addClass('has-error'); 203 | } else if (!validAmount) { 204 | $iframe.find('#errorAlert').text('Insufficient funds').slideDown(); 205 | $iframe.find('#amount').parent().addClass('has-error'); 206 | } else if (!navigator.onLine) { 207 | $iframe.find('#errorAlert').text('Connection offline').slideDown(); 208 | $iframe.find('#amount').parent().addClass('has-error'); 209 | } else { 210 | $(document).off('click.wallet contextmenu.wallet'); 211 | $iframe.find('#errorAlert').slideUp(); 212 | $iframe.find('#amount').parent().fadeOut('fast'); 213 | $iframe.find('#address').parent().fadeOut('fast'); 214 | $iframe.find('#password').parent().fadeOut('fast'); 215 | $iframe.find('#button').fadeOut('fast', function () { 216 | $iframe.find('#progress').fadeIn('fast', function () { 217 | wallet.send(newAddress, newAmount, FEE, $iframe.find('#password').val()).then(function () { 218 | $iframe.find('#progress').fadeOut('fast', function () { 219 | $iframe.find('#successAlert').fadeIn('fast').delay(1000).fadeIn('fast', removeFrame); 220 | }); 221 | }, function (e) { 222 | $iframe.find('#progress').fadeOut('fast', function () { 223 | if (e.message === 'Incorrect password') { 224 | $iframe.find('#password').parent().addClass('has-error'); 225 | } else if (e.message === 'Insufficient funds') { 226 | $iframe.find('#amount').parent().addClass('has-error'); 227 | } 228 | $iframe.find('#errorAlert').text(e.message).slideDown(); 229 | if (!address) { 230 | $iframe.find('#address').parent().fadeIn(); 231 | } 232 | if (!amount) { 233 | $iframe.find('#amount').parent().fadeIn(); 234 | } 235 | if (wallet.isEncrypted()) { 236 | $iframe.find('#password').parent().fadeIn(); 237 | } 238 | $iframe.find('#button').fadeIn(); 239 | $(document).on('click.wallet contextmenu.wallet', removeFrame); 240 | }); 241 | }); 242 | }); 243 | }); 244 | } 245 | }); 246 | 247 | $(document).on('click.wallet contextmenu.wallet', removeFrame); 248 | 249 | function removeFrame() { 250 | $(document).off('click.wallet contextmenu.wallet'); 251 | $(iframe).fadeOut('fast', function () { 252 | $(this).remove(); 253 | }); 254 | } 255 | }); 256 | } 257 | 258 | 259 | }); -------------------------------------------------------------------------------- /data/js/preferences.js: -------------------------------------------------------------------------------- 1 | /** 2 | * preferences.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Preferences handles storing and retrieving saved values 9 | */ 10 | 11 | (function (window) { 12 | 13 | var ADDRESS = "wallet.address", 14 | PRIVATE_KEY = "wallet.private_key", 15 | IS_ENCRYPTED = "wallet.is_encrypted", 16 | LAST_BALANCE = "wallet.last_balance", 17 | EXCHANGE_RATE = 'wallet.exchange_rate', 18 | BTC_UNITS = 'wallet.btc_units', 19 | CURRENCY = 'wallet.currency', 20 | preferences = function() {}; 21 | 22 | function sync() { 23 | return new Promise(function (resolve) { 24 | // Different APIs for Chrome and Firefox 25 | if (typeof chrome !== 'undefined') { 26 | var object = {}; 27 | object[ADDRESS] = ''; 28 | object[PRIVATE_KEY] = ''; 29 | object[IS_ENCRYPTED] = false; 30 | object[LAST_BALANCE] = 0; 31 | object[EXCHANGE_RATE] = 0; 32 | object[BTC_UNITS] = 'BTC'; 33 | object[CURRENCY] = 'USD'; 34 | chrome.storage.sync.get(object, resolve); 35 | } else { 36 | util.message('get').then(function (message) { 37 | if (typeof message[PRIVATE_KEY] === 'undefined') { 38 | message[ADDRESS] = ''; 39 | message[PRIVATE_KEY] = ''; 40 | message[IS_ENCRYPTED] = false; 41 | message[LAST_BALANCE] = 0; 42 | message[EXCHANGE_RATE] = 0; 43 | message[BTC_UNITS] = 'BTC'; 44 | message[CURRENCY] = 'USD'; 45 | return util.message('save', message); 46 | } else { 47 | return message; 48 | } 49 | }).then(function (message) { 50 | resolve(message); 51 | }); 52 | } 53 | }); 54 | } 55 | 56 | function get(pref) { 57 | return function () { 58 | return sync().then(function (values) { 59 | return values[pref]; 60 | }); 61 | }; 62 | }; 63 | 64 | function set(key, value) { 65 | return new Promise(function (resolve) { 66 | var object = {}; 67 | object[key] = value; 68 | // Different APIs for Chrome and Firefox 69 | if (typeof chrome !== 'undefined') { 70 | chrome.storage.sync.set(object, resolve); 71 | } else { 72 | util.message('save', object).then(resolve); 73 | } 74 | }); 75 | }; 76 | 77 | preferences.prototype = { 78 | 79 | getAddress: get(ADDRESS), 80 | setAddress: function (address) { 81 | return set(ADDRESS, address); 82 | }, 83 | 84 | getPrivateKey: get(PRIVATE_KEY), 85 | setPrivateKey: function (privateKey) { 86 | return set(PRIVATE_KEY, privateKey); 87 | }, 88 | 89 | getIsEncrypted: get(IS_ENCRYPTED), 90 | setIsEncrypted: function (isEncrypted) { 91 | return set(IS_ENCRYPTED, isEncrypted); 92 | }, 93 | 94 | getLastBalance: get(LAST_BALANCE), 95 | setLastBalance: function (lastBalance) { 96 | return set(LAST_BALANCE, lastBalance); 97 | }, 98 | 99 | getExchangeRate: get(EXCHANGE_RATE), 100 | setExchangeRate: function (exchangeRate) { 101 | return set(EXCHANGE_RATE, exchangeRate); 102 | }, 103 | 104 | getBTCUnits: get(BTC_UNITS), 105 | setBTCUnits: function (btcUnits) { 106 | return set(BTC_UNITS, btcUnits); 107 | }, 108 | 109 | getCurrency: get(CURRENCY), 110 | setCurrency: function (currency) { 111 | return set(CURRENCY, currency).then(function () { 112 | currencyManager.updateExchangeRate(); 113 | }); 114 | } 115 | }; 116 | 117 | window.preferences = new preferences(); 118 | 119 | })(window); -------------------------------------------------------------------------------- /data/js/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * util.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Utility methods 9 | */ 10 | 11 | (function (window) { 12 | 13 | var util = function () {}, 14 | // Promisified ajax request 15 | request = function (url, type, data) { 16 | return new Promise(function (resolve, reject) { 17 | var req = new XMLHttpRequest(); 18 | req.open((type ? type : 'GET'), url, true); 19 | req.onload = function () { 20 | if (req.status == 200) { 21 | resolve(req.response); 22 | } else { 23 | reject(Error(req.statusText)); 24 | } 25 | } 26 | req.onerror = function () { 27 | reject(Error('Network error')); 28 | } 29 | if (type === 'POST') { 30 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 31 | } 32 | req.send(data); 33 | }); 34 | }; 35 | 36 | util.prototype = { 37 | getJSON: function (url) { 38 | if (typeof chrome !== 'undefined') { 39 | return request(url).then(JSON.parse); 40 | } else { 41 | return ret.message('getJSON', url); 42 | } 43 | }, 44 | 45 | get: function (url) { 46 | return request(url); 47 | }, 48 | 49 | post: function (url, data) { 50 | if (typeof chrome !== 'undefined') { 51 | return request(url, 'POST', data); 52 | } else { 53 | return ret.message('post', {url:url, content:data}); 54 | } 55 | }, 56 | 57 | // Used to send messages from content scripts to add-on scripts and return values to content scripts in Firefox add-on 58 | message: function (name, value) { 59 | return new Promise(function (resolve) { 60 | // 'self' can also be 'addon' depending on how script is injected 61 | var ref = (typeof addon === 'undefined' ? self : addon); 62 | ref.port.on(name, resolve); 63 | ref.port.emit(name, value); 64 | }); 65 | } 66 | }; 67 | 68 | var ret = new util(); 69 | 70 | // Different workarounds to inject content into iFrames for Chrome and Firefox 71 | util.prototype.iframe = function (src) { 72 | return new Promise(function (resolve) { 73 | var iframe = document.createElement('iframe'); 74 | document.body.appendChild(iframe); 75 | iframe.setAttribute('style', 'background-color: transparent; position: absolute; z-index: 2147483647; border: 0px;'); 76 | iframe.setAttribute('allowtransparency', 'true'); 77 | iframe.frameBorder = '0'; 78 | if (typeof chrome !== 'undefined') { 79 | // For Chrome get the HTML content with an ajax call and write it into the document 80 | iframe.src = 'about:blank'; 81 | var request = new XMLHttpRequest(); 82 | request.open('GET', chrome.extension.getURL('data/' + src), false); 83 | request.send(null); 84 | var text = request.response; 85 | // Replace css relative locations with absolute locations since Chrome won't find relative 86 | text = text.replace(/css\//g, chrome.extension.getURL('') + 'data/css/'); 87 | iframe.contentWindow.document.open('text/html', 'replace'); 88 | iframe.contentWindow.document.write(text); 89 | iframe.contentWindow.document.close(); 90 | resolve(iframe); 91 | } else { 92 | // For Firefox get the encoded HTML and set it to the iFrame's src 93 | ret.message('html', src).then(function (url) { 94 | iframe.src = url; 95 | // Only way to reliably know when the frame is ready in Firefox is by polling 96 | function pollReady() { 97 | if (!iframe.contentWindow.document.getElementById('progress')) { 98 | setTimeout(pollReady, 100); 99 | } else { 100 | resolve(iframe); 101 | } 102 | } 103 | pollReady(); 104 | }); 105 | } 106 | }); 107 | } 108 | 109 | window.util = ret; 110 | 111 | })(window); -------------------------------------------------------------------------------- /data/js/wallet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * wallet.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Wallet handles the address, private key and encryption, 9 | * as well as sending and determining balance 10 | */ 11 | 12 | (function (window) { 13 | var balance = 0, 14 | address = '', 15 | privateKey = '', 16 | isEncrypted = false, 17 | websocket = null, 18 | balanceListener = null; 19 | 20 | var wallet = function () {}; 21 | wallet.prototype = { 22 | 23 | getAddress: function () { 24 | return address; 25 | }, 26 | 27 | getBalance: function () { 28 | return balance; 29 | }, 30 | 31 | isEncrypted: function () { 32 | return isEncrypted; 33 | }, 34 | 35 | // Balance listener gets called with new balance whenever it updates 36 | setBalanceListener: function (listener) { 37 | balanceListener = listener; 38 | }, 39 | 40 | // Create a new address 41 | generateAddress: function (password) { 42 | return new Promise(function (resolve, reject) { 43 | if (ret.validatePassword(password)) { 44 | var eckey = new Bitcoin.ECKey(false); 45 | if (isEncrypted) { 46 | if (typeof chrome !== 'undefined') { 47 | privateKey = CryptoJS.AES.encrypt(eckey.getExportedPrivateKey(), password); 48 | } else { 49 | privateKey = JSON.parse(CryptoJS.AES.encrypt(eckey.getExportedPrivateKey(), password, {format:jsonFormatter})); 50 | } 51 | } else { 52 | privateKey = eckey.getExportedPrivateKey(); 53 | } 54 | address = eckey.getBitcoinAddress().toString(); 55 | balance = 0; 56 | Promise.all([preferences.setAddress(address), preferences.setPrivateKey(privateKey), preferences.setIsEncrypted(isEncrypted)]).then(function () { 57 | updateBalance() 58 | resolve(); 59 | }); 60 | } else { 61 | reject(Error('Incorrect password')); 62 | } 63 | }); 64 | }, 65 | 66 | // Restore the previously saved address 67 | restoreAddress: function () { 68 | return new Promise(function (resolve, reject) { 69 | Promise.all([preferences.getAddress(), preferences.getPrivateKey(), preferences.getIsEncrypted()]).then(function (values) { 70 | if (values[0].length > 0) { 71 | address = values[0]; 72 | privateKey = values[1]; 73 | isEncrypted = values[2]; 74 | updateBalance(); 75 | resolve(); 76 | } else { 77 | reject(Error('No address')); 78 | } 79 | }); 80 | }); 81 | }, 82 | 83 | // Import an address using a private key 84 | importAddress: function (password, _privateKey) { 85 | return new Promise(function (resolve, reject) { 86 | if (ret.validatePassword(password)) { 87 | try { 88 | var eckey = new Bitcoin.ECKey(_privateKey); 89 | if (isEncrypted) { 90 | if (typeof chrome !== 'undefined') { 91 | privateKey = CryptoJS.AES.encrypt(eckey.getExportedPrivateKey(), password); 92 | } else { 93 | privateKey = JSON.parse(CryptoJS.AES.encrypt(eckey.getExportedPrivateKey(), password, {format:jsonFormatter})); 94 | } 95 | } else { 96 | privateKey = eckey.getExportedPrivateKey(); 97 | } 98 | address = eckey.getBitcoinAddress().toString(); 99 | balance = 0; 100 | Promise.all([preferences.setAddress(address), preferences.setPrivateKey(privateKey), preferences.setLastBalance(0)]).then(function () { 101 | updateBalance(); 102 | resolve(); 103 | }); 104 | } catch (e) { 105 | reject(Error('Invalid private key')); 106 | } 107 | } else { 108 | reject(Error('Incorrect password')); 109 | } 110 | }); 111 | }, 112 | 113 | // Check if the password is valid 114 | validatePassword: function (password) { 115 | if (isEncrypted) { 116 | try { 117 | // If we can decrypt the private key with the password, then the password is correct 118 | // We never store a copy of the password anywhere 119 | if (typeof chrome !== 'undefined') { 120 | return CryptoJS.AES.decrypt(privateKey, password).toString(CryptoJS.enc.Utf8); 121 | } else { 122 | return CryptoJS.AES.decrypt(JSON.stringify(privateKey), password, {format:jsonFormatter}).toString(CryptoJS.enc.Utf8); 123 | } 124 | } catch (e) { 125 | return false; 126 | } 127 | } else { 128 | return true; 129 | } 130 | }, 131 | 132 | // Return a decrypted private key using the password 133 | getDecryptedPrivateKey: function (password) { 134 | if (isEncrypted) { 135 | if (typeof chrome !== 'undefined') { 136 | var decryptedPrivateKey = CryptoJS.AES.decrypt(privateKey, password); 137 | } else { 138 | var decryptedPrivateKey = CryptoJS.AES.decrypt(JSON.stringify(privateKey), password, {format:jsonFormatter}); 139 | } 140 | try { 141 | if (!decryptedPrivateKey.toString(CryptoJS.enc.Utf8)) { 142 | return null; 143 | } 144 | } catch (e) { 145 | return null; 146 | } 147 | return decryptedPrivateKey.toString(CryptoJS.enc.Utf8); 148 | } else { 149 | return privateKey; 150 | } 151 | } 152 | 153 | }; 154 | 155 | // Gets the current balance and sets up a websocket to monitor new transactions 156 | function updateBalance() { 157 | // Make sure we have an address 158 | if (address.length) { 159 | // Last stored balance is the fastest way to update 160 | preferences.getLastBalance().then(function (result) { 161 | balance = result; 162 | if (balanceListener) balanceListener(balance); 163 | // Check blockchain.info for the current balance 164 | util.get('https://blockchain.info/q/addressbalance/' + address).then(function (response) { 165 | balance = response; 166 | return preferences.setLastBalance(balance); 167 | }).then(function () { 168 | if (balanceListener) balanceListener(balance); 169 | // Close the websocket if it was still open 170 | if (websocket) { 171 | websocket.close(); 172 | } 173 | // Create a new websocket to blockchain.info 174 | websocket = new WebSocket("ws://ws.blockchain.info:8335/inv"); 175 | websocket.onopen = function() { 176 | // Tell the websocket we want to monitor the address 177 | websocket.send('{"op":"addr_sub", "addr":"' + address + '"}'); 178 | }; 179 | websocket.onmessage = function (evt) { 180 | // Parse the new transaction 181 | var json = JSON.parse(evt.data); 182 | var inputs = json.x.inputs; 183 | var outputs = json.x.out; 184 | var i; 185 | // Subtract all inputs from the balance 186 | for (i = 0; i < inputs.length; i++) { 187 | var input = inputs[i].prev_out; 188 | if (input.addr === address) { 189 | balance = Number(balance) - Number(input.value); 190 | } 191 | } 192 | // Add all output to the balance 193 | for (i = 0; i < outputs.length; i++) { 194 | var output = outputs[i]; 195 | if (output.addr === address) { 196 | balance = Number(balance) + Number(output.value); 197 | } 198 | } 199 | // Save the new balance and notify the listener 200 | preferences.setLastBalance(balance).then(function () { 201 | if (balanceListener) balanceListener(balance); 202 | }); 203 | }; 204 | }); 205 | }); 206 | } 207 | } 208 | 209 | var ret = new wallet(); 210 | 211 | // Change the password to a new password 212 | wallet.prototype.updatePassword = function (password, newPassword) { 213 | return new Promise(function (resolve, reject) { 214 | // Make sure the previous password is correct 215 | var decryptedPrivateKey = ret.getDecryptedPrivateKey(password); 216 | if (decryptedPrivateKey) { 217 | // If we have a new password we use it, otherwise leave cleartext 218 | if (newPassword) { 219 | if (typeof chrome !== 'undefined') { 220 | privateKey = CryptoJS.AES.encrypt(eckey.getExportedPrivateKey(), newPassword); 221 | } else { 222 | privateKey = JSON.parse(CryptoJS.AES.encrypt(decryptedPrivateKey, newPassword, {format:jsonFormatter})); 223 | } 224 | isEncrypted = true; 225 | } else { 226 | privateKey = decryptedPrivateKey; 227 | isEncrypted = false; 228 | } 229 | // Save the encrypted private key 230 | // Passwords are never saved anywhere 231 | Promise.all([preferences.setIsEncrypted(isEncrypted), preferences.setPrivateKey(privateKey)]).then(resolve); 232 | } else { 233 | reject(Error('Incorrect password')); 234 | } 235 | }); 236 | }; 237 | 238 | // Send bitcoin from the wallet to another address 239 | wallet.prototype.send = function (sendAddress, amount, fee, password) { 240 | return new Promise(function (resolve, reject) { 241 | var decryptedPrivateKey = ret.getDecryptedPrivateKey(password); 242 | if (decryptedPrivateKey) { 243 | // Get all unspent outputs from blockchain.info to generate our inputs 244 | util.getJSON('https://blockchain.info/unspent?address=' + address).then(function (json) { 245 | var inputs = json.unspent_outputs, 246 | selectedOuts = [], 247 | eckey = new Bitcoin.ECKey(decryptedPrivateKey), 248 | // Total cost is amount plus fee 249 | totalInt = Number(amount) + Number(fee), 250 | txValue = new BigInteger('' + totalInt, 10), 251 | availableValue = BigInteger.ZERO; 252 | // Gather enough inputs so that their value is greater than or equal to the total cost 253 | for (var i = 0; i < inputs.length; i++) { 254 | selectedOuts.push(inputs[i]); 255 | availableValue = availableValue.add(new BigInteger('' + inputs[i].value, 10)); 256 | if (availableValue.compareTo(txValue) >= 0) break; 257 | } 258 | // If there aren't enough unspent outputs to available then we can't send the transaction 259 | if (availableValue.compareTo(txValue) < 0) { 260 | reject(Error('Insufficient funds')); 261 | } else { 262 | // Create the transaction 263 | var sendTx = new Bitcoin.Transaction(); 264 | // Add all our unspent outputs to the transaction as the inputs 265 | for (i = 0; i < selectedOuts.length; i++) { 266 | var hash = Crypto.util.bytesToBase64(Crypto.util.hexToBytes(selectedOuts[i].tx_hash)); 267 | var script = new Bitcoin.Script(Crypto.util.hexToBytes(selectedOuts[i].script)); 268 | var txin = new Bitcoin.TransactionIn({ 269 | outpoint: { 270 | hash: hash, 271 | index: selectedOuts[i].tx_output_n 272 | }, 273 | script: script, 274 | sequence: 4294967295 275 | }); 276 | sendTx.addInput(txin); 277 | } 278 | // Add the send address to the transaction as the output 279 | sendTx.addOutput(new Bitcoin.Address(sendAddress), new BigInteger('' + amount, 10)); 280 | // Add any leftover value to the transaction as an output pointing back to this wallet, 281 | // minus the fee of course 282 | var changeValue = availableValue.subtract(txValue); 283 | if (changeValue.compareTo(BigInteger.ZERO) > 0) { 284 | sendTx.addOutput(eckey.getBitcoinAddress(), changeValue); 285 | } 286 | // Sign all the input hashes 287 | var hashType = 1; // SIGHASH_ALL 288 | for (i = 0; i < sendTx.ins.length; i++) { 289 | var connectedScript = sendTx.ins[i].script; 290 | hash = sendTx.hashTransactionForSignature(connectedScript, i, hashType); 291 | var signature = eckey.sign(hash); 292 | signature.push(parseInt(hashType, 10)); 293 | var pubKey = eckey.getPub(); 294 | script = new Bitcoin.Script(); 295 | script.writeBytes(signature); 296 | script.writeBytes(pubKey); 297 | sendTx.ins[i].script = script; 298 | } 299 | // Push the transaction to blockchain.info 300 | var data = 'tx=' + Crypto.util.bytesToHex(sendTx.serialize()); 301 | util.post('https://blockchain.info/pushtx', data).then(function () { 302 | // Notify the balance listener of the changed amount immediately, 303 | // but don't set the balance since the transaction will be processed by the websocket 304 | if (balanceListener) balanceListener(balance - amount - fee); 305 | resolve(); 306 | }, function () { 307 | reject(Error('Unknown error')); 308 | }); 309 | } 310 | }, function () { 311 | reject(Error('Unknown error')); 312 | }); 313 | } else { 314 | reject(Error('Incorrect password')); 315 | } 316 | }); 317 | }; 318 | 319 | var jsonFormatter = { 320 | stringify: function (cipherParams) { 321 | // create json object with ciphertext 322 | var jsonObj = { 323 | ct: cipherParams.ciphertext.toString(CryptoJS.enc.Hex) 324 | }; 325 | 326 | // optionally add iv and salt 327 | if (cipherParams.iv) { 328 | jsonObj.iv = cipherParams.iv.toString(); 329 | } 330 | if (cipherParams.salt) { 331 | jsonObj.s = cipherParams.salt.toString(); 332 | } 333 | 334 | // stringify json object 335 | return JSON.stringify(jsonObj); 336 | }, 337 | 338 | parse: function (jsonStr) { 339 | // parse json string 340 | var jsonObj = JSON.parse(jsonStr); 341 | 342 | // extract ciphertext from json object, and create cipher params object 343 | var cipherParams = CryptoJS.lib.CipherParams.create({ 344 | ciphertext: CryptoJS.enc.Hex.parse(jsonObj.ct) 345 | }); 346 | 347 | // optionally extract iv and salt 348 | if (jsonObj.iv) { 349 | cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv) 350 | } 351 | if (jsonObj.s) { 352 | cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.s) 353 | } 354 | 355 | return cipherParams; 356 | } 357 | }; 358 | 359 | window.wallet = ret; 360 | })(window); -------------------------------------------------------------------------------- /data/paypopup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bitcoin Wallet Extension 5 | 6 | 7 | 8 | 9 | 10 | 11 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * main.js 3 | * Copyright (c) 2014 Andrew Toth 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the MIT license. 7 | * 8 | * Main file for Firefox add-on 9 | */ 10 | 11 | (function () { 12 | var self = require('sdk/self'), 13 | data = self.data 14 | 15 | // Create the wallet panel 16 | var walletPanel = require('sdk/panel').Panel({ 17 | width:362, 18 | height:278, 19 | contentURL: data.url('index.html'), 20 | onShow: function () { 21 | walletPanel.port.emit('show'); 22 | walletPanel.port.emit('version', self.version); 23 | } 24 | }); 25 | addListeners(walletPanel); 26 | 27 | walletPanel.port.on('resize', function (height) { 28 | walletPanel.resize(walletPanel.width, height); 29 | }); 30 | 31 | // Attach the wallet to the bitcoin button 32 | require('sdk/widget').Widget({ 33 | id: 'open-wallet-btn', 34 | label: 'Bitcoin Wallet', 35 | contentURL: data.url('bitcoin38.png'), 36 | panel: walletPanel 37 | }); 38 | 39 | // Inject the hover popup scripts into every page 40 | require('sdk/page-mod').PageMod({ 41 | include: '*', 42 | contentScriptFile: [ 43 | data.url('js/libs/promise.min.js'), 44 | data.url('js/libs/jquery.min.js'), 45 | data.url('js/libs/bitcoinjs-lib.min.js'), 46 | data.url('js/util.js'), 47 | data.url('js/preferences.js'), 48 | data.url('js/currency-manager.js'), 49 | data.url('js/hoverpopup.js')], 50 | onAttach: function (worker) { 51 | addListeners(worker); 52 | } 53 | }); 54 | 55 | var tabs = require('sdk/tabs'); 56 | 57 | // Add listeners to the worker to communicate with scripts 58 | function addListeners(worker) { 59 | // Get prefs from storage 60 | worker.port.on('get', function () { 61 | var storage = require('sdk/simple-storage').storage; 62 | worker.port.emit('get', storage); 63 | }); 64 | 65 | // Save prefs to storage 66 | worker.port.on('save', function (object) { 67 | var storage = require('sdk/simple-storage').storage; 68 | for (var i in object) { 69 | storage[i] = object[i]; 70 | } 71 | worker.port.emit('save', storage); 72 | }); 73 | 74 | // Open tabs 75 | worker.port.on('openTab', function (url) { 76 | tabs.open(url); 77 | }); 78 | 79 | // Get HTML for local files 80 | worker.port.on('html', function (url) { 81 | let content = data.load(url); 82 | // Replace relative paths of css files to absolute path 83 | content = content.replace(/css\//g, data.url('css/')); 84 | content = encodeURIComponent(content); 85 | content = 'data:text/html;charset=utf-8,' + content; 86 | worker.port.emit('html', content); 87 | }); 88 | 89 | // Cross-domain XHRs 90 | worker.port.on('getJSON', function (url) { 91 | require("sdk/request").Request({ 92 | url: url, 93 | onComplete: function (response) { 94 | worker.port.emit('getJSON', response.json); 95 | } 96 | }).get(); 97 | }); 98 | 99 | worker.port.on('post', function (message) { 100 | require("sdk/request").Request({ 101 | url: message.url, 102 | content: message.content, 103 | onComplete: function (response) { 104 | worker.port.emit('post', response); 105 | } 106 | }).post(); 107 | }); 108 | } 109 | 110 | var workers = {}; 111 | 112 | // Inject pay popup scripts into every page 113 | tabs.on('ready', function (tab) { 114 | workers[tab.id] = tab.attach({ 115 | contentScriptFile: [ 116 | data.url('js/libs/promise.min.js'), 117 | data.url('js/libs/jquery.min.js'), 118 | data.url('js/libs/cryptojs.min.js'), 119 | data.url('js/libs/bitcoinjs-lib.min.js'), 120 | data.url('js/util.js'), 121 | data.url('js/preferences.js'), 122 | data.url('js/currency-manager.js'), 123 | data.url('js/wallet.js'), 124 | data.url('js/paypopup.js')] 125 | }); 126 | addListeners(workers[tab.id]); 127 | }); 128 | 129 | 130 | var cm = require('sdk/context-menu'); 131 | 132 | // Create the context menu and inject the scripts to control it 133 | cm.Item({ 134 | label: 'Send BTC', 135 | image: data.url('bitcoin38.png'), 136 | context: cm.SelectorContext('*'), 137 | contentScriptFile: [ 138 | data.url('js/libs/bitcoinjs-lib.min.js'), 139 | data.url('js/context-menu.js') 140 | ], 141 | onMessage: function (message) { 142 | if (message.address === null) { 143 | this.label = 'Send BTC'; 144 | } else if (typeof message.address === 'string') { 145 | this.label = 'Pay ' + message.address; 146 | } 147 | if (message.clicked) { 148 | var worker = workers[tabs.activeTab.id]; 149 | worker.port.emit('pay', message); 150 | } 151 | } 152 | }); 153 | 154 | })(); -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "BitBrowser Bitcoin Wallet", 5 | "author": "Andrew Toth", 6 | "description": "Bitcoin wallet in the browser. Send and receive instantly on any web page.", 7 | "version": "1.1", 8 | 9 | "icons": { 10 | "16": "data/bitcoin16.png", 11 | "48": "data/bitcoin48.png", 12 | "128": "data/bitcoin128.png" 13 | }, 14 | 15 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", 16 | 17 | "permissions": ["contextMenus", "activeTab", "", "storage"], 18 | 19 | "content_scripts": [ 20 | { 21 | "matches": [""], 22 | "js": [ 23 | "data/js/libs/jquery.min.js", 24 | "data/js/libs/cryptojs.min.js", 25 | "data/js/libs/bitcoinjs-lib.min.js", 26 | "data/js/util.js", 27 | "data/js/preferences.js", 28 | "data/js/currency-manager.js", 29 | "data/js/wallet.js", 30 | "data/js/paypopup.js", 31 | "data/js/hoverpopup.js" 32 | ], 33 | "all_frames": true 34 | } 35 | ], 36 | 37 | "background": { 38 | "scripts": ["data/js/background.js"] 39 | }, 40 | 41 | "web_accessible_resources": [ 42 | "data/*" 43 | ], 44 | 45 | "browser_action": { 46 | "default_icon": { 47 | "19": "data/bitcoin19.png", 48 | "38": "data/bitcoin38.png" 49 | }, 50 | "default_title": "Bitcoin Wallet", 51 | "default_popup": "data/index.html" 52 | } 53 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcoinwallet", 3 | "title": "BitBrowser Bitcoin Wallet", 4 | "id": "jid1-sqPdj54n2PcVOQ", 5 | "description": "Bitcoin wallet in the browser. Send and receive instantly on any web page.", 6 | "author": "Andrew Toth", 7 | "license": "MIT", 8 | "version": "1.1", 9 | "permissions": { 10 | "cross-domain-content": ["https://blockchain.info/", "https://api.bitcoinaverage.com/"] 11 | } 12 | } 13 | --------------------------------------------------------------------------------