├── .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 | 
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 |
7 |
8 |
9 |
10 |
11 | {{t 'orders.type.limit'}}
12 | {{t 'orders.type.market'}}
13 |
14 |
15 | {{t 'orders.state.pending'}}
16 | {{t 'orders.state.history'}}
17 |
18 |
19 |
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 |
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 |
2 |
3 |
4 |
{{title}}
5 |
6 |
7 |
14 |
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 |
3 |
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 |
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 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
93 |
94 |
95 |
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 |
--------------------------------------------------------------------------------