├── .gitignore ├── LICENSE ├── README.md ├── screenshot.png └── web ├── package-lock.json ├── package.json ├── src ├── 404.html ├── account │ ├── asset.scss │ ├── cancel_guide.png │ ├── index.js │ ├── index.scss │ ├── order.scss │ ├── order_item.html │ ├── orders.html │ └── snapshot.js ├── animate.scss ├── api │ ├── account.js │ ├── engine.js │ ├── group.js │ ├── groups.json │ ├── index.js │ ├── market.js │ ├── mixin.js │ └── ocean.js ├── app.js ├── auth │ └── index.js ├── constant.scss ├── database │ ├── asset.js │ ├── assets.json │ ├── index.js │ ├── market.js │ ├── order.js │ ├── trade.js │ └── transfer.js ├── error.html ├── fonts │ ├── index.scss │ ├── maven-pro │ │ ├── maven-pro-v9-latin-500.woff2 │ │ ├── maven-pro-v9-latin-700.woff2 │ │ └── maven-pro-v9-latin-regular.woff2 │ └── roboto │ │ ├── roboto-mono-v4-latin-100.woff2 │ │ ├── roboto-mono-v4-latin-300.woff2 │ │ ├── roboto-mono-v4-latin-500.woff2 │ │ ├── roboto-mono-v4-latin-regular.woff2 │ │ ├── roboto-v16-latin-100.woff2 │ │ ├── roboto-v16-latin-300.woff2 │ │ ├── roboto-v16-latin-500.woff2 │ │ └── roboto-v16-latin-regular.woff2 ├── helpers │ ├── dimZero.js │ ├── msgpack.js │ └── t.js ├── jquery-color-plus-names.js ├── launcher.png ├── layout.html ├── layout.scss ├── loading.html ├── locale │ ├── en-US.json │ ├── index.js │ └── zh-Hans.json ├── market │ ├── chart.js │ ├── index.html │ ├── index.js │ ├── index.scss │ ├── logo.png │ ├── market.js │ ├── market_item.html │ ├── masthead.jpg │ ├── order_item.html │ ├── symbol.png │ ├── trade.html │ ├── trade.scss │ └── trade_item.html ├── normalize.css └── utils │ ├── form.js │ ├── time.js │ └── url.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | web/nginx.conf 4 | web/deploy.sh 5 | *.go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mixcoin 2 | An open source decentralized exchange to trade all digital assets with your wallet. 3 | 4 | 5 | ![screenshot](https://github.com/over140/exchange/blob/master/screenshot.png) 6 | 7 | 8 | Donate me Bitcoin: https://donate.cafe/over140 -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/screenshot.png -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixcoin", 3 | "version": "1.0.0", 4 | "description": "MixCoin cache layer Web interface.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "watch": "webpack -w --mode=development", 9 | "dist": "NODE_ENV=production webpack -p --mode=production" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "base64url": "^3.0.0", 15 | "bignumber.js": "^7.2.1", 16 | "bugsnag-js": "^4.7.3", 17 | "handlebars": "^4.7.6", 18 | "highcharts": "^8.1.2", 19 | "intl-tel-input": "^13.0.2", 20 | "jquery": "^3.5.1", 21 | "jsrsasign": "^8.0.20", 22 | "lovefield": "^2.1.12", 23 | "int64-buffer": "^0.99.1007", 24 | "jsonwebtoken": "^8.5.1", 25 | "moment": "^2.22.2", 26 | "moment-timezone": "^0.5.21", 27 | "msgpack5": "^4.2.0", 28 | "navigo": "^7.1.2", 29 | "node-forge": "^0.9.1", 30 | "node-polyglot": "^2.3.0", 31 | "noty": "^3.2.0-beta", 32 | "pako": "^1.0.6", 33 | "qrious": "^4.0.2", 34 | "reconnecting-websocket": "^4.0.0-rc5", 35 | "simple-line-icons": "^2.4.1", 36 | "uuid": "^8.3.0", 37 | "uuid-parse": "^1.0.0" 38 | }, 39 | "devDependencies": { 40 | "compression-webpack-plugin": "^4.0.0", 41 | "css-loader": "^0.28.11", 42 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 43 | "favicons-webpack-plugin": "^4.2.0", 44 | "file-loader": "^1.1.11", 45 | "handlebars-loader": "^1.7.0", 46 | "html-webpack-plugin": "^4.3.0", 47 | "node-sass": "^4.14.1", 48 | "offline-plugin": "^5.0.5", 49 | "sass-loader": "^9.0.2", 50 | "script-ext-html-webpack-plugin": "^2.1.4", 51 | "style-loader": "^1.2.1", 52 | "webpack": "^4.43.0", 53 | "webpack-cli": "^3.3.12" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /web/src/404.html: -------------------------------------------------------------------------------- 1 |
2 |

404

3 |
4 | -------------------------------------------------------------------------------- /web/src/account/asset.scss: -------------------------------------------------------------------------------- 1 | @import '../constant.scss'; 2 | 3 | .account.layout { 4 | 5 | .management.container { 6 | width: 100%; 7 | max-width: 640px; 8 | margin: 0 auto; 9 | box-sizing: border-box; 10 | padding: 0 16px; 11 | } 12 | 13 | .account.balance { 14 | img { 15 | width: 64px; 16 | height: 64px; 17 | } 18 | 19 | h2 { 20 | margin: 16px 0 32px; 21 | } 22 | } 23 | 24 | .account.deposit.container, 25 | .address.deposit.container { 26 | display: none; 27 | 28 | input.account, 29 | input.address { 30 | margin: 0 0 32px; 31 | font-size: 14px; 32 | font-family: $font-main-mono; 33 | font-weight: 300; 34 | background: rgba(0,0,0,0.07); 35 | border: 1px solid rgba(0,0,0,0.07); 36 | border-top: 0 none; 37 | border-radius: 0 0 2px 2px; 38 | padding: 8px 16px; 39 | box-sizing: border-box; 40 | } 41 | 42 | input.account { 43 | margin-bottom: 16px; 44 | } 45 | 46 | .label { 47 | margin: 0; 48 | color: $color-main-background; 49 | border: 1px solid rgba(0,0,0,0.07); 50 | border-bottom: 0 none; 51 | border-radius: 2px 2px 0 0; 52 | background: rgba(0,0,0,0.07); 53 | 54 | label { 55 | margin: 0; 56 | display: inline-block; 57 | background: $color-side-bid; 58 | padding: 4px 8px; 59 | font-size: 12px; 60 | } 61 | } 62 | 63 | .label.account.name { 64 | margin-top: 32px; 65 | } 66 | 67 | .code.container { 68 | box-sizing: border-box; 69 | max-width: 360px; 70 | margin: 0 auto; 71 | 72 | &.account { 73 | max-width: 240px; 74 | } 75 | 76 | canvas { 77 | width: 100%; 78 | } 79 | } 80 | } 81 | 82 | .deposit.notice { 83 | margin: 32px 0; 84 | } 85 | 86 | .accounts.list { 87 | width: 100%; 88 | border-collapse: collapse; 89 | font-size: 16px; 90 | max-width: 640px; 91 | margin: 0 auto; 92 | box-sizing: border-box; 93 | padding: 0 16px; 94 | 95 | tbody { 96 | display: table-row-group; 97 | vertical-align: middle; 98 | } 99 | 100 | th, td { 101 | text-align: right; 102 | padding: 16px 0; 103 | 104 | &:first-child, 105 | &:nth-child(2) { 106 | text-align: left; 107 | } 108 | } 109 | 110 | th { 111 | cursor: pointer; 112 | text-transform: uppercase; 113 | font-weight: 500; 114 | } 115 | 116 | tr { 117 | border-bottom: 1px solid rgba(0,0,0,0.05); 118 | 119 | &:last-child { 120 | border-bottom: none; 121 | } 122 | } 123 | 124 | td { 125 | line-height: 20px; 126 | } 127 | 128 | img { 129 | width: 32px; 130 | height: 32px; 131 | padding-bottom: 4px; 132 | display: block; 133 | } 134 | 135 | .action { 136 | flex: 1; 137 | cursor: pointer; 138 | border: 1px solid rgba(0,0,0,0.1); 139 | color: $color-main-foreground-light; 140 | border-radius: 2px; 141 | display: inline-block; 142 | text-align: center; 143 | margin: 4px; 144 | padding: 4px 8px; 145 | 146 | &:last-child { 147 | margin-right: 0; 148 | } 149 | } 150 | } 151 | 152 | .withdrawal.action.container { 153 | .steps, 154 | .steps li { 155 | margin: 0; 156 | padding: 0; 157 | list-style: circle; 158 | text-align: left; 159 | line-height: 1.3em; 160 | } 161 | 162 | .steps li { 163 | margin: 0 0 8px 16px; 164 | 165 | a { 166 | color: $color-main-highlight; 167 | text-decoration: underline; 168 | } 169 | } 170 | 171 | .mixin.connect.button a { 172 | display: inline-block; 173 | text-decoration: none; 174 | background: $color-main-highlight; 175 | box-shadow: 0 0 4px rgba(0,0,0,0.3); 176 | border-radius: 4px; 177 | color: $color-main-background; 178 | font-size: 20px; 179 | padding: 12px 32px; 180 | margin: 32px 0 16px; 181 | } 182 | 183 | .mixin.connected { 184 | text-align: left; 185 | line-height: 1.5em; 186 | 187 | div { 188 | margin-bottom: 16px; 189 | } 190 | 191 | input { 192 | margin: 16px 0; 193 | } 194 | 195 | input[type="text"] { 196 | font-size: 20px; 197 | letter-spacing: 2px; 198 | } 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /web/src/account/cancel_guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/account/cancel_guide.png -------------------------------------------------------------------------------- /web/src/account/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import $ from 'jquery'; 3 | import 'intl-tel-input/build/css/intlTelInput.css'; 4 | import 'intl-tel-input'; 5 | import { v4 as uuid } from 'uuid'; 6 | import forge from 'node-forge'; 7 | import moment from 'moment'; 8 | import jwt from 'jsonwebtoken'; 9 | import LittleEndian from "int64-buffer"; 10 | import crypto from 'crypto'; 11 | 12 | import Mixin from '../api/mixin.js'; 13 | import TimeUtils from '../utils/time.js'; 14 | import Msgpack from '../helpers/msgpack.js'; 15 | import Snapshot from './snapshot.js'; 16 | 17 | 18 | function Account(router, api, db, bugsnag) { 19 | this.router = router; 20 | this.api = api; 21 | this.db = db; 22 | this.bugsnag = bugsnag; 23 | this.templateOrders = require('./orders.html'); 24 | this.itemOrder = require('./order_item.html'); 25 | this.mixin = new Mixin(this); 26 | this.msgpack = new Msgpack(); 27 | this.snapshot = new Snapshot(api, db, bugsnag); 28 | this.assets = {}; 29 | } 30 | 31 | Account.prototype = { 32 | 33 | hideLoader: function() { 34 | $('.cancel.order.form .submit-loader').hide(); 35 | $('.cancel.order.form :submit').show(); 36 | $('.cancel.order.form :submit').prop('disabled', false); 37 | }, 38 | 39 | encode: function (buffer) { 40 | return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_'); 41 | }, 42 | 43 | fetchAsset: function (assetId) { 44 | const self = this; 45 | self.api.mixin.asset(function (resp) { 46 | if (resp.error) { 47 | return; 48 | } 49 | self.db.asset.cacheAssets[resp.data.asset_id] = resp.data; 50 | self.db.asset.saveAsset(resp.data); 51 | }, assetId); 52 | }, 53 | 54 | fetchAssets: function (callback) { 55 | const self = this; 56 | self.db.prepare(function () { 57 | self.db.asset.fetchAssets(function (assets) { 58 | self.db.asset.cache(assets); 59 | callback(); 60 | }); 61 | }); 62 | }, 63 | 64 | fetchOrders: function (callback) { 65 | const self = this; 66 | self.fetchAssets(function () { 67 | self.db.order.fetchOrders(function (orders) { 68 | callback(orders); 69 | self.snapshot.syncSnapshots(); 70 | }); 71 | }); 72 | }, 73 | 74 | orders: function () { 75 | const self = this; 76 | self.fetchOrders(function (orders) { 77 | for (var i = 0; i < orders.length; i++) { 78 | var order = orders[i]; 79 | order.trace = uuid().toLowerCase(); 80 | order.time = TimeUtils.short(order.created_at); 81 | order.sideLocale = order.side === 'B' ? window.i18n.t('market.form.buy') : window.i18n.t('market.form.sell'); 82 | order.sideColor = order.side === 'B' ? 'Buy' : 'Sell'; 83 | order.quote = self.db.asset.getById(order.quote_asset_id); 84 | order.base = self.db.asset.getById(order.base_asset_id); 85 | 86 | if (order.order_type === 'M' && order.side === 'B') { 87 | order.amount_symbol = order.quote ? order.quote.symbol : '???'; 88 | } else { 89 | order.amount_symbol = order.base ? order.base.symbol : '???'; 90 | } 91 | if (order.order_type === 'L' && order.price) { 92 | order.price_symbol = order.quote ? order.quote.symbol : '???'; 93 | } 94 | 95 | if (!order.base) { 96 | self.fetchAsset(order.base_asset_id); 97 | } 98 | } 99 | 100 | self.orders = orders; 101 | 102 | $('body').attr('class', 'account layout'); 103 | $('#layout-container').html(self.templateOrders({ 104 | guideURL: require('./cancel_guide.png') 105 | })); 106 | 107 | self.orderFilterType = 'L'; 108 | self.orderFilterState = 'PENDING'; 109 | self.filterOrders(); 110 | 111 | $('#orders-type').on('change', function() { 112 | self.orderFilterType = $(this).val(); 113 | self.filterOrders(); 114 | }); 115 | $('#orders-status').on('change', function() { 116 | self.orderFilterState = $(this).val(); 117 | self.filterOrders(); 118 | }); 119 | 120 | $('.header').on('click', '.nav.back', function () { 121 | self.router.replace('/'); 122 | }); 123 | 124 | if (self.mixin.environment() == undefined) { 125 | $('.nav.power.account.sign.out.button').html(''); 126 | $('.header').on('click', '.account.sign.out.button', function () { 127 | self.api.account.clear(); 128 | window.location.href = '/'; 129 | }); 130 | } else { 131 | $('.nav.power.account.sign.out.button').html(''); 132 | $('.header').on('click', '.account.sign.out.button', function () { 133 | window.localStorage.removeItem('start_snapshots'); 134 | window.localStorage.removeItem('end_snapshots'); 135 | window.location.reload(); 136 | }); 137 | } 138 | self.router.updatePageLinks(); 139 | }); 140 | }, 141 | 142 | filterOrders: function () { 143 | const type = this.orderFilterType; 144 | const state = this.orderFilterState; 145 | const orders = this.orders.filter(function(order) { 146 | return order.order_type === type && order.state === state; 147 | }); 148 | 149 | $('#orders-content').html(this.itemOrder({ 150 | canCancel: type === 'L' && state === 'PENDING', 151 | orders: orders 152 | })); 153 | 154 | this.handleOrderCancel(); 155 | }, 156 | 157 | getCancelOrderAsset: function (callback) { 158 | const self = this; 159 | const oooAssetId = "de5a6414-c181-3ecc-b401-ce375d08c399"; 160 | const cnbAssetId = "965e5c6e-434c-3fa9-b780-c50f43cd955c"; 161 | const nxcAssetId = "66152c0b-3355-38ef-9ec5-cae97e29472a"; 162 | const candyAssetId = "01c46685-f6b0-3c16-95c1-b3d9515e2c9f"; 163 | 164 | const cancelAssets = [oooAssetId, cnbAssetId, nxcAssetId, candyAssetId]; 165 | for (var i = 0; i < cancelAssets.length; i++) { 166 | const asset = self.db.asset.getById(cancelAssets[i]); 167 | if (asset && parseFloat(asset.balance) > 0.00000001) { 168 | callback(asset); 169 | return; 170 | } 171 | } 172 | 173 | self.sendUserCoin(function (resp) { 174 | if (resp.error) { 175 | return; 176 | } 177 | 178 | callback(self.db.asset.getById(cnbAssetId)); 179 | }) 180 | }, 181 | 182 | encryptedPin: function(pin, pinToken, sessionId, privateKey, iterator) { 183 | const blockSize = 16; 184 | let Uint64LE = LittleEndian.Int64BE; 185 | 186 | pinToken = new Buffer(pinToken, 'base64'); 187 | privateKey = forge.pki.privateKeyFromPem(privateKey); 188 | let pinKey = privateKey.decrypt(pinToken, 'RSA-OAEP', { 189 | md: forge.md.sha256.create(), 190 | label: sessionId 191 | }); 192 | let time = new Uint64LE(moment.utc().unix()); 193 | time = [...time.toBuffer()].reverse(); 194 | if (iterator == undefined || iterator === "") { 195 | iterator = Date.now() * 1000000; 196 | } 197 | iterator = new Uint64LE(iterator); 198 | iterator = [...iterator.toBuffer()].reverse(); 199 | pin = Buffer.from(pin, 'utf8'); 200 | let buf = Buffer.concat([pin, Buffer.from(time), Buffer.from(iterator)]); 201 | let padding = blockSize - buf.length % blockSize; 202 | let paddingArray = []; 203 | for (let i = 0; i < padding; i++) { 204 | paddingArray.push(padding); 205 | } 206 | buf = Buffer.concat([buf, new Buffer(paddingArray)]); 207 | 208 | let iv16 = crypto.randomBytes(16); 209 | let cipher = crypto.createCipheriv('aes-256-cbc', this.hexToBytes(forge.util.binary.hex.encode(pinKey)), iv16); 210 | cipher.setAutoPadding(false); 211 | let encrypted_pin_buff = cipher.update(buf, 'utf-8'); 212 | encrypted_pin_buff = Buffer.concat([iv16, encrypted_pin_buff]); 213 | return Buffer.from(encrypted_pin_buff).toString('base64'); 214 | }, 215 | 216 | hexToBytes: function (hex) { 217 | var bytes = []; 218 | for (let c = 0; c < hex.length; c += 2) { 219 | bytes.push(parseInt(hex.substr(c, 2), 16)); 220 | } 221 | return bytes; 222 | }, 223 | 224 | signAuthenticationToken: function (uid, sid, privateKey, method, uri, params) { 225 | if (typeof (params) === "object") { 226 | params = JSON.stringify(params); 227 | } else if (typeof (params) !== "string") { 228 | params = "" 229 | } 230 | 231 | let expire = moment.utc().add(30, 'minutes').unix(); 232 | let md = forge.md.sha256.create(); 233 | md.update(forge.util.encodeUtf8(method + uri + params)); 234 | let payload = { 235 | uid: uid, 236 | sid: sid, 237 | iat: moment.utc().unix(), 238 | exp: expire, 239 | jti: uuid(), 240 | sig: md.digest().toHex(), 241 | scp: 'FULL' 242 | }; 243 | return jwt.sign(payload, privateKey, { algorithm: 'RS512' }); 244 | }, 245 | 246 | prepareUserId: function(callback) { 247 | const currentUserId = this.api.account.userId(); 248 | if (currentUserId) { 249 | callback(currentUserId); 250 | } else { 251 | this.api.account.info(function (resp) { 252 | if (resp.error) { 253 | return; 254 | } 255 | window.localStorage.setItem('user_id', resp.data.user_id); 256 | callback(resp.data.user_id); 257 | }); 258 | } 259 | }, 260 | 261 | sendUserCoin: function(callback) { 262 | const self = this; 263 | self.prepareUserId(function (currentUserId) { 264 | const params = { 265 | asset_id : "965e5c6e-434c-3fa9-b780-c50f43cd955c", 266 | opponent_id : currentUserId, 267 | amount : "0.1", 268 | pin : self.encryptedPin(CAPP_PIN, CAPP_PIN_TOKEN, CAPP_SESSION_ID, CAPP_PRIVATE_KEY), 269 | memo : 'Used to cancel orders' 270 | } 271 | 272 | const method = 'POST'; 273 | const path = '/transfers'; 274 | const body = JSON.stringify(params); 275 | 276 | var url = 'https://mixin-api.zeromesh.net' + path; 277 | var token = self.signAuthenticationToken(CAPP_USER_ID, CAPP_SESSION_ID, CAPP_PRIVATE_KEY, method, path, params); 278 | return self.api.send(token, method, url, body, callback); 279 | }); 280 | }, 281 | 282 | handleOrderCancel: function () { 283 | const self = this; 284 | $('.orders.list .cancel.action a').click(function () { 285 | var item = $(this).parents('.order.item'); 286 | const orderId = $(item).attr('data-id'); 287 | const traceId = $(item).attr('trace-id'); 288 | 289 | self.getCancelOrderAsset(function (asset) { 290 | if (!asset) { 291 | self.api.notify('error', window.i18n.t('orders.insufficient.balance')); 292 | return; 293 | } 294 | 295 | const msgpack = require('msgpack5')(); 296 | const uuidParse = require('uuid-parse'); 297 | const memo = self.encode(msgpack.encode({'O': uuidParse.parse(orderId)})); 298 | 299 | var redirect_to; 300 | var url = 'pay?recipient=' + ENGINE_USER_ID + '&asset=' + asset.asset_id + '&amount=0.00000001&memo=' + memo + '&trace=' + traceId; 301 | 302 | if (self.mixin.environment() == undefined) { 303 | redirect_to = window.open(""); 304 | } 305 | 306 | self.created_at = new Date(); 307 | 308 | clearInterval(self.paymentInterval); 309 | var verifyTrade = function() { 310 | self.api.mixin.verifyTrade(function (resp) { 311 | if ((new Date() - self.created_at) > 60 * 1000) { 312 | if (redirect_to != undefined) { 313 | redirect_to.close(); 314 | } 315 | window.location.reload(); 316 | return 317 | } 318 | if (resp.error) { 319 | return true; 320 | } 321 | 322 | for (var i = 0; i < self.orders.length; i++) { 323 | var order = self.orders[i]; 324 | if (order.order_id === orderId) { 325 | order.state = 'DONE'; 326 | break; 327 | } 328 | } 329 | self.db.order.canceledOrder(orderId); 330 | self.snapshot.syncSnapshots(); 331 | 332 | $(item).fadeOut().remove(); 333 | 334 | const data = resp.data; 335 | if (redirect_to != undefined) { 336 | redirect_to.close(); 337 | } 338 | 339 | clearInterval(self.paymentInterval); 340 | }, traceId); 341 | } 342 | self.paymentInterval = setInterval(function() { verifyTrade(); }, 3000); 343 | 344 | if (self.mixin.environment() == undefined) { 345 | redirect_to.location = 'https://mixin.one/' + url; 346 | } else { 347 | window.location.replace('mixin://' + url); 348 | } 349 | }); 350 | }); 351 | } 352 | }; 353 | 354 | export default Account; 355 | -------------------------------------------------------------------------------- /web/src/account/index.scss: -------------------------------------------------------------------------------- 1 | @import '../constant.scss'; 2 | @import './asset.scss'; 3 | @import './order.scss'; 4 | 5 | .account.layout { 6 | 7 | .header { 8 | box-sizing: border-box; 9 | width: 100%; 10 | height: 96px; 11 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 12 | position: relative; 13 | text-align: center; 14 | 15 | .nav { 16 | display: inline-block; 17 | font-size: 32px; 18 | font-weight: 300; 19 | line-height: 96px; 20 | color: $color-main-foreground-dark; 21 | font-family: $font-main-title; 22 | position: absolute; 23 | top: 50%; 24 | transform: translateY(-50%); 25 | 26 | &.back { 27 | left: 24px; 28 | color: $color-main-foreground-light; 29 | } 30 | 31 | &.power, 32 | &.support { 33 | right: 24px; 34 | color: $color-main-foreground-light; 35 | } 36 | 37 | &:hover { 38 | color: $color-main-highlight; 39 | } 40 | } 41 | 42 | .title { 43 | font-family: $font-main-title; 44 | display: inline-block; 45 | font-size: 24px; 46 | font-weight: 300; 47 | line-height: 96px; 48 | margin: 0; 49 | padding: 0; 50 | } 51 | } 52 | 53 | .content { 54 | width: 100%; 55 | box-sizing: border-box; 56 | margin: 0 auto 12px; 57 | padding: 12px 0px; 58 | text-align: center; 59 | } 60 | 61 | .form.container { 62 | max-width: 400px; 63 | margin: 0 auto; 64 | } 65 | 66 | .tabs { 67 | background: $color-main-background; 68 | display: flex; 69 | font-size: 14px; 70 | line-height: 32px; 71 | width: 100%; 72 | box-sizing: border-box; 73 | margin-bottom: 48px; 74 | } 75 | 76 | .tab { 77 | flex: 1; 78 | width: 50%; 79 | box-sizing: border-box; 80 | text-align: center; 81 | text-transform: uppercase; 82 | cursor: pointer; 83 | border-bottom: 2px solid rgba(0,0,0,0.1); 84 | color: $color-main-foreground-light; 85 | 86 | &.active { 87 | border-bottom: 2px solid $color-main-highlight; 88 | } 89 | } 90 | 91 | .field { 92 | margin: 32px 0; 93 | } 94 | 95 | h2 { 96 | font-size: 20px; 97 | font-weight: 300; 98 | margin: 32px 0; 99 | } 100 | 101 | label { 102 | font-size: 16px; 103 | font-weight: 300; 104 | margin: 16px 0; 105 | text-align: left; 106 | display: block; 107 | } 108 | 109 | .intl-tel-input { 110 | width: 100%; 111 | } 112 | 113 | input[type="password"], 114 | input[type="text"], 115 | input[type="number"], 116 | input[type="tel"] { 117 | border: 0 none; 118 | outline: 0 none; 119 | border-radius: 0; 120 | border-bottom: 1px solid $color-main-foreground-light; 121 | padding-top: 8px; 122 | padding-bottom: 8px; 123 | letter-spacing: 4px; 124 | width: 100%; 125 | 126 | &:focus { 127 | border-color: $color-main-highlight; 128 | } 129 | } 130 | 131 | input[type="text"], 132 | input[type="number"] { 133 | text-align: center; 134 | letter-spacing: 16px; 135 | font-size: 32px; 136 | } 137 | 138 | input[type="text"] { 139 | letter-spacing: normal; 140 | font-size: 18px; 141 | } 142 | 143 | input[type="submit"], 144 | .submit-loader { 145 | display: inline-block; 146 | margin-top: 16px; 147 | outline: 0 none; 148 | border: 0 none; 149 | border-radius: 2px; 150 | padding: 12px 32px; 151 | background-color: $color-main-highlight; 152 | color: $color-main-background; 153 | font-weight: 300; 154 | cursor: pointer; 155 | box-sizing: border-box; 156 | width: 100%; 157 | } 158 | 159 | .submit-loader { 160 | position: relative; 161 | display: none; 162 | cursor: wait; 163 | background-color: $color-main-background; 164 | margin-top: 64px; 165 | 166 | .spinner { 167 | position: absolute; 168 | top: 50%; 169 | left: 50%; 170 | transform: translate(-50%, -50%); 171 | width: 70px; 172 | padding-top: 4px; 173 | text-align: center; 174 | } 175 | 176 | .spinner > div { 177 | width: 12px; 178 | height: 12px; 179 | background-color: $color-main-highlight; 180 | border-radius: 100%; 181 | display: inline-block; 182 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 183 | } 184 | 185 | .spinner .bounce1 { 186 | animation-delay: -0.32s; 187 | } 188 | 189 | .spinner .bounce2 { 190 | animation-delay: -0.16s; 191 | } 192 | 193 | @keyframes sk-bouncedelay { 194 | 0%, 80%, 100% { 195 | transform: scale(0); 196 | } 40% { 197 | transform: scale(1.0); 198 | } 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /web/src/account/order.scss: -------------------------------------------------------------------------------- 1 | @import '../constant.scss'; 2 | 3 | .account.layout { 4 | 5 | .orders.container { 6 | width: 100%; 7 | max-width: 640px; 8 | margin: 0 auto; 9 | box-sizing: border-box; 10 | padding: 0 8px; 11 | } 12 | 13 | .navi { 14 | text-align: right; 15 | .item { 16 | border: 1px solid rgba(0,0,0,0.1); 17 | border-radius: 2px; 18 | color: #000; 19 | display: inline-block; 20 | padding: 0.3rem 0.6rem; 21 | margin-left: 0.8rem; 22 | &.active, &:hover { 23 | background-color: rgba(0,0,0,0.05); 24 | } 25 | } 26 | 27 | select { 28 | width: auto; 29 | border: solid 1px #B9B9B9; 30 | appearance:none; 31 | -moz-appearance:none; 32 | -webkit-appearance:none; 33 | // background: url("http://ourjs.github.io/static/2015/arrow.png") no-repeat scroll right center transparent; 34 | padding: 8px 24px 8px 24px; 35 | margin: 12px 6px; 36 | } 37 | 38 | option{ 39 | text-align:center; 40 | } 41 | } 42 | 43 | .orders.list { 44 | width: 100%; 45 | border-collapse: collapse; 46 | font-size: 14px; 47 | max-width: 640px; 48 | margin: 0 auto; 49 | box-sizing: border-box; 50 | padding: 0 16px; 51 | 52 | thead { 53 | display: table-header-group; 54 | vertical-align: middle; 55 | border-top: 1px solid rgba(0,0,0,0.05); 56 | } 57 | 58 | tbody { 59 | display: table-row-group; 60 | vertical-align: middle; 61 | font-family: $font-main-mono; 62 | } 63 | 64 | th, td { 65 | text-align: right; 66 | padding: 16px 0; 67 | 68 | // &:first-child { 69 | // text-align: left; 70 | // } 71 | } 72 | 73 | th { 74 | cursor: pointer; 75 | text-transform: uppercase; 76 | font-weight: 500; 77 | } 78 | 79 | tr { 80 | border-bottom: 1px solid rgba(0,0,0,0.05); 81 | 82 | &:last-child { 83 | border-bottom: none; 84 | } 85 | } 86 | 87 | thead { 88 | border-bottom: 1px solid rgba(0,0,0,0.05); 89 | } 90 | 91 | td { 92 | line-height: 20px; 93 | word-wrap: break-word; 94 | 95 | .sub { 96 | font-size: 90%; 97 | opacity: 0.3; 98 | } 99 | } 100 | 101 | .symbol { 102 | margin-left: 2px 103 | } 104 | 105 | .Sell.side { 106 | color: $color-side-ask; 107 | } 108 | 109 | .Buy.side { 110 | color: $color-side-bid; 111 | } 112 | 113 | .filled { 114 | opacity: 0.3; 115 | margin-left: 4px; 116 | font-size: 80%; 117 | font-weight: 300; 118 | } 119 | 120 | .cancel.action a { 121 | color: $color-main-foreground-light; 122 | font-size: 10px; 123 | border: solid 1px #B9B9B9; 124 | border-radius: 4px; 125 | padding: 6px 12px; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /web/src/account/order_item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{t 'orders.table.price'}} 5 | {{t 'orders.table.volume'}} 6 | {{t 'orders.table.time'}} 7 | {{#if canCancel}} 8 | {{t 'orders.table.operation'}} 9 | {{/if}} 10 | 11 | 12 | 13 | {{#each orders}} 14 | 15 | 16 | {{this.price}}{{this.price_symbol}} 17 |
18 | {{this.sideLocale}} 19 | 20 | {{this.amount}}{{this.amount_symbol}} 21 |
22 | {{this.filled_amount}} 23 | 24 | {{this.time}} 25 | {{#if ../canCancel}} 26 | {{t 'orders.cancel'}} 27 | {{/if}} 28 | 29 | {{/each}} 30 | -------------------------------------------------------------------------------- /web/src/account/orders.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

{{t 'orders.title'}}

5 | 6 |
7 |
8 |
9 | 19 | 20 | 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /web/src/account/snapshot.js: -------------------------------------------------------------------------------- 1 | 2 | import Msgpack from '../helpers/msgpack.js'; 3 | import TimeUtils from '../utils/time.js'; 4 | import {BigNumber} from 'bignumber.js'; 5 | 6 | function Snapshot(api, db, bugsnag) { 7 | this.api = api 8 | this.database = db; 9 | this.bugsnag = bugsnag; 10 | this.firstTradeTime = '2018-08-11T23:59:59.779447612Z' 11 | this.msgpack = new Msgpack(); 12 | } 13 | 14 | Snapshot.prototype = { 15 | 16 | decode: function (base64) { 17 | return new Buffer(base64.replace(/\-/g, '+').replace(/\_/g, '/'), 'base64'); 18 | }, 19 | 20 | isOrderMemo: function (base64) { 21 | return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(base64.replace(/\-/g, '+').replace(/\_/g, '/')); 22 | }, 23 | 24 | decodeMemo: function(snapshot) { 25 | const buf = this.decode(snapshot.memo); 26 | try { 27 | return this.msgpack.decodeMap(buf, 0, buf.readUInt8(0) & 0x0f, 1); 28 | } catch (error) { 29 | this.bugsnag.notify(error, { metaData: snapshot }); 30 | } 31 | }, 32 | 33 | fetchNextPage: function(resp, pageEnd, limit) { 34 | if (resp.data.length === 0) { 35 | return; 36 | } 37 | 38 | const lastSnapshot = resp.data[resp.data.length - 1]; 39 | const firstSnapshot = resp.data[0]; 40 | const endTime = window.localStorage.getItem('end_snapshots'); 41 | const startTime = window.localStorage.getItem('start_snapshots'); 42 | 43 | if (startTime == null || firstSnapshot.created_at > startTime) { 44 | window.localStorage.setItem('start_snapshots', firstSnapshot.created_at); 45 | } 46 | 47 | if (pageEnd || lastSnapshot.created_at <= this.firstTradeTime) { 48 | window.localStorage.setItem('end_snapshots', this.firstTradeTime); 49 | } else if (endTime == null || lastSnapshot.created_at < endTime) { 50 | window.localStorage.setItem('end_snapshots', lastSnapshot.created_at); 51 | this.syncSnapshots(lastSnapshot.created_at, limit); 52 | } 53 | }, 54 | 55 | syncSnapshots: function (offset, limit) { 56 | const self = this; 57 | const endTime = window.localStorage.getItem('end_snapshots'); 58 | const maxLimit = 500; 59 | 60 | if (limit === undefined) { 61 | limit = 50; 62 | } 63 | 64 | if (offset == undefined) { 65 | if (endTime == null) { 66 | offset = TimeUtils.rfc3339(new Date()); 67 | limit = maxLimit; 68 | } else { 69 | if (endTime > this.firstTradeTime) { 70 | offset = endTime; 71 | limit = maxLimit; 72 | } else { 73 | offset = TimeUtils.rfc3339(new Date()); 74 | } 75 | } 76 | } 77 | 78 | self.api.mixin.snapshots(function (resp) { 79 | if (resp.error) { 80 | self.api.notifyError('error', resp.error); 81 | return; 82 | } 83 | if (!resp.data) { 84 | return; 85 | } 86 | 87 | if (resp.data.length == 0) { 88 | if (endTime == null) { 89 | window.localStorage.setItem('end_snapshots', this.firstTradeTime); 90 | } 91 | return; 92 | } 93 | 94 | self.processSnapshots(resp, limit); 95 | }, offset, limit); 96 | }, 97 | 98 | processSnapshots: function (resp, limit) { 99 | const self = this; 100 | const startTime = window.localStorage.getItem('start_snapshots'); 101 | const endTime = window.localStorage.getItem('end_snapshots'); 102 | const isPageEnded = resp.data.length < limit; 103 | var snapshots = resp.data; 104 | 105 | if (startTime != null && endTime != null) { 106 | snapshots = resp.data.filter(function(snapshot) { 107 | return snapshot.created_at > startTime || snapshot.created_at < endTime; 108 | }); 109 | } 110 | 111 | snapshots = snapshots.filter(function(snapshot) { 112 | return snapshot.memo !== '' && snapshot.memo !== undefined && self.isOrderMemo(snapshot.memo) 113 | }); 114 | 115 | var orderMaps = {}; 116 | var orders = []; 117 | var transfers = []; 118 | 119 | for (var i = 0; i < snapshots.length; i++) { 120 | const snapshot = snapshots[i]; 121 | var amount = new BigNumber(snapshot.amount); 122 | const orderAction = self.decodeMemo(snapshot); 123 | if (!orderAction) { 124 | continue; 125 | } 126 | 127 | if (amount.isNegative()) { 128 | /* 129 | type OrderAction struct { 130 | S string // side 131 | A uuid.UUID // asset 132 | P string // price 133 | T string // type 134 | O uuid.UUID // order 135 | } 136 | */ 137 | if (!orderAction.O && orderAction.S && orderAction.A && orderAction.T) { 138 | if (orderAction.T === 'L' && !orderAction.P) { 139 | self.bugsnag.notify(new Error('Error Limit Order'), { metaData: snapshot }); 140 | continue; 141 | } 142 | 143 | var order = {}; 144 | order.order_id = snapshot.trace_id; 145 | order.order_type = orderAction.T; 146 | order.quote_asset_id = orderAction.S === 'B' ? snapshot.asset_id : orderAction.A; 147 | order.base_asset_id = orderAction.S === 'B' ? orderAction.A : snapshot.asset_id; 148 | if (orderAction.T === 'L' && orderAction.S === 'B') { 149 | const priceDecimal = new BigNumber(orderAction.P); 150 | if (isNaN(priceDecimal) || priceDecimal.isZero()) { 151 | continue; 152 | } 153 | order.amount = amount.div(priceDecimal).abs(); 154 | } else { 155 | order.amount = amount.abs(); 156 | } 157 | order.filled_amount = new BigNumber(0); 158 | order.side = orderAction.S; 159 | order.price = orderAction.P ? orderAction.P.replace(/\.0+$/,"") : '0'; 160 | order.state = 'PENDING'; 161 | order.created_at = snapshot.created_at; 162 | orderMaps[snapshot.trace_id] = order; 163 | orders.push(order); 164 | } 165 | } else { 166 | if (orderAction.S) { 167 | var transfer = {}; 168 | transfer.transfer_id = snapshot.trace_id; 169 | transfer.source = orderAction.S; 170 | transfer.amount = snapshot.amount; 171 | transfer.asset_id = snapshot.asset_id; 172 | transfer.order_id = ''; 173 | transfer.ask_order_id = ''; 174 | transfer.bid_order_id = ''; 175 | transfer.created_at = snapshot.created_at; 176 | switch (orderAction.S) { 177 | case 'FILL': 178 | case 'REFUND': 179 | case 'CANCEL': 180 | const order_id = orderAction.O; 181 | if (order_id) { 182 | transfer.order_id = order_id; 183 | transfers.push(transfer); 184 | } 185 | break; 186 | case 'MATCH': 187 | transfer.ask_order_id = orderAction.A; 188 | if (orderAction.B) { 189 | transfer.bid_order_id = orderAction.B; 190 | } else { 191 | transfer.bid_order_id = ''; 192 | } 193 | transfers.push(transfer); 194 | break; 195 | } 196 | } 197 | } 198 | } 199 | 200 | for (var i = 0; i < transfers.length; i++) { 201 | const transfer = transfers[i]; 202 | if (transfer.source !== 'MATCH') { 203 | var order = orderMaps[transfer.order_id]; 204 | if (order) { 205 | order.state = 'DONE'; 206 | } 207 | } 208 | } 209 | 210 | if (orders.length === 0 && transfers.length === 0) { 211 | self.fetchNextPage(resp, isPageEnded, limit); 212 | return; 213 | } 214 | 215 | const db = self.database.db; 216 | if (db) { 217 | const tx = db.createTransaction(); 218 | const orderTable = db.getSchema().table('orders'); 219 | const transferTable = db.getSchema().table('transfers'); 220 | 221 | tx.begin([orderTable, transferTable]).then(function() { 222 | return tx.attach(self.database.order.saveOrders(db, orders)); 223 | }).then(function() { 224 | return tx.attach(self.database.transfer.saveTransfers(db, transfers)); 225 | }).then(function() { 226 | return tx.attach(db.select().from(orderTable).where(orderTable.state.eq('PENDING'))); 227 | }).then(function(orders) { 228 | self.pendingOrders = orders; 229 | var ids = orders.map(function(row) { 230 | return row['order_id']; 231 | }); 232 | const predicate = lf.op.or(transferTable.ask_order_id.in(ids), transferTable.bid_order_id.in(ids), transferTable.order_id.in(ids)); 233 | return tx.attach(db.select().from(transferTable).where(predicate)); 234 | }).then(function(transfers) { 235 | var pendingOrders = self.pendingOrders; 236 | var orders = {}; 237 | 238 | for (var i = 0; i < pendingOrders.length; i++) { 239 | var order = pendingOrders[i]; 240 | order.filled_amount = new BigNumber(0); 241 | order.amount = new BigNumber(order.amount); 242 | orders[order.order_id] = order; 243 | } 244 | 245 | for (var i = 0; i < transfers.length; i++) { 246 | const transfer = transfers[i]; 247 | switch (transfer.source) { 248 | case 'FILL': 249 | case 'REFUND': 250 | case 'CANCEL': 251 | var order = orders[transfer.order_id]; 252 | if (order) { 253 | order.state = 'DONE'; 254 | } 255 | break; 256 | case 'MATCH': 257 | var order = orders[transfer.ask_order_id]; 258 | if (!order || order.state === 'DONE' || order.quote_asset_id !== transfer.asset_id) { 259 | order = orders[transfer.bid_order_id]; 260 | if (!order || order.state === 'DONE' || order.base_asset_id !== transfer.asset_id) { 261 | break; 262 | } 263 | } 264 | 265 | order.filled_amount = order.filled_amount.plus(transfer.amount); 266 | if (order.side === 'B') { 267 | if (order.filled_amount.multipliedBy(1.0011).isGreaterThanOrEqualTo(order.amount)) { 268 | order.state = 'DONE'; 269 | } 270 | } else { 271 | if (order.filled_amount.multipliedBy(1.0011).isGreaterThanOrEqualTo(order.amount.multipliedBy(order.price))) { 272 | order.state = 'DONE'; 273 | } 274 | } 275 | break; 276 | } 277 | } 278 | return tx.attach(self.database.order.saveOrders(db, pendingOrders)); 279 | }).then(function() { 280 | return tx.commit(); 281 | }).then(function() { 282 | self.fetchNextPage(resp, isPageEnded, limit); 283 | }); 284 | } 285 | } 286 | 287 | }; 288 | 289 | export default Snapshot; 290 | -------------------------------------------------------------------------------- /web/src/api/account.js: -------------------------------------------------------------------------------- 1 | function Account(api) { 2 | this.api = api; 3 | } 4 | 5 | Account.prototype = { 6 | info: function (callback) { 7 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/me', undefined, function(resp) { 8 | return callback(resp); 9 | }); 10 | }, 11 | 12 | authenticate: function (callback, authorizationCode) { 13 | var params = { 14 | "code": authorizationCode, 15 | "client_secret": CLIENT_SECRET, 16 | "client_id": CLIENT_ID 17 | }; 18 | this.api.requestMixin('POST', 'https://mixin-api.zeromesh.net/oauth/token', params, function(resp) { 19 | if (resp.data) { 20 | window.localStorage.setItem('user_id', resp.data.user_id); 21 | window.localStorage.setItem('token', resp.data.access_token); 22 | window.localStorage.setItem('scope', resp.data.scope); 23 | } 24 | return callback(resp); 25 | }); 26 | }, 27 | 28 | userId: function () { 29 | return window.localStorage.getItem('user_id'); 30 | }, 31 | 32 | token: function () { 33 | return window.localStorage.getItem('token'); 34 | }, 35 | 36 | loggedIn: function() { 37 | return window.localStorage.getItem('token') !== ""; 38 | }, 39 | 40 | clear: function (callback) { 41 | if (window.indexedDB) { 42 | window.indexedDB.deleteDatabase('mixcoin'); 43 | } 44 | window.localStorage.clear(); 45 | if (typeof callback === 'function') { 46 | callback(); 47 | } 48 | } 49 | }; 50 | 51 | export default Account; 52 | -------------------------------------------------------------------------------- /web/src/api/engine.js: -------------------------------------------------------------------------------- 1 | import ReconnectingWebSocket from 'reconnecting-websocket'; 2 | import pako from 'pako'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | function Engine(endpoint) { 6 | const self = this; 7 | self.handlers = {}; 8 | self.ws = new ReconnectingWebSocket(endpoint, [], { 9 | maxReconnectionDelay: 5000, 10 | minReconnectionDelay: 1000, 11 | reconnectionDelayGrowFactor: 1.2, 12 | connectionTimeout: 4000, 13 | maxRetries: Infinity, 14 | debug: true 15 | }); 16 | 17 | self.ws.addEventListener("message", (event) => { 18 | var fileReader = new FileReader(); 19 | fileReader.onload = function() { 20 | var msg = pako.ungzip(new Uint8Array(this.result), { to: 'string' }); 21 | self.handle(JSON.parse(msg)); 22 | }; 23 | fileReader.readAsArrayBuffer(event.data); 24 | }); 25 | 26 | self.ws.addEventListener("open", (event) => { 27 | for (var i in self.handlers) { 28 | self.send(self.handlers[i].message); 29 | } 30 | }); 31 | 32 | self.ws.addEventListener('close', () => self.ws._shouldReconnect && self.ws._connect()); 33 | } 34 | 35 | Engine.prototype = { 36 | reset: function() { 37 | try { 38 | this.ws.close(); 39 | } catch (e) { 40 | if (e instanceof DOMException) { 41 | } else { 42 | console.error(e); 43 | } 44 | } 45 | }, 46 | 47 | send: function (msg) { 48 | try { 49 | this.ws.send(pako.gzip(JSON.stringify(msg))); 50 | } catch (e) { 51 | if (e instanceof DOMException) { 52 | } else { 53 | console.error(e); 54 | } 55 | } 56 | }, 57 | 58 | handle: function (msg) { 59 | var handler = this.handlers[msg.data.market]; 60 | if (handler) { 61 | handler.callback(msg); 62 | } 63 | }, 64 | 65 | subscribe: function (market, callback) { 66 | var handler = this.handlers[market]; 67 | if (handler) { 68 | this.unsubscribe(market); 69 | } 70 | var msg = { 71 | id: uuidv4().toLowerCase(), 72 | action: 'SUBSCRIBE_BOOK', 73 | params: { market: market } 74 | }; 75 | this.send(msg); 76 | this.handlers[market] = { 77 | callback: callback, 78 | message: msg 79 | }; 80 | }, 81 | 82 | unsubscribe: function (market) { 83 | var handler = this.handlers[market]; 84 | if (handler) { 85 | delete this.handlers[market]; 86 | this.send({ 87 | id: uuidv4().toLowerCase(), 88 | action: 'UNSUBSCRIBE_BOOK', 89 | params: { market: market } 90 | }); 91 | } 92 | } 93 | }; 94 | 95 | export default Engine; 96 | -------------------------------------------------------------------------------- /web/src/api/group.js: -------------------------------------------------------------------------------- 1 | function Group() { 2 | this.groups = require('./groups.json'); 3 | } 4 | 5 | Group.prototype = { 6 | 7 | getByAsset: function (lang, assetId) { 8 | const groups = this.groups; 9 | for (var i = 0; i < groups.length; i++) { 10 | const group = groups[i]; 11 | if (group.asset_id === assetId && lang.slice(0, group.lang.length) === group.lang) { 12 | return group; 13 | } 14 | } 15 | return undefined; 16 | } 17 | } 18 | 19 | export default Group; 20 | -------------------------------------------------------------------------------- /web/src/api/groups.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "conversation_id": "fc08a96b-0426-4eb5-91bc-7138fe982766", 4 | "name": "加入 XIN 交易群", 5 | "asset_id": "c94ac88f-4671-3976-b60a-09064f1811e8", 6 | "lang": "zh", 7 | "url": "https://mixin.one/codes/806b40a6-00d9-4132-a2d6-8423bb7d7cc5" 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /web/src/api/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Noty from 'noty'; 3 | import Account from './account.js'; 4 | import Engine from './engine.js'; 5 | import Mixin from './mixin.js'; 6 | import Ocean from './ocean.js'; 7 | import Market from './market.js'; 8 | 9 | function API(router, root, engine) { 10 | this.router = router; 11 | this.root = root; 12 | this.account = new Account(this); 13 | this.mixin = new Mixin(this); 14 | this.ocean = new Ocean(this); 15 | this.engine = new Engine(engine); 16 | this.market = new Market(this); 17 | this.Error404 = require('../404.html'); 18 | this.ErrorGeneral = require('../error.html'); 19 | } 20 | 21 | API.prototype = { 22 | 23 | requestMixin: function(method, path, params, callback) { 24 | const self = this; 25 | $.ajax({ 26 | type: method, 27 | url: path, 28 | contentType: "application/json", 29 | data: JSON.stringify(params), 30 | beforeSend: function(xhr) { 31 | xhr.setRequestHeader("Authorization", "Bearer " + self.account.token()); 32 | }, 33 | success: function(resp) { 34 | var consumed = false; 35 | if (typeof callback === 'function') { 36 | consumed = callback(resp); 37 | } 38 | if (!consumed && resp.error !== null && resp.error !== undefined) { 39 | self.error(resp); 40 | } 41 | }, 42 | error: function(event) { 43 | self.error(event.responseJSON, callback); 44 | } 45 | }); 46 | }, 47 | 48 | request: function(method, path, params, callback) { 49 | const self = this; 50 | var body = JSON.stringify(params); 51 | var url = self.root + path; 52 | if (path.indexOf('https://') === 0) { 53 | url = path; 54 | } 55 | if (url.indexOf('https://mixin-api.zeromesh.net') === 0) { 56 | var uri = path.slice('https://mixin-api.zeromesh.net'.length); 57 | self.account.mixinToken(uri, function (resp) { 58 | if (resp.error) { 59 | return callback(resp); 60 | } 61 | return self.send(resp.data.token, method, url, body, callback); 62 | }); 63 | } else { 64 | var token = self.account.token(method, path, body); 65 | return self.send(token, method, url, body, callback); 66 | } 67 | }, 68 | 69 | send: function (token, method, url, body, callback) { 70 | const self = this; 71 | $.ajax({ 72 | type: method, 73 | url: url, 74 | contentType: "application/json", 75 | data: body, 76 | beforeSend: function(xhr) { 77 | xhr.setRequestHeader("Authorization", "Bearer " + token); 78 | }, 79 | success: function(resp) { 80 | var consumed = false; 81 | if (typeof callback === 'function') { 82 | consumed = callback(resp); 83 | } 84 | if (!consumed && resp.error !== null && resp.error !== undefined) { 85 | self.error(resp); 86 | } 87 | }, 88 | error: function(event) { 89 | self.error(event.responseJSON, callback); 90 | } 91 | }); 92 | }, 93 | 94 | error: function(resp, callback) { 95 | if (resp == null || resp == undefined || resp.error === null || resp.error === undefined) { 96 | resp = {error: { code: 0, description: 'unknown error' }}; 97 | } 98 | 99 | var consumed = false; 100 | if (typeof callback === 'function') { 101 | consumed = callback(resp); 102 | } 103 | if (!consumed) { 104 | switch (resp.error.code) { 105 | case 401: 106 | case 403: 107 | this.account.clear(); 108 | var obj = new URL(window.location); 109 | var returnTo = encodeURIComponent(obj.href.substr(obj.origin.length)); 110 | window.location.replace('https://mixin-www.zeromesh.net/oauth/authorize?client_id=' + CLIENT_ID + '&scope=PROFILE:READ+ASSETS:READ+SNAPSHOTS:READ&response_type=code&return_to=' + returnTo); 111 | break; 112 | case 404: 113 | $('#layout-container').html(this.Error404()); 114 | $('body').attr('class', 'error layout'); 115 | this.router.updatePageLinks(); 116 | break; 117 | default: 118 | if ($('#layout-container > .spinner-container').length === 1) { 119 | $('#layout-container').html(this.ErrorGeneral()); 120 | $('body').attr('class', 'error layout'); 121 | this.router.updatePageLinks(); 122 | } 123 | this.notify('error', i18n.t('general.errors.' + resp.error.code)); 124 | break; 125 | } 126 | } 127 | }, 128 | 129 | notifyError: function(type, error) { 130 | var errorInfo = ''; 131 | if (error.description) { 132 | errorInfo += error.description; 133 | } 134 | if (error.code) { 135 | errorInfo += ' ' + error.code; 136 | } 137 | if (errorInfo !== '') { 138 | this.notify('error', errorInfo); 139 | } 140 | }, 141 | 142 | notify: function(type, text) { 143 | new Noty({ 144 | type: type, 145 | layout: 'top', 146 | theme: 'nest', 147 | text: text, 148 | timeout: 3000, 149 | progressBar: false, 150 | queue: 'api', 151 | killer: 'api', 152 | force: true, 153 | animation: { 154 | open: 'animated bounceInDown', 155 | close: 'animated slideOutUp noty' 156 | } 157 | }).show(); 158 | } 159 | }; 160 | 161 | export default API; 162 | -------------------------------------------------------------------------------- /web/src/api/market.js: -------------------------------------------------------------------------------- 1 | function Market(api) { 2 | this.api = api; 3 | } 4 | 5 | Market.prototype = { 6 | markets: function (callback) { 7 | this.api.request('GET', '/markets', undefined, function (resp) { 8 | return callback(resp); 9 | }); 10 | }, 11 | 12 | market: function (callback, market) { 13 | this.api.request('GET', '/markets/' + market, undefined, function (resp) { 14 | return callback(resp); 15 | }); 16 | }, 17 | 18 | oneMarket: function (callback, baseAssetId, quoteAssetId) { 19 | this.api.request('GET', '/markets/' + baseAssetId + '-' + quoteAssetId, undefined, function (resp) { 20 | if (resp.error && resp.error.code && resp.error.code === 404) { 21 | callback({data: {base: baseAssetId, change: 0, price: 0, quote: quoteAssetId, quote_usd: 0, total: 0, volume: 0 }}); 22 | return true; 23 | } 24 | return callback(resp); 25 | }); 26 | }, 27 | 28 | candles: function (callback, market, granularity) { 29 | this.api.request('GET', '/markets/' + market + '/candles/' + granularity, undefined, function (resp) { 30 | return callback(resp); 31 | }); 32 | } 33 | }; 34 | 35 | export default Market; 36 | -------------------------------------------------------------------------------- /web/src/api/mixin.js: -------------------------------------------------------------------------------- 1 | import Account from './account.js'; 2 | 3 | function Mixin(api) { 4 | this.api = api; 5 | this.account = new Account(this); 6 | } 7 | 8 | Mixin.prototype = { 9 | environment: function () { 10 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.MixinContext) { 11 | return 'iOS'; 12 | } 13 | if (window.MixinContext && window.MixinContext.getContext) { 14 | return 'Android'; 15 | } 16 | return undefined; 17 | }, 18 | 19 | assets: function (callback) { 20 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/assets', undefined, function (resp) { 21 | return callback(resp); 22 | }); 23 | }, 24 | 25 | asset: function (callback, id) { 26 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/assets/' + id, undefined, function (resp) { 27 | return callback(resp); 28 | }); 29 | }, 30 | 31 | search: function (callback, symbol) { 32 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/network/assets/search/' + symbol, undefined, function (resp) { 33 | return callback(resp); 34 | }); 35 | }, 36 | 37 | snapshot: function (callback, snapshotId) { 38 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/snapshots/' + snapshotId, undefined, function (resp) { 39 | return callback(resp); 40 | }); 41 | }, 42 | 43 | snapshots: function (callback, offset, limit, opponent) { 44 | if (limit === undefined) { 45 | limit = 500; 46 | } 47 | var url = 'https://mixin-api.zeromesh.net/snapshots?limit=' + limit + '&offset=' + offset 48 | if (opponent) { 49 | url += '&opponent=' + opponent 50 | } 51 | this.api.requestMixin('GET', url, undefined, function (resp) { 52 | return callback(resp); 53 | }); 54 | }, 55 | 56 | conversation: function (callback, conversationId) { 57 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/conversations/' + conversationId, undefined, function (resp) { 58 | return callback(resp); 59 | }); 60 | }, 61 | 62 | verifyTrade: function(callback, trace) { 63 | this.api.requestMixin('GET', 'https://mixin-api.zeromesh.net/transfers/trace/'+trace, undefined, function(resp) { 64 | return callback(resp); 65 | }); 66 | } 67 | }; 68 | 69 | export default Mixin; 70 | -------------------------------------------------------------------------------- /web/src/api/ocean.js: -------------------------------------------------------------------------------- 1 | function Ocean(api) { 2 | this.api = api; 3 | } 4 | 5 | Ocean.prototype = { 6 | orders: function (callback, offset) { 7 | this.api.request('GET', 'https://events.ocean.one/orders?state=PENDING&order=DESC&limit=100&offset=' + offset, undefined, function (resp) { 8 | return callback(resp); 9 | }); 10 | }, 11 | 12 | ticker: function (callback, market) { 13 | this.api.request('GET', 'https://events.ocean.one/markets/' + market + '/ticker', undefined, function (resp) { 14 | return callback(resp); 15 | }); 16 | }, 17 | 18 | trades: function (callback, market, offset, limit) { 19 | if (!limit) { 20 | limit = 100; 21 | } 22 | this.api.request('GET', 'https://events.ocean.one/markets/' + market + '/trades?order=DESC&limit=' + limit + '&offset=' + offset, undefined, function (resp) { 23 | return callback(resp); 24 | }); 25 | } 26 | }; 27 | 28 | export default Ocean; 29 | -------------------------------------------------------------------------------- /web/src/app.js: -------------------------------------------------------------------------------- 1 | import 'simple-line-icons/scss/simple-line-icons.scss'; 2 | import './layout.scss'; 3 | import $ from 'jquery'; 4 | import Navigo from 'navigo'; 5 | import Locale from './locale'; 6 | import API from './api'; 7 | import Auth from './auth'; 8 | import Market from './market'; 9 | import Account from './account'; 10 | import Database from './database'; 11 | import bugsnag from 'bugsnag-js'; 12 | 13 | const PartialLoading = require('./loading.html'); 14 | const Error404 = require('./404.html'); 15 | const router = new Navigo(WEB_ROOT); 16 | const api = new API(router, API_ROOT, ENGINE_ROOT); 17 | const bugsnagClient = bugsnag('6a5f428fcc4525507ddb77cc24bdd5c8'); 18 | const db = new Database(); 19 | const OfflinePlugin = require('offline-plugin/runtime'); 20 | 21 | 22 | window.i18n = new Locale(navigator.language); 23 | 24 | router.replace = function(url) { 25 | this.resolve(url); 26 | this.pause(true); 27 | this.navigate(url); 28 | this.pause(false); 29 | }; 30 | 31 | router.hooks({ 32 | before: function(done, params) { 33 | document.title = window.i18n.t('appName'); 34 | $('body').attr('class', 'loading layout'); 35 | $('#layout-container').html(PartialLoading()); 36 | done(true); 37 | }, 38 | after: function(params) { 39 | router.updatePageLinks(); 40 | } 41 | }); 42 | 43 | OfflinePlugin.install({ 44 | onInstalled: function() { 45 | console.info('OfflinePlugin...onInstalled...'); 46 | }, 47 | 48 | onUpdating: function() { 49 | console.info('OfflinePlugin...onUpdating...'); 50 | }, 51 | 52 | onUpdateReady: function() { 53 | OfflinePlugin.applyUpdate(); 54 | }, 55 | onUpdated: function() { 56 | console.info('OfflinePlugin...onUpdated...'); 57 | // window.location.reload(); 58 | } 59 | }); 60 | 61 | router.on({ 62 | '/': function () { 63 | new Market(router, api, db, bugsnagClient).assets(); 64 | }, 65 | '/market/:base': function (params) { 66 | new Market(router, api, db, bugsnagClient).defaultMarket(params['base']); 67 | }, 68 | '/auth': function () { 69 | new Auth(router, api).render(); 70 | }, 71 | '/orders': function () { 72 | new Account(router, api, db, bugsnagClient).orders(); 73 | } 74 | }).notFound(function () { 75 | $('#layout-container').html(Error404()); 76 | $('body').attr('class', 'error layout'); 77 | router.updatePageLinks(); 78 | }).resolve(); -------------------------------------------------------------------------------- /web/src/auth/index.js: -------------------------------------------------------------------------------- 1 | import URLUtils from '../utils/url.js'; 2 | 3 | function Auth(router, api) { 4 | this.router = router; 5 | this.api = api; 6 | } 7 | 8 | Auth.prototype = { 9 | render: function () { 10 | const self = this; 11 | const error = URLUtils.getUrlParameter("error"); 12 | const authorizationCode = URLUtils.getUrlParameter("code"); 13 | var returnTo = URLUtils.getUrlParameter("return_to"); 14 | if (returnTo === undefined || returnTo === null || returnTo === "") { 15 | returnTo = "/"; 16 | } 17 | returnTo = WEB_ROOT + returnTo; 18 | if (error === 'access_denied') { 19 | self.api.notify('error', i18n.t('general.errors.403')); 20 | window.location.replace(returnTo); 21 | return; 22 | } 23 | self.api.account.authenticate(function (resp) { 24 | if (resp.error && resp.error.code === 403) { 25 | self.api.notify('error', i18n.t('general.errors.403')); 26 | window.location.replace(returnTo); 27 | return true; 28 | } 29 | if (resp.error) { 30 | return false; 31 | } 32 | window.location.replace(returnTo); 33 | }, authorizationCode); 34 | } 35 | }; 36 | 37 | export default Auth; 38 | -------------------------------------------------------------------------------- /web/src/constant.scss: -------------------------------------------------------------------------------- 1 | $font-main-content: 'Maven Pro', sans-serif; 2 | $font-main-title: 'Roboto', sans-serif; 3 | $font-main-mono: 'Roboto Mono', monospace; 4 | 5 | $color-main-background: #FFFFFF; 6 | $color-main-foreground-dark: #000000; 7 | $color-main-foreground-light: #333333; 8 | $color-main-highlight: #00B0E9; 9 | 10 | $color-side-bid: #00B56E; 11 | $color-side-bid-light: #127B57; 12 | $color-side-ask: #E55541; 13 | $color-side-ask-light: #824243; 14 | -------------------------------------------------------------------------------- /web/src/database/asset.js: -------------------------------------------------------------------------------- 1 | 2 | function Asset(database) { 3 | this.database = database; 4 | const assets = require('./assets.json'); 5 | this.cache(assets); 6 | this.btcAsset = this.cacheAssets['c6d0c728-2624-429b-8e0d-d9d19b6592fa']; 7 | this.xinAsset = this.cacheAssets['c94ac88f-4671-3976-b60a-09064f1811e8']; 8 | this.usdtAsset = this.cacheAssets['815b0b1a-2764-3736-8faa-42d694fa620a']; 9 | this.pusdAsset = this.cacheAssets['31d2ea9c-95eb-3355-b65b-ba096853bc18']; 10 | } 11 | 12 | Asset.prototype = { 13 | 14 | getById: function (assetId) { 15 | const asset = this.cacheAssets[assetId]; 16 | if (asset) { 17 | return asset; 18 | } 19 | switch (assetId) { 20 | case this.btcAsset.asset_id: 21 | return this.btcAsset; 22 | case this.xinAsset.asset_id: 23 | return this.xinAsset; 24 | case this.usdtAsset.asset_id: 25 | return this.usdtAsset; 26 | case this.pusdAsset.asset_id: 27 | return this.pusdAsset; 28 | default: 29 | return null; 30 | } 31 | }, 32 | 33 | getBySymbol: function (symbol) { 34 | switch (symbol) { 35 | case this.btcAsset.symbol: 36 | return this.btcAsset; 37 | case this.xinAsset.symbol: 38 | return this.xinAsset; 39 | case this.usdtAsset.symbol: 40 | return this.usdtAsset; 41 | case this.pusdAsset.symbol: 42 | return this.pusdAsset; 43 | default: 44 | return null; 45 | } 46 | }, 47 | 48 | cache: function (assets) { 49 | var cacheAssets = {}; 50 | for (var j = 0; j < assets.length; j++) { 51 | const asset = assets[j]; 52 | cacheAssets[asset.asset_id] = asset; 53 | } 54 | this.cacheAssets = cacheAssets; 55 | }, 56 | 57 | saveAsset: function (asset, callback) { 58 | const assetTable = this.database.db.getSchema().table('assets'); 59 | var row = assetTable.createRow({ 60 | 'asset_id': asset.asset_id, 61 | 'chain_id': asset.chain_id, 62 | 'icon_url': asset.icon_url, 63 | 'symbol': asset.symbol, 64 | 'balance': asset.balance, 65 | 'price_usd': asset.price_usd, 66 | 'name': asset.name 67 | }); 68 | this.database.db.insertOrReplace().into(assetTable).values([row]).exec().then(function(rows) { 69 | if (callback) { 70 | callback(rows); 71 | } 72 | }); 73 | }, 74 | 75 | saveAssets: function (assets, callback) { 76 | const assetTable = this.database.db.getSchema().table('assets'); 77 | var rows = []; 78 | for (var i = 0; i < assets.length; i++) { 79 | const asset = assets[i]; 80 | rows.push(assetTable.createRow({ 81 | 'asset_id': asset.asset_id, 82 | 'chain_id': asset.chain_id, 83 | 'icon_url': asset.icon_url, 84 | 'symbol': asset.symbol, 85 | 'balance': asset.balance, 86 | 'price_usd': asset.price_usd, 87 | 'name': asset.name 88 | })); 89 | } 90 | this.database.db.insertOrReplace().into(assetTable).values(rows).exec().then(function(rows) { 91 | if (callback) { 92 | callback(rows); 93 | } 94 | }); 95 | }, 96 | 97 | fetchAssets: function (callback) { 98 | const assetTable = this.database.db.getSchema().table('assets'); 99 | this.database.db.select().from(assetTable).exec().then(function(rows) { 100 | callback(rows); 101 | }); 102 | } 103 | 104 | }; 105 | 106 | export default Asset; 107 | -------------------------------------------------------------------------------- /web/src/database/assets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "chain_id": "c6d0c728-2624-429b-8e0d-d9d19b6592fa", 4 | "asset_id": "c6d0c728-2624-429b-8e0d-d9d19b6592fa", 5 | "icon_url": "https://images.mixin.one/HvYGJsV5TGeZ-X9Ek3FEQohQZ3fE9LBEBGcOcn4c4BNHovP4fW4YB97Dg5LcXoQ1hUjMEgjbl1DPlKg1TW7kK6XP=s128", 6 | "name": "Bitcoin", 7 | "symbol": "BTC" 8 | }, 9 | { 10 | "chain_id": "c6d0c728-2624-429b-8e0d-d9d19b6592fa", 11 | "asset_id": "815b0b1a-2764-3736-8faa-42d694fa620a", 12 | "icon_url": "https://images.mixin.one/ndNBEpObYs7450U08oAOMnSEPzN66SL8Mh-f2pPWBDeWaKbXTPUIdrZph7yj8Z93Rl8uZ16m7Qjz-E-9JFKSsJ-F=s128", 13 | "name": "Tether USD", 14 | "symbol": "USDT" 15 | }, 16 | { 17 | "chain_id": "43d61dcd-e413-450d-80b8-101d5e903357", 18 | "asset_id": "c94ac88f-4671-3976-b60a-09064f1811e8", 19 | "icon_url": "https://images.mixin.one/E2y0BnTopFK9qey0YI-8xV3M82kudNnTaGw0U5SU065864SsewNUo6fe9kDF1HIzVYhXqzws4lBZnLj1lPsjk-0=s128", 20 | "name": "Mixin", 21 | "symbol": "XIN" 22 | }, 23 | { 24 | "chain_id": "43d61dcd-e413-450d-80b8-101d5e903357", 25 | "asset_id": "31d2ea9c-95eb-3355-b65b-ba096853bc18", 26 | "icon_url": "https://mixin-images.zeromesh.net/cH4GWuPXbzeZl6OOunpn7BxE25n3v8URwnNszs0FpZqv3OTlxP1zpzKw89VKTpBwWL-Ned1R36mmy1C4GMuPX1rL-PjfEJ2zby9V=s128", 27 | "name": "Pando USD", 28 | "symbol": "pUSD" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /web/src/database/index.js: -------------------------------------------------------------------------------- 1 | import Asset from './asset.js'; 2 | import Order from './order.js'; 3 | import Trade from './trade.js'; 4 | import Transfer from './transfer.js'; 5 | import Market from './market.js'; 6 | 7 | function Database() { 8 | this.lf = require("lovefield"); 9 | this.asset = new Asset(this); 10 | this.trade = new Trade(this); 11 | this.order = new Order(this); 12 | this.transfer = new Transfer(this); 13 | this.market = new Market(this); 14 | } 15 | 16 | Database.prototype = { 17 | 18 | prepare: function(callback) { 19 | const self = this; 20 | if (self.db) { 21 | if (callback) { 22 | callback(); 23 | } 24 | } else { 25 | var schemaBuilder = lf.schema.create('mixcoin', 10); 26 | 27 | schemaBuilder.createTable('assets'). 28 | addColumn('asset_id', lf.Type.STRING). 29 | addColumn('chain_id', lf.Type.STRING). 30 | addColumn('icon_url', lf.Type.STRING). 31 | addColumn('symbol', lf.Type.STRING). 32 | addColumn('balance', lf.Type.STRING). 33 | addColumn('price_usd', lf.Type.STRING). 34 | addColumn('name', lf.Type.STRING). 35 | addPrimaryKey(['asset_id']); 36 | 37 | schemaBuilder.createTable('markets'). 38 | addColumn('base', lf.Type.STRING). 39 | addColumn('quote', lf.Type.STRING). 40 | addColumn('price', lf.Type.STRING). 41 | addColumn('volume', lf.Type.STRING). 42 | addColumn('total', lf.Type.STRING). 43 | addColumn('change', lf.Type.STRING). 44 | addColumn('favorite_time', lf.Type.STRING). 45 | addColumn('source', lf.Type.STRING). //SERVER/CLIENT 46 | addPrimaryKey(['base', 'quote']); 47 | 48 | schemaBuilder.createTable('trades'). 49 | addColumn('trade_id', lf.Type.STRING). 50 | addColumn('side', lf.Type.STRING). 51 | addColumn('quote', lf.Type.STRING). 52 | addColumn('base', lf.Type.STRING). 53 | addColumn('price', lf.Type.NUMBER). 54 | addColumn('amount', lf.Type.NUMBER). 55 | addColumn('created_at', lf.Type.STRING). 56 | addPrimaryKey(['trade_id']). 57 | addIndex('idx_created_at', ['base', 'quote', 'created_at'], true, lf.Order.DESC); 58 | 59 | schemaBuilder.createTable('orders'). 60 | addColumn('order_id', lf.Type.STRING). 61 | addColumn('order_type', lf.Type.STRING). 62 | addColumn('quote_asset_id', lf.Type.STRING). 63 | addColumn('base_asset_id', lf.Type.STRING). 64 | addColumn('amount', lf.Type.STRING). 65 | addColumn('filled_amount', lf.Type.STRING). 66 | addColumn('side', lf.Type.STRING). 67 | addColumn('price', lf.Type.STRING). 68 | addColumn('state', lf.Type.STRING). 69 | addColumn('created_at', lf.Type.STRING). 70 | addPrimaryKey(['order_id']). 71 | addIndex('idx_created_at', ['created_at'], true, lf.Order.DESC). 72 | addIndex('idx_state', ['state']); 73 | 74 | schemaBuilder.createTable('transfers'). 75 | addColumn('transfer_id', lf.Type.STRING). 76 | addColumn('source', lf.Type.STRING). 77 | addColumn('amount', lf.Type.STRING). 78 | addColumn('asset_id', lf.Type.STRING). 79 | addColumn('order_id', lf.Type.STRING). 80 | addColumn('ask_order_id', lf.Type.STRING). 81 | addColumn('bid_order_id', lf.Type.STRING). 82 | addColumn('created_at', lf.Type.STRING). 83 | addPrimaryKey(['transfer_id']); 84 | 85 | schemaBuilder.connect().then(function(database) { 86 | self.db = database; 87 | if (callback) { 88 | callback(); 89 | } 90 | }); 91 | } 92 | } 93 | 94 | }; 95 | 96 | export default Database; 97 | -------------------------------------------------------------------------------- /web/src/database/market.js: -------------------------------------------------------------------------------- 1 | import TimeUtils from '../utils/time.js'; 2 | import {BigNumber} from 'bignumber.js'; 3 | 4 | function Market(database) { 5 | this.database = database; 6 | } 7 | 8 | Market.prototype = { 9 | 10 | saveMarket: function (callback, market) { 11 | const marketTable = this.database.db.getSchema().table('markets'); 12 | var row = marketTable.createRow({ 13 | 'base': market.base, 14 | 'quote': market.quote, 15 | 'price': market.price, 16 | 'volume': market.volume, 17 | 'total': market.total, 18 | 'change': market.change, 19 | 'source': market.source, 20 | 'favorite_time': '' 21 | }); 22 | this.database.db.insertOrReplace().into(marketTable).values([row]).exec().then(function(rows) { 23 | if (callback) { 24 | callback(market); 25 | } 26 | }); 27 | }, 28 | 29 | saveMarkets: function (callback, markets) { 30 | const self = this; 31 | const marketTable = this.database.db.getSchema().table('markets'); 32 | var rows = []; 33 | for (var i = 0; i < markets.length; i++) { 34 | const market = markets[i]; 35 | rows.push(marketTable.createRow({ 36 | 'base': market.base, 37 | 'quote': market.quote, 38 | 'price': market.price, 39 | 'volume': market.volume, 40 | 'total': market.total, 41 | 'change': market.change, 42 | 'source': market.source, 43 | 'favorite_time': '' 44 | })); 45 | } 46 | this.database.db.insertOrReplace().into(marketTable).values(rows).exec().then(function(rows) { 47 | if (callback) { 48 | self.database.db.select().from(marketTable).where(marketTable.price.gt('0')).exec().then(function(rows) { 49 | callback(rows); 50 | }); 51 | } 52 | }); 53 | }, 54 | 55 | getMarket: function (callback, baseAssetId, quoteAssetId) { 56 | const marketTable = this.database.db.getSchema().table('markets'); 57 | const predicate = lf.op.and(marketTable.base.eq(baseAssetId), marketTable.quote.eq(quoteAssetId)); 58 | this.database.db.select().from(marketTable).where(predicate).exec().then(function(rows) { 59 | callback(rows[0]); 60 | }); 61 | }, 62 | 63 | updateClientMarket: function (callback, baseAssetId, quoteAssetId, market) { 64 | const db = this.database.db; 65 | const tx = db.createTransaction(); 66 | const marketTable = db.getSchema().table('markets'); 67 | const tradeTable = db.getSchema().table('trades'); 68 | const marketPredicate = lf.op.and(marketTable.base.eq(baseAssetId), marketTable.quote.eq(quoteAssetId)); 69 | 70 | tx.begin([tradeTable, marketTable]).then(function() { 71 | const date = TimeUtils.rfc3339(new Date(new Date().getTime() - 24*60*60*1000)); 72 | const predicate = lf.op.and(tradeTable.base.eq(baseAssetId), tradeTable.quote.eq(quoteAssetId), tradeTable.created_at.gte(date)); 73 | return tx.attach(db.select(tradeTable.amount, tradeTable.price).from(tradeTable).where(predicate).limit(1000).orderBy(tradeTable.created_at, lf.Order.DESC)); 74 | }).then(function(trades) { 75 | if (trades.length == 0) { 76 | return Promise.resolve(); 77 | } 78 | var total = new BigNumber(0); 79 | var volume = new BigNumber(0); 80 | var change = new BigNumber(0); 81 | const lastTrade = trades[0]; 82 | 83 | for (var i = 0; i < trades.length; i++) { 84 | const trade = trades[i]; 85 | const amount = new BigNumber(trade.amount); 86 | volume = volume.plus(amount); 87 | total = total.plus(amount.times(trade.price)); 88 | } 89 | 90 | const open = new BigNumber(trades[trades.length - 1].price); 91 | const close = new BigNumber(trades[0].price); 92 | change = close.minus(open).div(open); 93 | 94 | if (market) { 95 | return tx.attach(db.update(marketTable) 96 | .set(marketTable.volume, volume.toString()) 97 | .set(marketTable.total, total.toString()) 98 | .set(marketTable.change, change.toString()) 99 | .where(marketPredicate)); 100 | } else { 101 | const row = marketTable.createRow({ 102 | 'base': baseAssetId, 103 | 'quote': quoteAssetId, 104 | 'price': lastTrade.price, 105 | 'volume': volume.toString(), 106 | 'total': total.toString(), 107 | 'change': change.toString(), 108 | 'source': 'CLIENT', 109 | 'favorite_time': '' 110 | }); 111 | return tx.attach(db.insertOrReplace().into(marketTable).values([row])); 112 | } 113 | }).then(function() { 114 | const predicate = lf.op.and(tradeTable.base.eq(baseAssetId), tradeTable.quote.eq(quoteAssetId)); 115 | return tx.attach(db.select().from(tradeTable).where(predicate).limit(1).orderBy(tradeTable.created_at, lf.Order.DESC)); 116 | }).then(function(trades) { 117 | const lastTrade = trades[0]; 118 | if (!lastTrade) { 119 | return Promise.resolve(); 120 | } 121 | if (market) { 122 | return tx.attach(db.update(marketTable).set(marketTable.price, lastTrade.price).where(marketPredicate)); 123 | } else { 124 | const row = marketTable.createRow({ 125 | 'base': baseAssetId, 126 | 'quote': quoteAssetId, 127 | 'price': lastTrade.price, 128 | 'volume': '0', 129 | 'total': '0', 130 | 'change': '0', 131 | 'source': 'CLIENT', 132 | 'favorite_time': '' 133 | }); 134 | return tx.attach(db.insertOrReplace().into(marketTable).values([row])); 135 | } 136 | }).then(function() { 137 | return tx.attach(db.select().from(marketTable).where(marketPredicate)); 138 | }).then(function(markets) { 139 | callback(markets[0]); 140 | return tx.commit(); 141 | }); 142 | }, 143 | 144 | fetchMarkets: function (callback) { 145 | const marketTable = this.database.db.getSchema().table('markets'); 146 | const predicate = marketTable.price.gt('0'); 147 | this.database.db.select().from(marketTable).where(predicate).exec().then(function(rows) { 148 | callback(rows); 149 | }); 150 | } 151 | 152 | }; 153 | 154 | export default Market; 155 | -------------------------------------------------------------------------------- /web/src/database/order.js: -------------------------------------------------------------------------------- 1 | 2 | function Order(database) { 3 | this.database = database; 4 | } 5 | 6 | Order.prototype = { 7 | 8 | saveOrders: function (db, orders) { 9 | const orderTable = db.getSchema().table('orders'); 10 | var rows = []; 11 | for (var i = 0; i < orders.length; i++) { 12 | var order = orders[i]; 13 | order.amount = order.amount.toString(); 14 | order.filled_amount = order.filled_amount.toString(); 15 | rows.push(orderTable.createRow({ 16 | 'order_id': order.order_id, 17 | 'order_type': order.order_type, 18 | 'quote_asset_id': order.quote_asset_id, 19 | 'base_asset_id': order.base_asset_id, 20 | 'amount': order.amount, 21 | 'filled_amount': order.filled_amount, 22 | 'side': order.side, 23 | 'price': order.price, 24 | 'state': order.state, 25 | 'created_at': order.created_at 26 | })); 27 | } 28 | return db.insertOrReplace().into(orderTable).values(rows); 29 | }, 30 | 31 | fetchOrders: function (callback) { 32 | const orderTable = this.database.db.getSchema().table('orders'); 33 | this.database.db.select().from(orderTable).orderBy(orderTable.created_at, lf.Order.DESC).exec().then(function(rows) { 34 | callback(rows); 35 | }); 36 | }, 37 | 38 | canceledOrder: function (orderId) { 39 | const orderTable = this.database.db.getSchema().table('orders'); 40 | this.database.db.update(orderTable).set(orderTable.state, 'DONE').where(orderTable.order_id.eq(orderId)).exec(); 41 | }, 42 | 43 | getOrder: function (callback, orderId) { 44 | const orderTable = this.database.db.getSchema().table('orders'); 45 | this.database.db.select().from(orderTable).where(orderTable.order_id.eq(orderId)).exec().then(function(rows) { 46 | callback(rows); 47 | }); 48 | } 49 | 50 | }; 51 | 52 | export default Order; 53 | -------------------------------------------------------------------------------- /web/src/database/trade.js: -------------------------------------------------------------------------------- 1 | import TimeUtils from '../utils/time.js'; 2 | import {BigNumber} from 'bignumber.js'; 3 | 4 | function Trade(database) { 5 | this.database = database; 6 | } 7 | 8 | Trade.prototype = { 9 | 10 | saveTrades: function (callback, trades, baseAssetId, quoteAssetId, market) { 11 | const db = this.database.db; 12 | const tradeTable = db.getSchema().table('trades'); 13 | const marketTable = db.getSchema().table('markets'); 14 | const predicate = lf.op.and(tradeTable.base.eq(baseAssetId), tradeTable.quote.eq(quoteAssetId)); 15 | 16 | if (trades.length > 0) { 17 | const tx = db.createTransaction(); 18 | tx.begin([tradeTable, marketTable]).then(function() { 19 | var rows = []; 20 | for (var i = 0; i < trades.length; i++) { 21 | const trade = trades[i]; 22 | rows.push(tradeTable.createRow({ 23 | 'trade_id': trade.trade_id, 24 | 'side': trade.side, 25 | 'quote': trade.quote, 26 | 'base': trade.base, 27 | 'price': trade.price, 28 | 'amount': trade.amount, 29 | 'created_at': trade.created_at 30 | })); 31 | } 32 | return tx.attach(db.insertOrReplace().into(tradeTable).values(rows)); 33 | }).then(function() { 34 | return tx.attach(db.select().from(tradeTable).where(predicate).limit(50).orderBy(tradeTable.created_at, lf.Order.DESC)); 35 | }).then(function(rows) { 36 | callback(rows); 37 | return tx.commit(); 38 | }); 39 | } else { 40 | db.select().from(tradeTable).where(predicate).limit(50).orderBy(tradeTable.created_at, lf.Order.DESC).exec().then(function(rows) { 41 | callback(rows); 42 | }); 43 | } 44 | }, 45 | 46 | fetchTrades: function (callback, baseAssetId, quoteAssetId, limit) { 47 | const tradeTable = this.database.db.getSchema().table('trades'); 48 | const predicate = lf.op.and(tradeTable.base.eq(baseAssetId), tradeTable.quote.eq(quoteAssetId)); 49 | this.database.db.select().from(tradeTable).where(predicate).limit(limit).orderBy(tradeTable.created_at, lf.Order.DESC).exec().then(function(rows) { 50 | callback(rows); 51 | }); 52 | }, 53 | 54 | getLastTrade: function (callback, baseAssetId, quoteAssetId) { 55 | return this.fetchTrades(callback, baseAssetId, quoteAssetId, 1) 56 | } 57 | 58 | }; 59 | 60 | export default Trade; 61 | -------------------------------------------------------------------------------- /web/src/database/transfer.js: -------------------------------------------------------------------------------- 1 | 2 | function Transfer(database) { 3 | this.database = database; 4 | } 5 | 6 | Transfer.prototype = { 7 | 8 | saveTransfers: function (db, transfers) { 9 | const transferTable = db.getSchema().table('transfers'); 10 | var rows = []; 11 | for (var i = 0; i < transfers.length; i++) { 12 | const transfer = transfers[i]; 13 | rows.push(transferTable.createRow({ 14 | 'transfer_id': transfer.transfer_id, 15 | 'source': transfer.source, 16 | 'amount': transfer.amount, 17 | 'asset_id': transfer.asset_id, 18 | 'order_id': transfer.order_id, 19 | 'ask_order_id': transfer.ask_order_id, 20 | 'bid_order_id': transfer.bid_order_id, 21 | 'created_at': transfer.created_at 22 | })); 23 | } 24 | return db.insertOrReplace().into(transferTable).values(rows); 25 | }, 26 | 27 | getTransfers: function (callback, orderId) { 28 | const transferTable = this.database.db.getSchema().table('transfers'); 29 | const predicate = lf.op.or(transferTable.ask_order_id.eq(orderId), transferTable.bid_order_id.eq(orderId), transferTable.order_id.eq(orderId)); 30 | this.database.db.select().from(transferTable).where(predicate).exec().then(function(rows) { 31 | callback(rows); 32 | }); 33 | } 34 | 35 | }; 36 | 37 | export default Transfer; 38 | -------------------------------------------------------------------------------- /web/src/error.html: -------------------------------------------------------------------------------- 1 |
2 |

ERROR

3 |
4 | -------------------------------------------------------------------------------- /web/src/fonts/index.scss: -------------------------------------------------------------------------------- 1 | /* roboto-100 - latin */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-weight: 100; 6 | src: local('Roboto Thin'), local('Roboto-Thin'), 7 | url('fonts/roboto/roboto-v16-latin-100.woff2') format('woff2'); 8 | } 9 | 10 | /* roboto-300 - latin */ 11 | @font-face { 12 | font-family: 'Roboto'; 13 | font-style: normal; 14 | font-weight: 300; 15 | src: local('Roboto Light'), local('Roboto-Light'), 16 | url('fonts/roboto/roboto-v16-latin-300.woff2') format('woff2'); 17 | } 18 | 19 | /* roboto-regular - latin */ 20 | @font-face { 21 | font-family: 'Roboto'; 22 | font-style: normal; 23 | font-weight: 400; 24 | src: local('Roboto'), local('Roboto-Regular'), 25 | url('fonts/roboto/roboto-v16-latin-regular.woff2') format('woff2'); 26 | } 27 | 28 | /* roboto-500 - latin */ 29 | @font-face { 30 | font-family: 'Roboto'; 31 | font-style: normal; 32 | font-weight: 500; 33 | src: local('Roboto Medium'), local('Roboto-Medium'), 34 | url('fonts/roboto/roboto-v16-latin-500.woff2') format('woff2'); 35 | } 36 | 37 | /* roboto-mono-100 - latin */ 38 | @font-face { 39 | font-family: 'Roboto Mono'; 40 | font-style: normal; 41 | font-weight: 100; 42 | src: local('Roboto Mono Thin'), local('RobotoMono-Thin'), 43 | url('fonts/roboto/roboto-mono-v4-latin-100.woff2') format('woff2'); 44 | } 45 | 46 | /* roboto-mono-300 - latin */ 47 | @font-face { 48 | font-family: 'Roboto Mono'; 49 | font-style: normal; 50 | font-weight: 300; 51 | src: local('Roboto Mono Light'), local('RobotoMono-Light'), 52 | url('fonts/roboto/roboto-mono-v4-latin-300.woff2') format('woff2'); 53 | } 54 | 55 | /* roboto-mono-regular - latin */ 56 | @font-face { 57 | font-family: 'Roboto Mono'; 58 | font-style: normal; 59 | font-weight: 400; 60 | src: local('Roboto Mono'), local('RobotoMono-Regular'), 61 | url('fonts/roboto/roboto-mono-v4-latin-regular.woff2') format('woff2'); 62 | } 63 | 64 | /* roboto-mono-500 - latin */ 65 | @font-face { 66 | font-family: 'Roboto Mono'; 67 | font-style: normal; 68 | font-weight: 500; 69 | src: local('Roboto Mono Medium'), local('RobotoMono-Medium'), 70 | url('fonts/roboto/roboto-mono-v4-latin-500.woff2') format('woff2'); 71 | } 72 | 73 | /* maven-pro-regular - latin */ 74 | @font-face { 75 | font-family: 'Maven Pro'; 76 | font-style: normal; 77 | font-weight: 400; 78 | src: local('Maven Pro Regular'), local('MavenPro-Regular'), 79 | url('fonts/maven-pro/maven-pro-v9-latin-regular.woff2') format('woff2'); 80 | } 81 | 82 | /* maven-pro-500 - latin */ 83 | @font-face { 84 | font-family: 'Maven Pro'; 85 | font-style: normal; 86 | font-weight: 500; 87 | src: local('Maven Pro Medium'), local('MavenPro-Medium'), 88 | url('fonts/maven-pro/maven-pro-v9-latin-500.woff2') format('woff2'); 89 | } 90 | 91 | /* maven-pro-700 - latin */ 92 | @font-face { 93 | font-family: 'Maven Pro'; 94 | font-style: normal; 95 | font-weight: 700; 96 | src: local('Maven Pro Bold'), local('MavenPro-Bold'), 97 | url('fonts/maven-pro/maven-pro-v9-latin-700.woff2') format('woff2'); 98 | } 99 | -------------------------------------------------------------------------------- /web/src/fonts/maven-pro/maven-pro-v9-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/maven-pro/maven-pro-v9-latin-500.woff2 -------------------------------------------------------------------------------- /web/src/fonts/maven-pro/maven-pro-v9-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/maven-pro/maven-pro-v9-latin-700.woff2 -------------------------------------------------------------------------------- /web/src/fonts/maven-pro/maven-pro-v9-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/maven-pro/maven-pro-v9-latin-regular.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-mono-v4-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-mono-v4-latin-100.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-mono-v4-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-mono-v4-latin-300.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-mono-v4-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-mono-v4-latin-500.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-mono-v4-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-mono-v4-latin-regular.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-v16-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-v16-latin-100.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-v16-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-v16-latin-300.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-v16-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-v16-latin-500.woff2 -------------------------------------------------------------------------------- /web/src/fonts/roboto/roboto-v16-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/fonts/roboto/roboto-v16-latin-regular.woff2 -------------------------------------------------------------------------------- /web/src/helpers/dimZero.js: -------------------------------------------------------------------------------- 1 | var handlebars = require('handlebars'); 2 | 3 | module.exports = function(value_str) { 4 | var res = /0{1,}$/.exec(value_str); 5 | if (res) { 6 | value_str = new handlebars.SafeString(res.input.slice(0,res.index) + '' + res[0] + ''); 7 | } 8 | res = /^0.0{0,}/.exec(value_str); 9 | if (res) { 10 | value_str = new handlebars.SafeString('' + res[0] + '' + res.input.slice(res[0].length,value_str.length)); 11 | } 12 | return value_str; 13 | }; 14 | -------------------------------------------------------------------------------- /web/src/helpers/msgpack.js: -------------------------------------------------------------------------------- 1 | function Msgpack() { 2 | 3 | } 4 | 5 | Msgpack.prototype = { 6 | decodeArray: function (uuidParse, buf, offset, length, headerLength) { 7 | var result = [] 8 | var i 9 | var totalBytesConsumed = 0 10 | 11 | offset += headerLength 12 | for (i = 0; i < length; i++) { 13 | var decodeResult = this.tryDecode(uuidParse, buf, offset) 14 | if (decodeResult) { 15 | result.push(decodeResult.value) 16 | offset += decodeResult.length 17 | totalBytesConsumed += decodeResult.length 18 | } else { 19 | return null 20 | } 21 | } 22 | return { value: uuidParse.unparse(result), length: headerLength + totalBytesConsumed } 23 | }, 24 | 25 | getSize: function (first) { 26 | switch (first) { 27 | case 0xc4: 28 | return 2 29 | case 0xc5: 30 | return 3 31 | case 0xc6: 32 | return 5 33 | case 0xc7: 34 | return 3 35 | case 0xc8: 36 | return 4 37 | case 0xc9: 38 | return 6 39 | case 0xca: 40 | return 5 41 | case 0xcb: 42 | return 9 43 | case 0xcc: 44 | return 2 45 | case 0xcd: 46 | return 3 47 | case 0xce: 48 | return 5 49 | case 0xcf: 50 | return 9 51 | case 0xd0: 52 | return 2 53 | case 0xd1: 54 | return 3 55 | case 0xd2: 56 | return 5 57 | case 0xd3: 58 | return 9 59 | case 0xd4: 60 | return 3 61 | case 0xd5: 62 | return 4 63 | case 0xd6: 64 | return 6 65 | case 0xd7: 66 | return 10 67 | case 0xd8: 68 | return 18 69 | case 0xd9: 70 | return 2 71 | case 0xda: 72 | return 3 73 | case 0xdb: 74 | return 5 75 | case 0xde: 76 | return 3 77 | default: 78 | return -1 79 | } 80 | }, 81 | 82 | hasMinBufferSize: function (first, length) { 83 | var size = this.getSize(first) 84 | 85 | if (size !== -1 && length < size) { 86 | return false 87 | } else { 88 | return true 89 | } 90 | }, 91 | 92 | tryDecode: function (uuidParse, buf, offset) { 93 | offset = offset === undefined ? 0 : offset 94 | var bufLength = buf.length - offset 95 | if (bufLength <= 0) { 96 | return null; 97 | } 98 | 99 | var type = buf.readUInt8(offset); 100 | if (!this.hasMinBufferSize(type, bufLength)) { 101 | return null 102 | } 103 | 104 | switch (type) { 105 | case 0xc0: 106 | return { value: null, length: 1 }; 107 | case 0xc2: 108 | return { value: false, length: 1 }; 109 | case 0xc3: 110 | return { value: true, length: 1 }; 111 | case 0xcc: // 1-byte unsigned int 112 | return { value: buf.readUInt8(offset + 1), length: 2 }; 113 | case 0xcd: // 2-bytes BE unsigned int 114 | return { value: buf.readUInt16BE(offset + 1), length: 3 }; 115 | case 0xce: // 4-bytes BE unsigned int 116 | return { value: buf.readUInt32BE(offset + 1), length: 5 }; 117 | case 0xd0: // 1-byte signed int 118 | return { value: buf.readInt8(offset + 1), length: 2 }; 119 | case 0xd1: 120 | return { value: buf.readInt16BE(offset + 1), length: 3 }; 121 | case 0xd2: 122 | return { value: buf.readInt32BE(offset + 1), length: 5 }; 123 | case 0xd9: // strings up to 2^8 - 1 bytes 124 | length = buf.readUInt8(offset + 1); 125 | return { value: buf.toString('utf8', offset + 2, offset + 2 + length), length: length }; 126 | case 0xda: // strings up to 2^16 - 2 bytes 127 | length = buf.readUInt16BE(offset + 1) 128 | return { value: buf.toString('utf8', offset + 3, offset + 3 + length), length: length }; 129 | case 0xdb: // strings up to 2^32 - 4 bytes 130 | length = buf.readUInt32BE(offset + 1); 131 | return { value: buf.toString('utf8', offset + 5, offset + 5 + length), length: length }; 132 | case 0xdc: 133 | length = buf.readUInt16BE(offset + 1) 134 | return this.decodeArray(uuidParse, buf, offset, length, 3); 135 | case 0xb0: 136 | length = buf.readUInt16BE(offset + 1); 137 | return { value: uuidParse.unparse(buf.slice(offset + 1, offset + 1 + 16)), length: 16 + 1 }; 138 | default: 139 | if ((type & 0xf0) === 0x90) { 140 | length = type & 0x0f; 141 | return this.decodeArray(uuidParse, buf, offset, length, 1); 142 | } else if ((type & 0xf0) === 0x80) { 143 | length = type & 0x0f; 144 | return this.decodeMap(uuidParse, buf, offset, length, 1); 145 | } else if ((type & 0xe0) === 0xa0) { 146 | length = type & 0x1f 147 | return { value: buf.toString('utf8', offset + 1, offset + length + 1), length: length + 1 }; 148 | } else if (type >= 0xe0) { 149 | return { value: type - 0x100, length: 1 }; 150 | } else if (type < 0x80) { 151 | return { value: type, length: 1 }; 152 | } 153 | break; 154 | } 155 | return null; 156 | }, 157 | 158 | decodeMap: function (buf, offset, length, headerLength) { 159 | var result = {}; 160 | offset += headerLength; 161 | 162 | const uuidParse = require('uuid-parse'); 163 | 164 | for (var i = 0; i < length; i++) { 165 | const key = this.tryDecode(uuidParse, buf, offset); 166 | if (key) { 167 | offset += key.length; 168 | const value = this.tryDecode(uuidParse, buf, offset); 169 | if (value) { 170 | result[key.value] = value.value; 171 | offset += value.length; 172 | } 173 | } 174 | } 175 | 176 | return result 177 | } 178 | }; 179 | 180 | export default Msgpack; -------------------------------------------------------------------------------- /web/src/helpers/t.js: -------------------------------------------------------------------------------- 1 | module.exports = function(value, options) { 2 | return window.i18n.t(value, options.hash); 3 | }; 4 | -------------------------------------------------------------------------------- /web/src/jquery-color-plus-names.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Color Animations v2.1.2 3 | * https://github.com/jquery/jquery-color 4 | * 5 | * Copyright 2013 jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * Date: Wed Jan 16 08:47:09 2013 -0600 10 | */ 11 | function jQueryColor(jQuery) { 12 | (function( jQuery, undefined ) { 13 | 14 | var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", 15 | 16 | // plusequals test for += 100 -= 100 17 | rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, 18 | // a set of RE's that can match strings and generate color tuples. 19 | stringParsers = [{ 20 | re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, 21 | parse: function( execResult ) { 22 | return [ 23 | execResult[ 1 ], 24 | execResult[ 2 ], 25 | execResult[ 3 ], 26 | execResult[ 4 ] 27 | ]; 28 | } 29 | }, { 30 | re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, 31 | parse: function( execResult ) { 32 | return [ 33 | execResult[ 1 ] * 2.55, 34 | execResult[ 2 ] * 2.55, 35 | execResult[ 3 ] * 2.55, 36 | execResult[ 4 ] 37 | ]; 38 | } 39 | }, { 40 | // this regex ignores A-F because it's compared against an already lowercased string 41 | re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/, 42 | parse: function( execResult ) { 43 | return [ 44 | parseInt( execResult[ 1 ], 16 ), 45 | parseInt( execResult[ 2 ], 16 ), 46 | parseInt( execResult[ 3 ], 16 ) 47 | ]; 48 | } 49 | }, { 50 | // this regex ignores A-F because it's compared against an already lowercased string 51 | re: /#([a-f0-9])([a-f0-9])([a-f0-9])/, 52 | parse: function( execResult ) { 53 | return [ 54 | parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), 55 | parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), 56 | parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ) 57 | ]; 58 | } 59 | }, { 60 | re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, 61 | space: "hsla", 62 | parse: function( execResult ) { 63 | return [ 64 | execResult[ 1 ], 65 | execResult[ 2 ] / 100, 66 | execResult[ 3 ] / 100, 67 | execResult[ 4 ] 68 | ]; 69 | } 70 | }], 71 | 72 | // jQuery.Color( ) 73 | color = jQuery.Color = function( color, green, blue, alpha ) { 74 | return new jQuery.Color.fn.parse( color, green, blue, alpha ); 75 | }, 76 | spaces = { 77 | rgba: { 78 | props: { 79 | red: { 80 | idx: 0, 81 | type: "byte" 82 | }, 83 | green: { 84 | idx: 1, 85 | type: "byte" 86 | }, 87 | blue: { 88 | idx: 2, 89 | type: "byte" 90 | } 91 | } 92 | }, 93 | 94 | hsla: { 95 | props: { 96 | hue: { 97 | idx: 0, 98 | type: "degrees" 99 | }, 100 | saturation: { 101 | idx: 1, 102 | type: "percent" 103 | }, 104 | lightness: { 105 | idx: 2, 106 | type: "percent" 107 | } 108 | } 109 | } 110 | }, 111 | propTypes = { 112 | "byte": { 113 | floor: true, 114 | max: 255 115 | }, 116 | "percent": { 117 | max: 1 118 | }, 119 | "degrees": { 120 | mod: 360, 121 | floor: true 122 | } 123 | }, 124 | support = color.support = {}, 125 | 126 | // element for support tests 127 | supportElem = jQuery( "

" )[ 0 ], 128 | 129 | // colors = jQuery.Color.names 130 | colors, 131 | 132 | // local aliases of functions called often 133 | each = jQuery.each; 134 | 135 | // determine rgba support immediately 136 | supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; 137 | support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; 138 | 139 | // define cache name and alpha properties 140 | // for rgba and hsla spaces 141 | each( spaces, function( spaceName, space ) { 142 | space.cache = "_" + spaceName; 143 | space.props.alpha = { 144 | idx: 3, 145 | type: "percent", 146 | def: 1 147 | }; 148 | }); 149 | 150 | function clamp( value, prop, allowEmpty ) { 151 | var type = propTypes[ prop.type ] || {}; 152 | 153 | if ( value == null ) { 154 | return (allowEmpty || !prop.def) ? null : prop.def; 155 | } 156 | 157 | // ~~ is an short way of doing floor for positive numbers 158 | value = type.floor ? ~~value : parseFloat( value ); 159 | 160 | // IE will pass in empty strings as value for alpha, 161 | // which will hit this case 162 | if ( isNaN( value ) ) { 163 | return prop.def; 164 | } 165 | 166 | if ( type.mod ) { 167 | // we add mod before modding to make sure that negatives values 168 | // get converted properly: -10 -> 350 169 | return (value + type.mod) % type.mod; 170 | } 171 | 172 | // for now all property types without mod have min and max 173 | return 0 > value ? 0 : type.max < value ? type.max : value; 174 | } 175 | 176 | function stringParse( string ) { 177 | var inst = color(), 178 | rgba = inst._rgba = []; 179 | 180 | string = string.toLowerCase(); 181 | 182 | each( stringParsers, function( i, parser ) { 183 | var parsed, 184 | match = parser.re.exec( string ), 185 | values = match && parser.parse( match ), 186 | spaceName = parser.space || "rgba"; 187 | 188 | if ( values ) { 189 | parsed = inst[ spaceName ]( values ); 190 | 191 | // if this was an rgba parse the assignment might happen twice 192 | // oh well.... 193 | inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; 194 | rgba = inst._rgba = parsed._rgba; 195 | 196 | // exit each( stringParsers ) here because we matched 197 | return false; 198 | } 199 | }); 200 | 201 | // Found a stringParser that handled it 202 | if ( rgba.length ) { 203 | 204 | // if this came from a parsed string, force "transparent" when alpha is 0 205 | // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) 206 | if ( rgba.join() === "0,0,0,0" ) { 207 | jQuery.extend( rgba, colors.transparent ); 208 | } 209 | return inst; 210 | } 211 | 212 | // named colors 213 | return colors[ string ]; 214 | } 215 | 216 | color.fn = jQuery.extend( color.prototype, { 217 | parse: function( red, green, blue, alpha ) { 218 | if ( red === undefined ) { 219 | this._rgba = [ null, null, null, null ]; 220 | return this; 221 | } 222 | if ( red.jquery || red.nodeType ) { 223 | red = jQuery( red ).css( green ); 224 | green = undefined; 225 | } 226 | 227 | var inst = this, 228 | type = jQuery.type( red ), 229 | rgba = this._rgba = []; 230 | 231 | // more than 1 argument specified - assume ( red, green, blue, alpha ) 232 | if ( green !== undefined ) { 233 | red = [ red, green, blue, alpha ]; 234 | type = "array"; 235 | } 236 | 237 | if ( type === "string" ) { 238 | return this.parse( stringParse( red ) || colors._default ); 239 | } 240 | 241 | if ( type === "array" ) { 242 | each( spaces.rgba.props, function( key, prop ) { 243 | rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); 244 | }); 245 | return this; 246 | } 247 | 248 | if ( type === "object" ) { 249 | if ( red instanceof color ) { 250 | each( spaces, function( spaceName, space ) { 251 | if ( red[ space.cache ] ) { 252 | inst[ space.cache ] = red[ space.cache ].slice(); 253 | } 254 | }); 255 | } else { 256 | each( spaces, function( spaceName, space ) { 257 | var cache = space.cache; 258 | each( space.props, function( key, prop ) { 259 | 260 | // if the cache doesn't exist, and we know how to convert 261 | if ( !inst[ cache ] && space.to ) { 262 | 263 | // if the value was null, we don't need to copy it 264 | // if the key was alpha, we don't need to copy it either 265 | if ( key === "alpha" || red[ key ] == null ) { 266 | return; 267 | } 268 | inst[ cache ] = space.to( inst._rgba ); 269 | } 270 | 271 | // this is the only case where we allow nulls for ALL properties. 272 | // call clamp with alwaysAllowEmpty 273 | inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); 274 | }); 275 | 276 | // everything defined but alpha? 277 | if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { 278 | // use the default of 1 279 | inst[ cache ][ 3 ] = 1; 280 | if ( space.from ) { 281 | inst._rgba = space.from( inst[ cache ] ); 282 | } 283 | } 284 | }); 285 | } 286 | return this; 287 | } 288 | }, 289 | is: function( compare ) { 290 | var is = color( compare ), 291 | same = true, 292 | inst = this; 293 | 294 | each( spaces, function( _, space ) { 295 | var localCache, 296 | isCache = is[ space.cache ]; 297 | if (isCache) { 298 | localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || []; 299 | each( space.props, function( _, prop ) { 300 | if ( isCache[ prop.idx ] != null ) { 301 | same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); 302 | return same; 303 | } 304 | }); 305 | } 306 | return same; 307 | }); 308 | return same; 309 | }, 310 | _space: function() { 311 | var used = [], 312 | inst = this; 313 | each( spaces, function( spaceName, space ) { 314 | if ( inst[ space.cache ] ) { 315 | used.push( spaceName ); 316 | } 317 | }); 318 | return used.pop(); 319 | }, 320 | transition: function( other, distance ) { 321 | var end = color( other ), 322 | spaceName = end._space(), 323 | space = spaces[ spaceName ], 324 | startColor = this.alpha() === 0 ? color( "transparent" ) : this, 325 | start = startColor[ space.cache ] || space.to( startColor._rgba ), 326 | result = start.slice(); 327 | 328 | end = end[ space.cache ]; 329 | each( space.props, function( key, prop ) { 330 | var index = prop.idx, 331 | startValue = start[ index ], 332 | endValue = end[ index ], 333 | type = propTypes[ prop.type ] || {}; 334 | 335 | // if null, don't override start value 336 | if ( endValue === null ) { 337 | return; 338 | } 339 | // if null - use end 340 | if ( startValue === null ) { 341 | result[ index ] = endValue; 342 | } else { 343 | if ( type.mod ) { 344 | if ( endValue - startValue > type.mod / 2 ) { 345 | startValue += type.mod; 346 | } else if ( startValue - endValue > type.mod / 2 ) { 347 | startValue -= type.mod; 348 | } 349 | } 350 | result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); 351 | } 352 | }); 353 | return this[ spaceName ]( result ); 354 | }, 355 | blend: function( opaque ) { 356 | // if we are already opaque - return ourself 357 | if ( this._rgba[ 3 ] === 1 ) { 358 | return this; 359 | } 360 | 361 | var rgb = this._rgba.slice(), 362 | a = rgb.pop(), 363 | blend = color( opaque )._rgba; 364 | 365 | return color( jQuery.map( rgb, function( v, i ) { 366 | return ( 1 - a ) * blend[ i ] + a * v; 367 | })); 368 | }, 369 | toRgbaString: function() { 370 | var prefix = "rgba(", 371 | rgba = jQuery.map( this._rgba, function( v, i ) { 372 | return v == null ? ( i > 2 ? 1 : 0 ) : v; 373 | }); 374 | 375 | if ( rgba[ 3 ] === 1 ) { 376 | rgba.pop(); 377 | prefix = "rgb("; 378 | } 379 | 380 | return prefix + rgba.join() + ")"; 381 | }, 382 | toHslaString: function() { 383 | var prefix = "hsla(", 384 | hsla = jQuery.map( this.hsla(), function( v, i ) { 385 | if ( v == null ) { 386 | v = i > 2 ? 1 : 0; 387 | } 388 | 389 | // catch 1 and 2 390 | if ( i && i < 3 ) { 391 | v = Math.round( v * 100 ) + "%"; 392 | } 393 | return v; 394 | }); 395 | 396 | if ( hsla[ 3 ] === 1 ) { 397 | hsla.pop(); 398 | prefix = "hsl("; 399 | } 400 | return prefix + hsla.join() + ")"; 401 | }, 402 | toHexString: function( includeAlpha ) { 403 | var rgba = this._rgba.slice(), 404 | alpha = rgba.pop(); 405 | 406 | if ( includeAlpha ) { 407 | rgba.push( ~~( alpha * 255 ) ); 408 | } 409 | 410 | return "#" + jQuery.map( rgba, function( v ) { 411 | 412 | // default to 0 when nulls exist 413 | v = ( v || 0 ).toString( 16 ); 414 | return v.length === 1 ? "0" + v : v; 415 | }).join(""); 416 | }, 417 | toString: function() { 418 | return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); 419 | } 420 | }); 421 | color.fn.parse.prototype = color.fn; 422 | 423 | // hsla conversions adapted from: 424 | // https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 425 | 426 | function hue2rgb( p, q, h ) { 427 | h = ( h + 1 ) % 1; 428 | if ( h * 6 < 1 ) { 429 | return p + (q - p) * h * 6; 430 | } 431 | if ( h * 2 < 1) { 432 | return q; 433 | } 434 | if ( h * 3 < 2 ) { 435 | return p + (q - p) * ((2/3) - h) * 6; 436 | } 437 | return p; 438 | } 439 | 440 | spaces.hsla.to = function ( rgba ) { 441 | if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { 442 | return [ null, null, null, rgba[ 3 ] ]; 443 | } 444 | var r = rgba[ 0 ] / 255, 445 | g = rgba[ 1 ] / 255, 446 | b = rgba[ 2 ] / 255, 447 | a = rgba[ 3 ], 448 | max = Math.max( r, g, b ), 449 | min = Math.min( r, g, b ), 450 | diff = max - min, 451 | add = max + min, 452 | l = add * 0.5, 453 | h, s; 454 | 455 | if ( min === max ) { 456 | h = 0; 457 | } else if ( r === max ) { 458 | h = ( 60 * ( g - b ) / diff ) + 360; 459 | } else if ( g === max ) { 460 | h = ( 60 * ( b - r ) / diff ) + 120; 461 | } else { 462 | h = ( 60 * ( r - g ) / diff ) + 240; 463 | } 464 | 465 | // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% 466 | // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) 467 | if ( diff === 0 ) { 468 | s = 0; 469 | } else if ( l <= 0.5 ) { 470 | s = diff / add; 471 | } else { 472 | s = diff / ( 2 - add ); 473 | } 474 | return [ Math.round(h) % 360, s, l, a == null ? 1 : a ]; 475 | }; 476 | 477 | spaces.hsla.from = function ( hsla ) { 478 | if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { 479 | return [ null, null, null, hsla[ 3 ] ]; 480 | } 481 | var h = hsla[ 0 ] / 360, 482 | s = hsla[ 1 ], 483 | l = hsla[ 2 ], 484 | a = hsla[ 3 ], 485 | q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, 486 | p = 2 * l - q; 487 | 488 | return [ 489 | Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), 490 | Math.round( hue2rgb( p, q, h ) * 255 ), 491 | Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), 492 | a 493 | ]; 494 | }; 495 | 496 | 497 | each( spaces, function( spaceName, space ) { 498 | var props = space.props, 499 | cache = space.cache, 500 | to = space.to, 501 | from = space.from; 502 | 503 | // makes rgba() and hsla() 504 | color.fn[ spaceName ] = function( value ) { 505 | 506 | // generate a cache for this space if it doesn't exist 507 | if ( to && !this[ cache ] ) { 508 | this[ cache ] = to( this._rgba ); 509 | } 510 | if ( value === undefined ) { 511 | return this[ cache ].slice(); 512 | } 513 | 514 | var ret, 515 | type = jQuery.type( value ), 516 | arr = ( type === "array" || type === "object" ) ? value : arguments, 517 | local = this[ cache ].slice(); 518 | 519 | each( props, function( key, prop ) { 520 | var val = arr[ type === "object" ? key : prop.idx ]; 521 | if ( val == null ) { 522 | val = local[ prop.idx ]; 523 | } 524 | local[ prop.idx ] = clamp( val, prop ); 525 | }); 526 | 527 | if ( from ) { 528 | ret = color( from( local ) ); 529 | ret[ cache ] = local; 530 | return ret; 531 | } else { 532 | return color( local ); 533 | } 534 | }; 535 | 536 | // makes red() green() blue() alpha() hue() saturation() lightness() 537 | each( props, function( key, prop ) { 538 | // alpha is included in more than one space 539 | if ( color.fn[ key ] ) { 540 | return; 541 | } 542 | color.fn[ key ] = function( value ) { 543 | var vtype = jQuery.type( value ), 544 | fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ), 545 | local = this[ fn ](), 546 | cur = local[ prop.idx ], 547 | match; 548 | 549 | if ( vtype === "undefined" ) { 550 | return cur; 551 | } 552 | 553 | if ( vtype === "function" ) { 554 | value = value.call( this, cur ); 555 | vtype = jQuery.type( value ); 556 | } 557 | if ( value == null && prop.empty ) { 558 | return this; 559 | } 560 | if ( vtype === "string" ) { 561 | match = rplusequals.exec( value ); 562 | if ( match ) { 563 | value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); 564 | } 565 | } 566 | local[ prop.idx ] = value; 567 | return this[ fn ]( local ); 568 | }; 569 | }); 570 | }); 571 | 572 | // add cssHook and .fx.step function for each named hook. 573 | // accept a space separated string of properties 574 | color.hook = function( hook ) { 575 | var hooks = hook.split( " " ); 576 | each( hooks, function( i, hook ) { 577 | jQuery.cssHooks[ hook ] = { 578 | set: function( elem, value ) { 579 | var parsed, curElem, 580 | backgroundColor = ""; 581 | 582 | if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { 583 | value = color( parsed || value ); 584 | if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { 585 | curElem = hook === "backgroundColor" ? elem.parentNode : elem; 586 | while ( 587 | (backgroundColor === "" || backgroundColor === "transparent") && 588 | curElem && curElem.style 589 | ) { 590 | try { 591 | backgroundColor = jQuery.css( curElem, "backgroundColor" ); 592 | curElem = curElem.parentNode; 593 | } catch ( e ) { 594 | } 595 | } 596 | 597 | value = value.blend( backgroundColor && backgroundColor !== "transparent" ? 598 | backgroundColor : 599 | "_default" ); 600 | } 601 | 602 | value = value.toRgbaString(); 603 | } 604 | try { 605 | elem.style[ hook ] = value; 606 | } catch( e ) { 607 | // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' 608 | } 609 | } 610 | }; 611 | jQuery.fx.step[ hook ] = function( fx ) { 612 | if ( !fx.colorInit ) { 613 | fx.start = color( fx.elem, hook ); 614 | fx.end = color( fx.end ); 615 | fx.colorInit = true; 616 | } 617 | jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); 618 | }; 619 | }); 620 | 621 | }; 622 | 623 | color.hook( stepHooks ); 624 | 625 | jQuery.cssHooks.borderColor = { 626 | expand: function( value ) { 627 | var expanded = {}; 628 | 629 | each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) { 630 | expanded[ "border" + part + "Color" ] = value; 631 | }); 632 | return expanded; 633 | } 634 | }; 635 | 636 | // Basic color names only. 637 | // Usage of any of the other color names requires adding yourself or including 638 | // jquery.color.svg-names.js. 639 | colors = jQuery.Color.names = { 640 | // 4.1. Basic color keywords 641 | aqua: "#00ffff", 642 | black: "#000000", 643 | blue: "#0000ff", 644 | fuchsia: "#ff00ff", 645 | gray: "#808080", 646 | green: "#008000", 647 | lime: "#00ff00", 648 | maroon: "#800000", 649 | navy: "#000080", 650 | olive: "#808000", 651 | purple: "#800080", 652 | red: "#ff0000", 653 | silver: "#c0c0c0", 654 | teal: "#008080", 655 | white: "#ffffff", 656 | yellow: "#ffff00", 657 | 658 | // 4.2.3. "transparent" color keyword 659 | transparent: [ null, null, null, 0 ], 660 | 661 | _default: "#ffffff" 662 | }; 663 | 664 | })( jQuery ); 665 | 666 | /*! 667 | * jQuery Color Animations v2.1.2 - SVG Color Names 668 | * https://github.com/jquery/jquery-color 669 | * 670 | * Remaining HTML/CSS color names per W3C's CSS Color Module Level 3. 671 | * http://www.w3.org/TR/css3-color/#svg-color 672 | * 673 | * Copyright 2013 jQuery Foundation and other contributors 674 | * Released under the MIT license. 675 | * http://jquery.org/license 676 | * 677 | * Date: Wed Jan 16 08:47:09 2013 -0600 678 | */ 679 | jQuery.extend( jQuery.Color.names, { 680 | // 4.3. Extended color keywords (minus the basic ones in core color plugin) 681 | aliceblue: "#f0f8ff", 682 | antiquewhite: "#faebd7", 683 | aquamarine: "#7fffd4", 684 | azure: "#f0ffff", 685 | beige: "#f5f5dc", 686 | bisque: "#ffe4c4", 687 | blanchedalmond: "#ffebcd", 688 | blueviolet: "#8a2be2", 689 | brown: "#a52a2a", 690 | burlywood: "#deb887", 691 | cadetblue: "#5f9ea0", 692 | chartreuse: "#7fff00", 693 | chocolate: "#d2691e", 694 | coral: "#ff7f50", 695 | cornflowerblue: "#6495ed", 696 | cornsilk: "#fff8dc", 697 | crimson: "#dc143c", 698 | cyan: "#00ffff", 699 | darkblue: "#00008b", 700 | darkcyan: "#008b8b", 701 | darkgoldenrod: "#b8860b", 702 | darkgray: "#a9a9a9", 703 | darkgreen: "#006400", 704 | darkgrey: "#a9a9a9", 705 | darkkhaki: "#bdb76b", 706 | darkmagenta: "#8b008b", 707 | darkolivegreen: "#556b2f", 708 | darkorange: "#ff8c00", 709 | darkorchid: "#9932cc", 710 | darkred: "#8b0000", 711 | darksalmon: "#e9967a", 712 | darkseagreen: "#8fbc8f", 713 | darkslateblue: "#483d8b", 714 | darkslategray: "#2f4f4f", 715 | darkslategrey: "#2f4f4f", 716 | darkturquoise: "#00ced1", 717 | darkviolet: "#9400d3", 718 | deeppink: "#ff1493", 719 | deepskyblue: "#00bfff", 720 | dimgray: "#696969", 721 | dimgrey: "#696969", 722 | dodgerblue: "#1e90ff", 723 | firebrick: "#b22222", 724 | floralwhite: "#fffaf0", 725 | forestgreen: "#228b22", 726 | gainsboro: "#dcdcdc", 727 | ghostwhite: "#f8f8ff", 728 | gold: "#ffd700", 729 | goldenrod: "#daa520", 730 | greenyellow: "#adff2f", 731 | grey: "#808080", 732 | honeydew: "#f0fff0", 733 | hotpink: "#ff69b4", 734 | indianred: "#cd5c5c", 735 | indigo: "#4b0082", 736 | ivory: "#fffff0", 737 | khaki: "#f0e68c", 738 | lavender: "#e6e6fa", 739 | lavenderblush: "#fff0f5", 740 | lawngreen: "#7cfc00", 741 | lemonchiffon: "#fffacd", 742 | lightblue: "#add8e6", 743 | lightcoral: "#f08080", 744 | lightcyan: "#e0ffff", 745 | lightgoldenrodyellow: "#fafad2", 746 | lightgray: "#d3d3d3", 747 | lightgreen: "#90ee90", 748 | lightgrey: "#d3d3d3", 749 | lightpink: "#ffb6c1", 750 | lightsalmon: "#ffa07a", 751 | lightseagreen: "#20b2aa", 752 | lightskyblue: "#87cefa", 753 | lightslategray: "#778899", 754 | lightslategrey: "#778899", 755 | lightsteelblue: "#b0c4de", 756 | lightyellow: "#ffffe0", 757 | limegreen: "#32cd32", 758 | linen: "#faf0e6", 759 | mediumaquamarine: "#66cdaa", 760 | mediumblue: "#0000cd", 761 | mediumorchid: "#ba55d3", 762 | mediumpurple: "#9370db", 763 | mediumseagreen: "#3cb371", 764 | mediumslateblue: "#7b68ee", 765 | mediumspringgreen: "#00fa9a", 766 | mediumturquoise: "#48d1cc", 767 | mediumvioletred: "#c71585", 768 | midnightblue: "#191970", 769 | mintcream: "#f5fffa", 770 | mistyrose: "#ffe4e1", 771 | moccasin: "#ffe4b5", 772 | navajowhite: "#ffdead", 773 | oldlace: "#fdf5e6", 774 | olivedrab: "#6b8e23", 775 | orange: "#ffa500", 776 | orangered: "#ff4500", 777 | orchid: "#da70d6", 778 | palegoldenrod: "#eee8aa", 779 | palegreen: "#98fb98", 780 | paleturquoise: "#afeeee", 781 | palevioletred: "#db7093", 782 | papayawhip: "#ffefd5", 783 | peachpuff: "#ffdab9", 784 | peru: "#cd853f", 785 | pink: "#ffc0cb", 786 | plum: "#dda0dd", 787 | powderblue: "#b0e0e6", 788 | rosybrown: "#bc8f8f", 789 | royalblue: "#4169e1", 790 | saddlebrown: "#8b4513", 791 | salmon: "#fa8072", 792 | sandybrown: "#f4a460", 793 | seagreen: "#2e8b57", 794 | seashell: "#fff5ee", 795 | sienna: "#a0522d", 796 | skyblue: "#87ceeb", 797 | slateblue: "#6a5acd", 798 | slategray: "#708090", 799 | slategrey: "#708090", 800 | snow: "#fffafa", 801 | springgreen: "#00ff7f", 802 | steelblue: "#4682b4", 803 | tan: "#d2b48c", 804 | thistle: "#d8bfd8", 805 | tomato: "#ff6347", 806 | turquoise: "#40e0d0", 807 | violet: "#ee82ee", 808 | wheat: "#f5deb3", 809 | whitesmoke: "#f5f5f5", 810 | yellowgreen: "#9acd32" 811 | }); 812 | } 813 | 814 | export default jQueryColor; 815 | -------------------------------------------------------------------------------- /web/src/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/launcher.png -------------------------------------------------------------------------------- /web/src/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mixcoin 7 | 8 | 9 |

10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /web/src/layout.scss: -------------------------------------------------------------------------------- 1 | @import './normalize.css'; 2 | @import './animate.scss'; 3 | @import './fonts/index.scss'; 4 | @import './constant.scss'; 5 | @import '../node_modules/noty/src/noty.scss'; 6 | @import '../node_modules/noty/src/themes/nest.scss'; 7 | 8 | html, 9 | body { 10 | font-family: $font-main-content; 11 | color: $color-main-foreground-dark; 12 | margin: 0px; 13 | padding: 0px; 14 | } 15 | 16 | html { 17 | height: 100%; 18 | } 19 | 20 | a { 21 | text-decoration: none; 22 | } 23 | 24 | input[type='number'] { 25 | -moz-appearance:textfield; 26 | } 27 | 28 | input::-webkit-outer-spin-button, 29 | input::-webkit-inner-spin-button { 30 | -webkit-appearance: none; 31 | } 32 | 33 | .loading.layout { 34 | height: 100%; 35 | cursor: wait; 36 | 37 | #layout-container { 38 | height: 100%; 39 | position: relative; 40 | } 41 | } 42 | 43 | .error.layout { 44 | height: 100%; 45 | 46 | #layout-container { 47 | height: 100%; 48 | position: relative; 49 | } 50 | 51 | .error.content { 52 | position: absolute; 53 | left: 50%; 54 | top: 50%; 55 | transform: translate(-50%, -50%); 56 | font-family: $font-main-title; 57 | text-align: center; 58 | 59 | h1 { 60 | font-size: 64px; 61 | letter-spacing: 8px; 62 | } 63 | } 64 | } 65 | 66 | .spinner-container { 67 | position: relative; 68 | width: 100%; 69 | height: 100%; 70 | 71 | .spinner { 72 | position: relative; 73 | text-align: center; 74 | width: 60px; 75 | height: 40px; 76 | top: 50%; 77 | left: 50%; 78 | transform: translate(-50%, -50%); 79 | font-size: 10px; 80 | white-space: nowrap; 81 | } 82 | 83 | .spinner > div { 84 | background-color: $color-main-highlight; 85 | height: 100%; 86 | width: 6px; 87 | display: inline-block; 88 | 89 | animation: sk-stretchdelay 1.2s infinite ease-in-out; 90 | } 91 | 92 | .spinner .rect2 { 93 | animation-delay: -1.1s; 94 | } 95 | 96 | .spinner .rect3 { 97 | animation-delay: -1.0s; 98 | } 99 | 100 | .spinner .rect4 { 101 | animation-delay: -0.9s; 102 | } 103 | 104 | .spinner .rect5 { 105 | animation-delay: -0.8s; 106 | } 107 | 108 | @keyframes sk-stretchdelay { 109 | 0%, 40%, 100% { 110 | transform: scaleY(0.4); 111 | } 20% { 112 | transform: scaleY(1.0); 113 | } 114 | } 115 | } 116 | 117 | #noty_layout__top { 118 | left: 50%; 119 | top: 8px; 120 | transform: translateX(-50%); 121 | width: 90%; 122 | max-width: 500px; 123 | 124 | .noty_theme__nest.noty_bar .noty_body { 125 | padding: 16px; 126 | } 127 | 128 | .noty_has_timeout .noty_progressbar{ 129 | background-color: transparent; 130 | } 131 | 132 | .noty_type__success { 133 | background-color: #00B379; 134 | } 135 | 136 | .noty_type__warning { 137 | background-color: #FF884D; 138 | } 139 | } 140 | 141 | .animated.slideOutUp.noty { 142 | animation-duration: 0.2s; 143 | } 144 | -------------------------------------------------------------------------------- /web/src/loading.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /web/src/locale/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Mixcoin", 3 | "action": { 4 | "ok": "OK", 5 | "submit": "Submit" 6 | }, 7 | "general": { 8 | "errors": { 9 | "0": "Network error.", 10 | "400": "Invalid data.", 11 | "404": "Not found.", 12 | "401": "Unauthorized, maybe invalid email or password.", 13 | "403": "Access denied.", 14 | "500": "Internal server error.", 15 | "10001": "Internal server error.", 16 | "10002": "Invalid data.", 17 | "asset_access_denied": "Mixcoin need access to your assets, please reauthorize." 18 | } 19 | }, 20 | "orders": { 21 | "title": "My Orders", 22 | "state.pending": "Pending", 23 | "state.history": "History", 24 | "type.limit": "Limit", 25 | "type.market": "Market", 26 | "cancel": "CANCEL", 27 | "table.pair": "Pair", 28 | "table.type": "Type", 29 | "table.price": "Price", 30 | "table.amount": "Amount", 31 | "table.total": "Total", 32 | "table.volume": "Volume", 33 | "table.time": "Time", 34 | "table.operation": "Oper", 35 | "guide": "Enter the transaction id to cancel order:", 36 | "invalid.transaction.id": "Invalid transaction id", 37 | "cancel.success": "Order canceled successfully", 38 | "insufficient.balance": "Please keep some OOO or CNB or NXC or CANDY token to cancel the order." 39 | }, 40 | "home": { 41 | "headline": "An open source decentralized exchange to trade all digital assets with your wallet.", 42 | "sign.up": "Sign Up", 43 | "log.in": "Log In", 44 | "link.api": "API Reference", 45 | "link.fees": "Fees", 46 | "search.markets": "Search %{market} markets", 47 | "table.name": "Name", 48 | "table.price": "Price", 49 | "table.change": "24h Change", 50 | "table.volume": "24h volume" 51 | }, 52 | "market": { 53 | "ticker.change": "24h Change", 54 | "ticker.volume": "24h Volume", 55 | "ticker.total": "24h Total", 56 | "chart.depth": "Depth", 57 | "table.book": "Book", 58 | "table.history": "History", 59 | "form.limit": "Limit", 60 | "form.market": "Market", 61 | "form.buy": "Buy", 62 | "form.sell": "Sell", 63 | "form.price": "Price", 64 | "form.amount": "Amount", 65 | "form.button.buy": "Buy %{symbol}", 66 | "form.button.sell": "Sell %{symbol}", 67 | "account.wallet": "Wallet", 68 | "account.orders": "My Orders", 69 | 70 | "errors": { 71 | "price.max": "Price must be equal to or less than %{price} %{symbol}", 72 | "price.min": "Price must be equal to or greater than %{price} %{symbol}", 73 | "fund.max": "Total must be equal to or less than %{fund} %{symbol}", 74 | "fund.min": "Total must be equal to or greater than %{fund} %{symbol}", 75 | "amount.max": "Amount must be equal to or less than %{amount} %{symbol}", 76 | "amount.min": "Amount must be be equal to or greater than %{amount} %{symbol}" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /web/src/locale/index.js: -------------------------------------------------------------------------------- 1 | import Polyglot from 'node-polyglot'; 2 | 3 | function Locale(lang) { 4 | var locale = 'en-US'; 5 | if (lang && lang.indexOf('zh') >= 0) { 6 | locale = 'zh-Hans'; 7 | } 8 | this.polyglot = new Polyglot({locale: locale}); 9 | this.polyglot.extend(require('./' + locale + '.json')); 10 | } 11 | 12 | Locale.prototype = { 13 | t: function(key, options) { 14 | return this.polyglot.t(key, options); 15 | } 16 | }; 17 | 18 | export default Locale; 19 | -------------------------------------------------------------------------------- /web/src/locale/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Mixcoin 交易所", 3 | "action": { 4 | "ok": "好的", 5 | "submit": "提交" 6 | }, 7 | "general": { 8 | "errors": { 9 | "0": "网络错误。", 10 | "400": "数据无效。", 11 | "404": "未找到。", 12 | "401": "未经授权的访问。", 13 | "403": "拒绝访问。", 14 | "500": "内部服务器错误。", 15 | "10001": "内部服务器错误。", 16 | "10002": "数据无效。", 17 | "asset_access_denied": "Mixcoin 需要访问你的资产显示交易对,请重新授权。" 18 | } 19 | }, 20 | "orders": { 21 | "title": "我的挂单", 22 | "state.pending": "执行中订单", 23 | "state.history": "历史订单", 24 | "type.limit": "限价单", 25 | "type.market": "市价单", 26 | "cancel": "取消", 27 | "table.pair": "交易对", 28 | "table.type": "类型", 29 | "table.price": "价格", 30 | "table.amount": "数量", 31 | "table.total": "总量", 32 | "table.volume": "量", 33 | "table.time": "时间", 34 | "table.operation": "操作", 35 | "guide": "输入交易编号取消订单:", 36 | "invalid.transaction.id": "无效的交易编号", 37 | "cancel.success": "订单取消成功", 38 | "insufficient.balance": "请保留少量的 OOO、CNB、NXC 或 CANDY 代币用于取消订单" 39 | }, 40 | "home": { 41 | "headline": "一个开源的分布式交易所,用于交易所有数字资产。", 42 | "sign.up": "注册", 43 | "log.in": "登录", 44 | "link.api": "API 文档", 45 | "link.fees": "手续费", 46 | "search.markets": "搜索 %{market} 市场", 47 | "table.name": "名字", 48 | "table.price": "价格", 49 | "table.change": "24 时涨跌", 50 | "table.volume": "24 时成交量" 51 | }, 52 | "market": { 53 | "ticker.change": "24 时涨跌", 54 | "ticker.volume": "24 时成交量", 55 | "ticker.total": "24 时成交额", 56 | "chart.depth": "深度图", 57 | "table.book": "挂单簙", 58 | "table.history": "成交记录", 59 | "form.limit": "限价单", 60 | "form.market": "市价单", 61 | "form.buy": "买入", 62 | "form.sell": "卖出", 63 | "form.price": "价格", 64 | "form.amount": "数量", 65 | "form.button.buy": "买入 %{symbol}", 66 | "form.button.sell": "卖出 %{symbol}", 67 | "account.wallet": "钱包", 68 | "account.orders": "我的挂单", 69 | "success": "我的挂单", 70 | 71 | "errors": { 72 | "price.max": "价格不能大于 %{price} %{symbol}", 73 | "price.min": "价格不能小于 %{price} %{symbol}", 74 | "fund.max": "挂单总价不能大于 %{fund} %{symbol}", 75 | "fund.min": "挂单总价不能小于 %{fund} %{symbol}", 76 | "amount.max": "数量不能大于 %{amount} %{symbol}", 77 | "amount.min": "数量不能小于 %{amount} %{symbol}" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /web/src/market/chart.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import 'moment-timezone'; 3 | import Highcharts from 'highcharts/highstock'; 4 | import Indicators from 'highcharts/indicators/indicators.js'; 5 | import Ema from 'highcharts/indicators/ema.js'; 6 | import $ from 'jquery'; 7 | import {BigNumber} from 'bignumber.js'; 8 | Indicators(Highcharts); 9 | Ema(Highcharts); 10 | Highcharts.win.moment = moment; 11 | 12 | function Chart() { 13 | Highcharts.setOptions({ 14 | time: { 15 | timezone: Intl.DateTimeFormat().resolvedOptions().timeZone 16 | } 17 | }); 18 | } 19 | 20 | Chart.prototype = { 21 | prepareCandleData: function (data) { 22 | var ohlc = [], 23 | volume = [], 24 | dataLength = data.length; 25 | 26 | for (var i = 0; i < dataLength; i += 1) { 27 | ohlc.push([ 28 | data[i][0] * 1000, // the date 29 | data[i][3], // open 30 | data[i][2], // high 31 | data[i][1], // low 32 | data[i][4] // close 33 | ]); 34 | 35 | volume.push([ 36 | parseFloat(new BigNumber(data[i][0]).times(1000).toFixed(8)), // the date 37 | parseFloat(new BigNumber(data[i][5]).toFixed(8)) // the volume 38 | ]); 39 | } 40 | 41 | return [ohlc, volume]; 42 | }, 43 | 44 | renderPrice: function (ele, currency, data) { 45 | var groupingUnits = [ 46 | ['minute', [1, 5, 15, 30]], 47 | ['hour', [1, 6, 12, 24]] 48 | ]; 49 | 50 | data = this.prepareCandleData(data); 51 | var ohlc = data[0]; 52 | var volume = data[1]; 53 | 54 | var chart = Highcharts.StockChart(ele, { 55 | chart: { 56 | zoomType: 'none', 57 | pinchType: 'none', 58 | panning: false, 59 | spacing: [0, 0, 0, 0] 60 | }, 61 | 62 | credits: { 63 | enabled: false 64 | }, 65 | 66 | rangeSelector: { 67 | enabled: false 68 | }, 69 | 70 | scrollbar: { 71 | enabled: false 72 | }, 73 | 74 | navigator: { 75 | enabled: false 76 | }, 77 | 78 | legend: { 79 | enabled: false 80 | }, 81 | 82 | plotOptions: { 83 | series: { 84 | stickyTracking: false, 85 | showInLegend: false 86 | } 87 | }, 88 | 89 | yAxis: [{ 90 | labels: { 91 | align: 'right', 92 | x: -3, 93 | formatter: function () { 94 | return new BigNumber(this.value).toString(10); 95 | } 96 | }, 97 | height: '70%', 98 | gridLineWidth: 0.5, 99 | lineWidth: 0 100 | }, { 101 | labels: { 102 | align: 'right', 103 | x: -3 104 | }, 105 | top: '71%', 106 | height: '29%', 107 | offset: 0, 108 | gridLineWidth: 0.5, 109 | lineWidth: 0 110 | }], 111 | 112 | tooltip: { 113 | followPointer: true, 114 | followTouchMove: false, 115 | split: true 116 | }, 117 | 118 | series: [{ 119 | type: 'column', 120 | name: 'Volume', 121 | data: volume, 122 | yAxis: 1, 123 | dataGrouping: { 124 | units: groupingUnits 125 | }, 126 | color: 'rgba(41,149,242,0.3)' 127 | }, { 128 | type: 'candlestick', 129 | id: 'candle', 130 | name: currency, 131 | data: ohlc, 132 | dataGrouping: { 133 | units: groupingUnits 134 | } 135 | }, { 136 | type: 'ema', 137 | linkedTo: 'candle', 138 | params: { 139 | period: 12 140 | }, 141 | color: 'rgba(255,155,100,0.5)', 142 | lineWidth: 1 143 | }, { 144 | type: 'ema', 145 | linkedTo: 'candle', 146 | params: { 147 | period: 26 148 | }, 149 | color: 'rgba(100,155,255,0.5)', 150 | lineWidth: 1 151 | }] 152 | }); 153 | 154 | return chart; 155 | }, 156 | 157 | renderDepth: function (ele, bids, asks, depth) { 158 | if (bids.length === 0 || asks.length === 0) { 159 | return undefined; 160 | } 161 | 162 | var bidsData = []; 163 | for(var i = 0; i < bids.length; i++) { 164 | bids[i].volume = parseFloat(bids[i].amount); 165 | if (i > 0) { 166 | bids[i].volume = parseFloat(new BigNumber(bids[i-1].volume).plus(bids[i].volume).toFixed(8)); 167 | } 168 | bidsData.push({ 169 | x: parseFloat(bids[i].price), 170 | y: bids[i].volume 171 | }); 172 | } 173 | var start = bidsData.length * 1 / 4 + bidsData.length * depth / 2; 174 | if (bidsData.length > 1000) { 175 | start = bidsData.length - 1000 + 1000 * 1 / 4 + 1000 * depth / 2; 176 | } 177 | bidsData = bidsData.reverse(); 178 | bidsData = bidsData.splice(start); 179 | 180 | var asksInput = []; 181 | for(var i = 0; i < asks.length; i++) { 182 | asks[i].volume = parseFloat(parseFloat(asks[i].amount).toFixed(4)); 183 | if (i > 0) { 184 | asks[i].volume = parseFloat(new BigNumber(asks[i-1].volume).plus(asks[i].volume).toFixed(4)); 185 | } 186 | asksInput.push({ 187 | x: parseFloat(asks[i].price), 188 | y: asks[i].volume 189 | }); 190 | } 191 | var asksData = []; 192 | var priceThreshold = bidsData[bidsData.length - 1].x + asksInput[0].x - bidsData[0].x; 193 | for (var i = 0; i < asksInput.length; i++) { 194 | var point = asksInput[i]; 195 | if (point.x > priceThreshold && asksData.length > 10) { 196 | break; 197 | } 198 | asksData.push(point); 199 | } 200 | 201 | if (asksData.length > 1000) { 202 | asksData = asksData.slice(0, 1000); 203 | } 204 | var minPrice = bidsData[0].x; 205 | var maxPrice = asksData[asksData.length-1].x; 206 | var maxVolume = bidsData[0].y; 207 | if (asksData[asksData.length-1].y > maxVolume) { 208 | maxVolume = asksData[asksData.length-1].y; 209 | } 210 | 211 | var chart = Highcharts.chart(ele, { 212 | chart: { 213 | zoomType: 'none', 214 | pinchType: 'none', 215 | panning: false, 216 | spacing: [0, 0, 0, 0] 217 | }, 218 | 219 | credits: { 220 | enabled: false 221 | }, 222 | 223 | rangeSelector: { 224 | enabled: false 225 | }, 226 | 227 | scrollbar: { 228 | enabled: false 229 | }, 230 | 231 | navigator: { 232 | enabled: false 233 | }, 234 | 235 | legend: { 236 | enabled: false 237 | }, 238 | 239 | title: { 240 | text: null 241 | }, 242 | 243 | xAxis: { 244 | gridLineWidth: 0.5, 245 | min: minPrice, 246 | max: maxPrice, 247 | labels: { 248 | formatter: function () { 249 | return new BigNumber(this.value).toString(10); 250 | } 251 | } 252 | }, 253 | 254 | yAxis: { 255 | opposite: true, 256 | labels: { 257 | align: 'right', 258 | x: -3, 259 | y: -2, 260 | formatter: function () { 261 | return new BigNumber(this.value).toString(10); 262 | } 263 | }, 264 | lineWidth: 0, 265 | resize: { 266 | enabled: true 267 | }, 268 | gridLineWidth: 0.5, 269 | max: maxVolume, 270 | min: 0, 271 | title: { 272 | text: '', 273 | }, 274 | }, 275 | 276 | plotOptions: { 277 | series: { 278 | stickyTracking: false, 279 | animation: false, 280 | marker: { 281 | enabled: false, 282 | symbol: 'circle', 283 | states: { 284 | hover: { 285 | enabled: true 286 | } 287 | } 288 | } 289 | } 290 | }, 291 | tooltip: { 292 | followPointer: true, 293 | followTouchMove: false, 294 | crosshairs: [true, true], 295 | formatter: function () { 296 | return 'Price ' + this.x + '
' + this.series.name + '' + this.y + ''; 297 | } 298 | }, 299 | series: [ 300 | { 301 | type: 'area', 302 | name: 'Buy orders', 303 | data: bidsData, 304 | color: 'rgba(1,170,120,1.0)', 305 | fillColor: 'rgba(1,170,120,0.2)' 306 | }, 307 | { 308 | type: 'area', 309 | name: 'Sell orders', 310 | data: asksData, 311 | color: 'rgba(255,95,115,1.0)', 312 | fillColor: 'rgba(255,95,115,0.2)' 313 | } 314 | ] 315 | }); 316 | 317 | return chart; 318 | } 319 | }; 320 | 321 | export default Chart; 322 | -------------------------------------------------------------------------------- /web/src/market/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 | 38 | -------------------------------------------------------------------------------- /web/src/market/index.scss: -------------------------------------------------------------------------------- 1 | @import '../constant.scss'; 2 | 3 | .layout.nav { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | width: 100%; 8 | z-index: 99; 9 | 10 | .overlay { 11 | background: rgba(0,18,83,1); 12 | padding: 8px 16px; 13 | 14 | .logo { 15 | line-height: 48px; 16 | margin: 0; 17 | 18 | img { 19 | width: 64px; 20 | height: 30px; 21 | vertical-align: middle; 22 | padding-bottom: 8px; 23 | } 24 | } 25 | 26 | .title { 27 | margin: 0; 28 | line-height: 64px; 29 | display: inline-block; 30 | color: $color-main-background; 31 | position: absolute; 32 | left: 50%; 33 | top: 0; 34 | transform: translateX(-50%); 35 | font-size: 24px; 36 | font-weight: 300; 37 | cursor: pointer; 38 | 39 | .arrow { 40 | font-size: 12px; 41 | float: right; 42 | line-height: 64px; 43 | } 44 | } 45 | 46 | .account { 47 | color: $color-main-background; 48 | font-size: 24px; 49 | line-height: 64px; 50 | display: inline-block; 51 | position: absolute; 52 | top: 0; 53 | right: 16px; 54 | 55 | &.in { 56 | display: none; 57 | } 58 | } 59 | } 60 | 61 | .panel { 62 | width: 100%; 63 | height: 50px; 64 | background-color: #ffffff; 65 | display: none; 66 | 67 | &:after { 68 | content: ""; 69 | display: block; 70 | clear: both; 71 | } 72 | 73 | .tab { 74 | background-color: #ffffff; 75 | padding: 15px 14px; 76 | float: left; 77 | 78 | &.active { 79 | border-bottom: 2px solid $color-main-highlight; 80 | } 81 | } 82 | } 83 | } 84 | 85 | .layout.markets.container { 86 | width: 100%; 87 | box-sizing: border-box; 88 | background: $color-main-background; 89 | margin-top: 114px; 90 | display: none; 91 | 92 | .markets { 93 | width: 100%; 94 | display: none; 95 | 96 | .market.item { 97 | background: #ffffff; 98 | position: relative; 99 | border-bottom: 1px solid #f4f4f4; 100 | list-style: none; 101 | height: 48px; 102 | 103 | .asset-icon { 104 | position: absolute; 105 | top: 10px; 106 | left: 12px; 107 | width: 28px; 108 | height: 28px; 109 | vertical-align: middle; 110 | } 111 | 112 | .chain-icon { 113 | position: absolute; 114 | top: 30px; 115 | left: 12px; 116 | width: 8px; 117 | height: 8px; 118 | vertical-align: middle; 119 | } 120 | 121 | .name { 122 | position: absolute; 123 | top: 10px; 124 | left: 54px; 125 | color: #000000; 126 | letter-spacing: 1px; 127 | line-height: 1; 128 | } 129 | 130 | .price { 131 | position: absolute; 132 | bottom: 10px; 133 | left: 54px; 134 | color: #cccccc; 135 | font-size: 9px; 136 | } 137 | 138 | .favor { 139 | position: absolute; 140 | top: 50%; 141 | right: 18px; 142 | -webkit-transform: translateY(-50%); 143 | transform: translateY(-50%); 144 | 145 | &.active { 146 | color: $color-main-highlight; 147 | } 148 | } 149 | 150 | .change.down { 151 | position: absolute; 152 | top: 11px; 153 | right: 56px; 154 | font-size: 10px; 155 | font-weight: 300; 156 | color: #E55541; 157 | } 158 | 159 | .change.up { 160 | position: absolute; 161 | top: 11px; 162 | right: 56px; 163 | font-size: 10px; 164 | font-weight: 300; 165 | color: #00B56E; 166 | } 167 | 168 | .volume { 169 | position: absolute; 170 | right: 56px; 171 | bottom: 10px; 172 | color: #ccc; 173 | font-size: 9px; 174 | text-align: right; 175 | } 176 | } 177 | } 178 | 179 | } 180 | 181 | .layout.trade { 182 | position: fixed; 183 | width: 100%; 184 | height: 100%; 185 | background: $color-main-background; 186 | top: 64px; 187 | } 188 | 189 | .modal-container { 190 | background: rgba(0, 0, 0, 0.3); 191 | display: none; 192 | position: fixed; 193 | z-index: 999; 194 | top: 0; 195 | left: 0; 196 | width: 100%; 197 | height: 100%; 198 | 199 | img { 200 | border-radius: 1.5rem; 201 | width: 3rem; 202 | height: 3rem; 203 | margin-bottom: 1rem; 204 | } 205 | .modal-body { 206 | box-sizing: border-box; 207 | background: white; 208 | text-align: left; 209 | margin: 18rem auto 0; 210 | padding: 1rem; 211 | width: 80%; 212 | border-radius: 8px; 213 | } 214 | .header { 215 | text-align: right; 216 | font-size: 1.6rem; 217 | cursor: pointer; 218 | margin-bottom: .6rem; 219 | } 220 | .modal-name { 221 | font-size: 20px; 222 | margin-top: 0.5rem; 223 | margin-bottom: 1.5rem; 224 | margin-left: 10px; 225 | } 226 | .actions { 227 | margin-bottom: 0.5rem; 228 | text-align: right; 229 | } 230 | a.btn { 231 | margin-top: 1rem; 232 | margin-right: 10px; 233 | font-size: 18px; 234 | font-weight: 300; 235 | color: #007AFF; 236 | width: 50%; 237 | border-radius: 6px; 238 | &:hover { 239 | background-color: #3cb1d4; 240 | } 241 | } 242 | } -------------------------------------------------------------------------------- /web/src/market/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/market/logo.png -------------------------------------------------------------------------------- /web/src/market/market.js: -------------------------------------------------------------------------------- 1 | import TimeUtils from '../utils/time.js'; 2 | import {BigNumber} from 'bignumber.js'; 3 | 4 | function MarketController(api, db) { 5 | this.api = api; 6 | this.db = db; 7 | } 8 | 9 | MarketController.prototype = { 10 | 11 | getTimestamp: function(created_at) { 12 | const date = new Date(created_at); 13 | return parseInt((TimeUtils.getUTCDate(date).getTime() / 1000).toFixed(0)); 14 | }, 15 | 16 | processCandles: function (callback, baseAssetId, quoteAssetId, granularity) { 17 | const self = this; 18 | this.db.trade.fetchTrades(function (trades) { 19 | if (trades.length == 0) { 20 | callback([]); 21 | return; 22 | } 23 | trades = trades.reverse(); 24 | var timestamp = new BigNumber(parseInt((new Date().getTime() / 1000).toFixed(0))); 25 | timestamp = timestamp.minus(new BigNumber(granularity).times(60)); 26 | var candles = []; 27 | var tradeIdx = 0; 28 | 29 | var tradeTimestamp = new BigNumber(self.getTimestamp(trades[tradeIdx].created_at)); 30 | var price = Number(trades[tradeIdx].price); 31 | var firstOrder = false 32 | 33 | for (var i = 0; i < 60; i++) { 34 | if (tradeTimestamp.gt(timestamp.minus(granularity)) && tradeTimestamp.lte(timestamp) && tradeIdx < trades.length) { 35 | firstOrder = true 36 | var open = price; 37 | var close = price; 38 | var high = price; 39 | var low = price; 40 | var volume = new BigNumber(trades[tradeIdx].amount); 41 | var total = new BigNumber(price).times(volume); 42 | tradeIdx += 1; 43 | for (; tradeIdx < trades.length; tradeIdx++) { 44 | price = Number(trades[tradeIdx].price); 45 | tradeTimestamp = new BigNumber(self.getTimestamp(trades[tradeIdx].created_at)); 46 | 47 | if (tradeTimestamp.gt(timestamp.minus(granularity)) && tradeTimestamp.lte(timestamp)) { 48 | if (price > high) { 49 | high = price; 50 | } 51 | if (price < low) { 52 | low = price; 53 | } 54 | volume = volume.plus(trades[tradeIdx].amount); 55 | total = total.plus(new BigNumber(price).times(trades[tradeIdx].amount)); 56 | } else { 57 | close = price; 58 | break; 59 | } 60 | } 61 | 62 | candles.push([timestamp.toNumber(), Number(open), close, high, low, volume.toNumber(), total.toNumber()]); 63 | } else { 64 | if (firstOrder) { 65 | candles.push([timestamp.toNumber(), price, price, price, price, 0, 0]); 66 | } else { 67 | candles.push([timestamp.toNumber(), 0, 0, 0, 0, 0, 0]); 68 | } 69 | } 70 | timestamp = timestamp.plus(granularity); 71 | } 72 | callback(candles); 73 | }, baseAssetId, quoteAssetId, 500); 74 | }, 75 | 76 | syncServerMarket: function (baseAssetId, quoteAssetId) { 77 | const self = this; 78 | self.api.market.market(function (resp) { 79 | if (resp.error) { 80 | return true; 81 | } 82 | 83 | const market = resp.data; 84 | var m = {}; 85 | m.base = market.base; 86 | m.quote = market.quote; 87 | m.price = market.price; 88 | m.volume = market.volume; 89 | m.total = market.total; 90 | m.change = market.change; 91 | m.quote_usd = market.quote_usd; 92 | m.source = 'SERVER'; 93 | m.updated_at = TimeUtils.rfc3339(new Date()); 94 | 95 | self.db.market.saveMarkets(function (markets) { 96 | callback(markets); 97 | }, [m]); 98 | }, baseAssetId + '-' + quoteAssetId); 99 | }, 100 | 101 | syncServerMarkets: function (callback) { 102 | const self = this; 103 | self.api.market.markets(function (resp) { 104 | if (resp.error) { 105 | return true; 106 | } 107 | 108 | var markets = []; 109 | for (var i = 0; i < resp.data.length; i++) { 110 | const market = resp.data[i]; 111 | var m = {}; 112 | m.base = market.base; 113 | m.quote = market.quote; 114 | m.price = market.price; 115 | m.volume = market.volume; 116 | m.total = market.total; 117 | m.change = market.change; 118 | m.quote_usd = market.quote_usd; 119 | m.source = 'SERVER'; 120 | markets.push(m); 121 | } 122 | 123 | self.db.market.saveMarkets(function (markets) { 124 | callback(markets); 125 | }, markets); 126 | }); 127 | }, 128 | 129 | syncTrades: function (callback, baseAssetId, quoteAssetId, limit) { 130 | if (!baseAssetId || !quoteAssetId) { 131 | return; 132 | } 133 | 134 | const self = this; 135 | self.db.market.getMarket(function (market) { 136 | if (market && market.source === 'SERVER') { 137 | return; 138 | } 139 | 140 | self.db.trade.getLastTrade(function (trade) { 141 | var offset = trade ? trade.created_at : '2018-08-05T23:59:59.779447612Z'; 142 | if (!limit) { 143 | limit = trade ? 50 : 500; 144 | } 145 | 146 | if (!offset) { 147 | offset = TimeUtils.rfc3339(new Date()) 148 | } 149 | 150 | self.api.ocean.trades(function (resp) { 151 | if (resp.error) { 152 | return true; 153 | } 154 | 155 | const isPageEnded = resp.data.length < limit; 156 | self.db.trade.saveTrades(function(trades) { 157 | if (isPageEnded) { 158 | callback(trades); 159 | } else { 160 | self.syncTrades(callback, baseAssetId, quoteAssetId, 500); 161 | } 162 | }, resp.data, baseAssetId, quoteAssetId, market, trade); 163 | }, baseAssetId + '-' + quoteAssetId, offset, limit); 164 | 165 | }, baseAssetId, quoteAssetId); 166 | }, baseAssetId, quoteAssetId); 167 | }, 168 | 169 | }; 170 | 171 | export default MarketController; 172 | -------------------------------------------------------------------------------- /web/src/market/market_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{base.symbol}} 3 | {{chain.symbol}} 4 |
    5 | {{base.symbol}}/{{quote.symbol}} 6 |
    7 |
    8 | {{price}} 9 | {{quote.symbol}} 10 |
    11 |
    {{change}}
    12 |
    13 | Vol 14 | {{volume}} 15 | {{base.symbol}} 16 |
    17 |
    18 | 19 |
    20 |
  • -------------------------------------------------------------------------------- /web/src/market/masthead.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/market/masthead.jpg -------------------------------------------------------------------------------- /web/src/market/order_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    {{dimZero amount}}
    3 |
    {{dimZero price}}
    4 |
  • 5 | -------------------------------------------------------------------------------- /web/src/market/symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/over140/mixcoin/8a6896da349312eb6a1f4866c7df5047d2863c73/web/src/market/symbol.png -------------------------------------------------------------------------------- /web/src/market/trade.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 6 |
    7 | 0 {{quote.symbol}} 8 | 9 |
    10 |
    11 | 0% 12 | {{t 'market.ticker.change'}} 13 |
    14 |
    15 | 0 {{base.symbol}} 16 | {{t 'market.ticker.volume'}} 17 |
    18 |
    19 | 0 {{quote.symbol}} 20 | {{t 'market.ticker.total'}} 21 |
    22 |
    23 |
    24 |
    25 |
      26 |
    • 1m
    • 27 |
    • 5m
    • 28 |
    • 15m
    • 29 |
    • 1h
    • 30 |
    • 6h
    • 31 |
    • 1d
    • 32 |
    • {{t 'market.chart.depth'}}
    • 33 |
    34 | 35 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 |
    60 |
    61 |
    {{t 'market.table.book'}}
    62 |
    {{t 'market.table.history'}}
    63 |
    64 |
    65 |
    66 |
    67 |
    68 |
    69 |
    70 |
    71 |
    72 |
    73 |
    74 |
      75 |
    • 76 |
      77 |
      78 |
    • 79 |
    80 |
    81 |
    82 |
    83 |
    84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 |
    91 |
      92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    {{t 'market.form.limit'}}
    98 |
    {{t 'market.form.market'}}
    99 |
    100 |
    101 |
    {{t 'market.form.buy'}}
    102 |
    {{t 'market.form.sell'}}
    103 |
    104 |
    105 |
    106 | {{t 'market.form.price'}} 107 | 108 | {{quote.symbol}} 109 |
    110 |
    111 | {{t 'market.form.amount'}} 112 | 113 | {{base.symbol}} 114 |
    115 |
    116 | 117 | 118 | 119 | 120 | 121 | 122 |
    123 |
    124 |
    125 |
    126 |
    127 |
    128 |
    129 |
    130 |
    131 |
    132 |
    133 | {{t 'market.form.price'}} 134 | 135 | {{quote.symbol}} 136 |
    137 |
    138 | {{t 'market.form.amount'}} 139 | 140 | {{base.symbol}} 141 |
    142 |
    143 | 144 | 145 | 146 | 147 | 148 | 149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 |
    156 |
    157 |
    158 |
    159 |
    160 | {{t 'market.form.amount'}} 161 | 162 | {{quote.symbol}} 163 |
    164 |
    165 | 166 | 167 | 168 | 169 | 170 | 171 |
    172 |
    173 |
    174 |
    175 |
    176 |
    177 |
    178 |
    179 |
    180 |
    181 |
    182 | {{t 'market.form.amount'}} 183 | 184 | {{base.symbol}} 185 |
    186 |
    187 | 188 | 189 | 190 | 191 | 192 | 193 |
    194 |
    195 |
    196 |
    197 |
    198 |
    199 |
    200 |
    201 |
    202 | 215 |
    216 |
    217 |
    218 | -------------------------------------------------------------------------------- /web/src/market/trade.scss: -------------------------------------------------------------------------------- 1 | @import '../constant.scss'; 2 | 3 | .market.layout { 4 | 5 | .market.detail.container { 6 | width: 100%; 7 | height: 100vh; 8 | background: $color-main-background; 9 | box-sizing: border-box; 10 | padding-top: 64px; 11 | } 12 | 13 | .header.container { 14 | width: 100%; 15 | height: 64px; 16 | border-bottom: 1px solid rgba(0,0,0,0.07); 17 | box-sizing: border-box; 18 | padding: 16px 32px; 19 | display: flex; 20 | 21 | .ticker { 22 | margin-right: 32px; 23 | line-height: 32px; 24 | font-size: 24px; 25 | 26 | .sub { 27 | font-size: 70%; 28 | opacity: 0.3; 29 | } 30 | } 31 | 32 | .logo { 33 | width: 30px; 34 | height: 30px; 35 | padding-right: 24px; 36 | padding-bottom: 2px; 37 | 38 | img { 39 | width: 100%; 40 | height: 100%; 41 | } 42 | } 43 | 44 | .up .main { 45 | color: $color-side-bid; 46 | } 47 | 48 | .down .main { 49 | color: $color-side-ask; 50 | } 51 | } 52 | 53 | .main.container { 54 | display: flex; 55 | height: calc(100% - 64px); 56 | box-sizing: border-box; 57 | overflow: hidden; 58 | } 59 | 60 | .charts.container { 61 | position: relative; 62 | flex: 1; 63 | margin: 0 auto; 64 | height: calc(100%); 65 | border-right: 1px solid rgba(0,0,0,0.07); 66 | 67 | .icon { 68 | display: none; 69 | cursor: pointer; 70 | position: absolute; 71 | z-index: 1000; 72 | top: calc(50% + 32px); 73 | left: calc(50% - 32px); 74 | &.show { 75 | display: inline-block; 76 | } 77 | &.icon-plus { 78 | left: calc(50% + 32px); 79 | } 80 | &.disabled { 81 | color: rgba(0, 0, 0, 0.3); 82 | } 83 | } 84 | 85 | .tabs { 86 | margin: 0; 87 | padding: 0; 88 | display: flex; 89 | border-bottom: 1px solid rgba(0,0,0,0.07); 90 | 91 | li { 92 | flex: 1; 93 | list-style: none; 94 | margin: auto; 95 | padding: 0; 96 | line-height: 32px; 97 | font-size: 16px; 98 | text-transform: uppercase; 99 | cursor: pointer; 100 | text-align: center; 101 | 102 | &.active { 103 | background: rgba(0,0,0,0.07); 104 | } 105 | 106 | &.depth { 107 | display: none; 108 | } 109 | } 110 | } 111 | 112 | .price.chart { 113 | height: calc(50% - 16px); 114 | border-bottom: 1px solid rgba(0,0,0,0.07); 115 | } 116 | 117 | .depth.chart { 118 | height: calc(50% - 16px); 119 | border-bottom: 1px solid rgba(0,0,0,0.07); 120 | } 121 | 122 | .highcharts-point-up { 123 | fill: $color-side-bid; 124 | stroke: $color-side-bid; 125 | } 126 | 127 | .highcharts-point-down { 128 | fill: $color-side-ask; 129 | stroke: $color-side-ask; 130 | } 131 | 132 | .highcharts-ema-series { 133 | .highcharts-point { 134 | opacity: 0; 135 | } 136 | 137 | .highcharts-point-hover { 138 | opacity: 1; 139 | } 140 | } 141 | } 142 | 143 | .orders.trades { 144 | width: 280px; 145 | height: calc(100%); 146 | position: relative; 147 | overflow: hidden; 148 | font-family: $font-main-mono; 149 | font-weight: 400; 150 | font-size: 12px; 151 | color: #555555; 152 | border-right: 1px solid rgba(0,0,0,0.1); 153 | box-sizing: border-box; 154 | display: flex; 155 | 156 | .tabs { 157 | z-index: 1; 158 | font-family: $font-main-content; 159 | background: $color-main-background; 160 | display: flex; 161 | padding: 0 16px; 162 | font-size: 14px; 163 | line-height: 32px; 164 | position: absolute; 165 | top: 0; 166 | left: 0; 167 | width: 100%; 168 | box-sizing: border-box; 169 | } 170 | 171 | .tab { 172 | flex: 1; 173 | width: 50%; 174 | box-sizing: border-box; 175 | text-align: center; 176 | text-transform: uppercase; 177 | cursor: pointer; 178 | border-bottom: 2px solid rgba(0,0,0,0.07); 179 | 180 | &.active { 181 | border-bottom: 2px solid $color-main-highlight; 182 | } 183 | } 184 | 185 | .order.book { 186 | flex: 1; 187 | position: relative; 188 | height: 100%; 189 | overflow: hidden; 190 | } 191 | 192 | .trade.history { 193 | flex: 1; 194 | height: 100%; 195 | overflow: hidden; 196 | display: none; 197 | } 198 | 199 | .book.data { 200 | display: none; 201 | position: absolute; 202 | width: 100%; 203 | } 204 | 205 | .history.data { 206 | padding-top: 34px; 207 | box-sizing: border-box; 208 | 209 | li { 210 | div.time { 211 | text-align: left; 212 | opacity: 0.3; 213 | } 214 | 215 | div.amount { 216 | padding: 0 8px 0 4px; 217 | } 218 | } 219 | } 220 | 221 | ul { 222 | margin: 0; 223 | padding: 0; 224 | } 225 | 226 | li { 227 | list-style: none; 228 | margin: 0; 229 | padding: 0; 230 | cursor: pointer; 231 | line-height: 1.5em; 232 | display: flex; 233 | text-align: right; 234 | box-sizing: border-box; 235 | padding: 0 16px; 236 | 237 | div { 238 | flex: 1; 239 | } 240 | 241 | .num { 242 | opacity: 0.3; 243 | } 244 | 245 | &:hover { 246 | background: rgba(0,0,0,0.05) !important; 247 | } 248 | 249 | &.spread { 250 | display: block; 251 | line-height: 2em; 252 | 253 | div { 254 | display: inline-block; 255 | } 256 | 257 | div:first-child { 258 | font-size: 18px; 259 | } 260 | } 261 | } 262 | 263 | .ask .price { 264 | color: $color-side-ask; 265 | } 266 | 267 | .bid .price { 268 | color: $color-side-bid; 269 | } 270 | 271 | .trade.ask .price { 272 | color: $color-side-bid; 273 | } 274 | 275 | .trade.bid .price { 276 | color: $color-side-ask; 277 | } 278 | } 279 | 280 | .trade.form { 281 | width: 320px; 282 | height: calc(100%); 283 | position: relative; 284 | overflow: hidden; 285 | box-sizing: border-box; 286 | color: #555555; 287 | 288 | .tabs { 289 | display: flex; 290 | padding: 0 16px 8px 16px; 291 | font-size: 14px; 292 | line-height: 32px; 293 | 294 | &.side { 295 | padding-top: 8px; 296 | } 297 | } 298 | 299 | .tab { 300 | flex: 1; 301 | width: 50%; 302 | box-sizing: border-box; 303 | text-align: center; 304 | text-transform: uppercase; 305 | cursor: pointer; 306 | } 307 | 308 | .type.tab { 309 | border-bottom: 2px solid rgba(0,0,0,0.07); 310 | 311 | &.active { 312 | border-bottom: 2px solid $color-main-highlight; 313 | } 314 | } 315 | 316 | .side.tab { 317 | background: rgba(0,0,0,0.07); 318 | 319 | &.buy { 320 | border-radius: 4px 0 0 4px; 321 | 322 | &.active { 323 | background: $color-side-bid; 324 | color: $color-main-background; 325 | } 326 | } 327 | 328 | &.sell { 329 | border-radius: 0 4px 4px 0; 330 | 331 | &.active { 332 | background: $color-side-ask; 333 | color: $color-main-background; 334 | } 335 | } 336 | } 337 | 338 | .form { 339 | display: none; 340 | padding: 8px 16px; 341 | box-sizing: border-box; 342 | font-size: 16px; 343 | 344 | &.active { 345 | display: block; 346 | } 347 | 348 | .text.field { 349 | margin-bottom: 16px; 350 | border-bottom: 1px solid $color-main-highlight; 351 | display: flex; 352 | } 353 | 354 | .label { 355 | display: block; 356 | line-height: 48px; 357 | 358 | &.right { 359 | text-align: right; 360 | } 361 | } 362 | 363 | input[type="text"], 364 | input[type="number"] { 365 | margin: auto; 366 | flex: 1; 367 | border: 0 none; 368 | outline: 0 none; 369 | line-height: 48px; 370 | font-family: $font-main-mono; 371 | text-align: right; 372 | display: block; 373 | box-sizing: border-box; 374 | padding: 0 8px; 375 | min-width: 0; 376 | } 377 | 378 | .tips { 379 | margin-top: 32px; 380 | height: 24px; 381 | width: 100%; 382 | font-size: 10px; 383 | color: red; 384 | display: none; 385 | } 386 | 387 | input[type="submit"], 388 | .submit-loader { 389 | border: 0 none; 390 | outline: 0 none; 391 | margin-top: 32px; 392 | line-height: 42px; 393 | height: 42px; 394 | display: block; 395 | width: 100%; 396 | text-transform: uppercase; 397 | font-weight: 300; 398 | box-sizing: border-box; 399 | border-radius: 4px; 400 | box-shadow: 0 0 2px rgba(0,0,0,0.1); 401 | color: $color-main-background; 402 | cursor: pointer; 403 | 404 | &.buy { 405 | background: $color-side-bid; 406 | } 407 | 408 | &.sell { 409 | background: $color-side-ask; 410 | } 411 | } 412 | 413 | .submit-loader { 414 | display: none; 415 | } 416 | } 417 | 418 | .account.balances { 419 | line-height: 2.5em; 420 | font-size: 14px; 421 | padding: 16px 0; 422 | 423 | .balance { 424 | font-family: $font-main-mono; 425 | display: flex; 426 | padding: 0 16px; 427 | display: none; 428 | 429 | .asset { 430 | flex: 1; 431 | display: block; 432 | 433 | &.amount { 434 | text-align: right; 435 | } 436 | } 437 | } 438 | 439 | .actions { 440 | display: flex; 441 | margin-top: 8px; 442 | flex-wrap: wrap; 443 | 444 | .action { 445 | flex: 1; 446 | cursor: pointer; 447 | border: 1px solid rgba(0,0,0,0.1); 448 | color: $color-main-foreground-light; 449 | border-radius: 2px; 450 | display: block; 451 | text-align: center; 452 | text-transform: uppercase; 453 | min-width: 120px; 454 | margin: 8px 16px; 455 | 456 | i { 457 | font-size: 80%; 458 | margin-right: 4px; 459 | } 460 | } 461 | 462 | .join { 463 | display: none; 464 | border: 1px solid #397EE4; 465 | color: #397EE4; 466 | } 467 | 468 | } 469 | } 470 | } 471 | 472 | .submit-loader { 473 | position: relative; 474 | display: none; 475 | cursor: wait; 476 | background-color: $color-main-background; 477 | 478 | .spinner { 479 | position: absolute; 480 | top: 50%; 481 | left: 50%; 482 | transform: translate(-50%, -50%); 483 | width: 70px; 484 | padding-top: 4px; 485 | text-align: center; 486 | } 487 | 488 | .spinner > div { 489 | width: 12px; 490 | height: 12px; 491 | background-color: $color-main-background; 492 | border-radius: 100%; 493 | display: inline-block; 494 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 495 | } 496 | 497 | .spinner .bounce1 { 498 | animation-delay: -0.32s; 499 | } 500 | 501 | .spinner .bounce2 { 502 | animation-delay: -0.16s; 503 | } 504 | 505 | @keyframes sk-bouncedelay { 506 | 0%, 80%, 100% { 507 | transform: scale(0); 508 | } 40% { 509 | transform: scale(1.0); 510 | } 511 | } 512 | } 513 | 514 | @media (min-width: 1600px) { 515 | .orders.trades { 516 | width: 560px; 517 | font-size: 14px; 518 | font-weight: 300; 519 | 520 | .tabs { 521 | padding: 0; 522 | 523 | .tab.active { 524 | border-bottom: 2px solid rgba(0,0,0,0.1); 525 | } 526 | } 527 | 528 | .order.book, 529 | .trade.history { 530 | width: 50%; 531 | display: block; 532 | } 533 | 534 | .trade.history { 535 | border-left: 1px solid rgba(0,0,0,0.1); 536 | } 537 | } 538 | } 539 | 540 | @media (max-width: 1024px) { 541 | .market.detail.container { 542 | height: auto; 543 | } 544 | 545 | .header.container { 546 | .ticker { 547 | margin: 0; 548 | flex: 1; 549 | } 550 | 551 | .ticker.total { 552 | display: none; 553 | } 554 | 555 | .logo { 556 | display: none; 557 | } 558 | } 559 | 560 | .main.container { 561 | flex-wrap: wrap; 562 | height: auto; 563 | margin: 0; 564 | padding: 0; 565 | } 566 | 567 | .charts.container { 568 | flex-basis: 100%; 569 | height: 360px; 570 | 571 | .tabs li { 572 | font-size: 14px; 573 | 574 | &.depth { 575 | display: block; 576 | } 577 | } 578 | 579 | .price.chart { 580 | height: calc(100% - 32px); 581 | } 582 | 583 | .depth.chart { 584 | height: calc(100% - 32px); 585 | display: none; 586 | } 587 | } 588 | 589 | .orders.trades, 590 | .trade.form { 591 | height: 512px; 592 | width: 50%; 593 | border-top: 1px solid rgba(0,0,0,0.1); 594 | margin-top: 16px; 595 | 596 | .form { 597 | font-size: 14px; 598 | } 599 | } 600 | } 601 | 602 | @media (max-width: 768px) { 603 | .header.container { 604 | padding: 14px 16px 10px; 605 | 606 | .ticker { 607 | line-height: 20px; 608 | font-size: 16px; 609 | text-align: center; 610 | 611 | .main, 612 | .sub { 613 | display: block; 614 | } 615 | } 616 | } 617 | 618 | .charts.container { 619 | height: 300px; 620 | 621 | .tabs li { 622 | font-size: 12px; 623 | } 624 | } 625 | 626 | .orders.trades, 627 | .trade.form { 628 | height: 400px; 629 | min-height: 50vh; 630 | } 631 | 632 | .trade.form .side.tabs { 633 | padding-top: 0; 634 | } 635 | 636 | .trade.form .form { 637 | padding-top: 0; 638 | 639 | .text.field { 640 | margin-bottom: 8px; 641 | font-size: 12px; 642 | } 643 | 644 | .label, 645 | input[type="text"], 646 | input[type="number"] { 647 | line-height: 36px; 648 | } 649 | 650 | input[type="submit"], 651 | .submit-loader { 652 | margin-top: 16px; 653 | line-height: 36px; 654 | } 655 | } 656 | 657 | .trade.form .account.balances { 658 | font-size: 12px; 659 | padding: 8px 0; 660 | 661 | .balance { 662 | line-height: 2em; 663 | } 664 | } 665 | 666 | .orders.trades { 667 | font-size: 12px; 668 | 669 | .data { 670 | font-weight: 400; 671 | } 672 | } 673 | } 674 | 675 | } 676 | -------------------------------------------------------------------------------- /web/src/market/trade_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    {{time}}
    3 |
    {{dimZero amount}}
    4 |
    {{dimZero price}}
    5 |
  • 6 | -------------------------------------------------------------------------------- /web/src/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace, monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace, monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | -------------------------------------------------------------------------------- /web/src/utils/form.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | function FormUtils() { 4 | } 5 | 6 | FormUtils.prototype = { 7 | serialize: function (element) { 8 | var out = {}; 9 | var data = $(element).serializeArray(); 10 | for(var i = 0; i < data.length; i++){ 11 | var record = data[i]; 12 | out[record.name] = record.value; 13 | } 14 | return out; 15 | } 16 | } 17 | 18 | export default new FormUtils(); 19 | -------------------------------------------------------------------------------- /web/src/utils/time.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | function TimeUtils() { 4 | } 5 | 6 | TimeUtils.prototype = { 7 | rfc3339: function (d) { 8 | function pad(n){return n<10 ? '0'+n : n} 9 | return d.getUTCFullYear()+'-' 10 | + pad(d.getUTCMonth()+1)+'-' 11 | + pad(d.getUTCDate())+'T' 12 | + pad(d.getUTCHours())+':' 13 | + pad(d.getUTCMinutes())+':' 14 | + pad(d.getUTCSeconds())+'Z' 15 | }, 16 | 17 | short: function(time) { 18 | var date = new Date(time); 19 | if (date.setHours(0, 0, 0, 0) != new Date().setHours(0, 0, 0, 0)) { 20 | var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate(); 21 | var month = date.getMonth() < 9 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1; 22 | return day + '/' + month + '/' + (date.getYear() - 100); 23 | } 24 | date = new Date(time); 25 | var hour = date.getHours() < 10 ? '0' + date.getHours() : date.getHours(); 26 | var minute = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes(); 27 | var second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds(); 28 | return hour + ':' + minute + ':' + second; 29 | }, 30 | 31 | getUTCDate: function (d) { 32 | return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()); 33 | } 34 | } 35 | 36 | export default new TimeUtils(); 37 | -------------------------------------------------------------------------------- /web/src/utils/url.js: -------------------------------------------------------------------------------- 1 | function URLUtils() { 2 | } 3 | 4 | URLUtils.prototype = { 5 | getUrlParameter: function(name) { 6 | name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); 7 | var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 8 | var results = regex.exec(window.location.search); 9 | return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); 10 | } 11 | }; 12 | 13 | export default new URLUtils(); 14 | -------------------------------------------------------------------------------- /web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 5 | const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin"); 6 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); 7 | const OfflinePlugin = require('offline-plugin'); 8 | 9 | const extractSass = new ExtractTextPlugin({ 10 | filename: "[name]-[hash].css" 11 | }); 12 | 13 | const webRoot = function (env) { 14 | if (env === 'production') { 15 | return 'https://mixcoin.one'; 16 | } else { 17 | return 'http://wallet.exchange.local'; 18 | } 19 | }; 20 | 21 | const appId = function (env) { 22 | if (env === 'production') { 23 | return '82d20bc7-9a97-4a69-bcd0-4da502374f6c'; 24 | } else { 25 | return 'c2ab81d4-2226-4d0c-a49a-dc59b34f7972'; 26 | } 27 | }; 28 | 29 | const appSecret = function (env) { 30 | // return your app secret 31 | }; 32 | 33 | module.exports = { 34 | entry: { 35 | app: './src/app.js' 36 | }, 37 | 38 | output: { 39 | publicPath: '/assets/', 40 | path: path.resolve(__dirname, 'dist'), 41 | filename: '[name]-[chunkHash].js' 42 | }, 43 | 44 | resolve: { 45 | alias: { 46 | jquery: "jquery/dist/jquery", 47 | handlebars: "handlebars/dist/handlebars.runtime" 48 | } 49 | }, 50 | 51 | module: { 52 | rules: [{ 53 | test: /\.html$/, loader: "handlebars-loader?helperDirs[]=" + __dirname + "/src/helpers" 54 | }, { 55 | test: /\.(scss|css)$/, 56 | use: extractSass.extract({ 57 | use: [{ 58 | loader: "css-loader" 59 | }, { 60 | loader: "sass-loader" 61 | }], 62 | fallback: "style-loader" 63 | }) 64 | }, { 65 | test: /\.(woff|woff2|eot|ttf|otf|svg|png|jpg|gif)$/, 66 | use: [ 67 | 'file-loader' 68 | ] 69 | }] 70 | }, 71 | 72 | plugins: [ 73 | new webpack.DefinePlugin({ 74 | PRODUCTION: (process.env.NODE_ENV === 'production'), 75 | WEB_ROOT: JSON.stringify(webRoot(process.env.NODE_ENV)), 76 | API_ROOT: JSON.stringify("https://example.ocean.one"), 77 | ENGINE_ROOT: JSON.stringify("wss://events.ocean.one"), 78 | APP_NAME: JSON.stringify("Wallet Exchange"), 79 | CLIENT_ID: JSON.stringify(appId(process.env.NODE_ENV)), 80 | CLIENT_SECRET: JSON.stringify(appSecret(process.env.NODE_ENV)), 81 | ENGINE_USER_ID: JSON.stringify("aaff5bef-42fb-4c9f-90e0-29f69176b7d4") 82 | }), 83 | new HtmlWebpackPlugin({ 84 | template: './src/layout.html' 85 | }), 86 | new FaviconsWebpackPlugin({ 87 | logo: './src/launcher.png', 88 | prefix: 'icons-[hash]-', 89 | background: '#FFFFFF' 90 | }), 91 | new ScriptExtHtmlWebpackPlugin({ 92 | defaultAttribute: 'async' 93 | }), 94 | extractSass, 95 | new OfflinePlugin() 96 | ] 97 | }; 98 | --------------------------------------------------------------------------------