├── .gitignore
├── .huskyrc
├── .prettierrc
├── LICENSE
├── README.md
├── _redirects
├── commitlint.config.js
├── package.json
├── public
├── CNAME
├── Montserrat-Bold.ttf
├── Montserrat-Medium.ttf
├── Montserrat-SemiBold.ttf
├── favicon.png
├── index.html
├── manifest.json
└── robots.txt
├── server.js
├── src
├── actions
│ ├── i18n
│ │ └── index.js
│ ├── loopring
│ │ └── index.js
│ └── web3
│ │ └── index.js
├── components
│ ├── button
│ │ └── index.jsx
│ ├── chip
│ │ ├── index.jsx
│ │ └── styled.js
│ ├── drawer
│ │ ├── index.jsx
│ │ ├── sections
│ │ │ ├── exchange-info
│ │ │ │ └── index.jsx
│ │ │ ├── lrc
│ │ │ │ └── index.jsx
│ │ │ ├── settings
│ │ │ │ ├── index.jsx
│ │ │ │ └── styled.js
│ │ │ ├── support
│ │ │ │ └── index.jsx
│ │ │ └── technical-resources
│ │ │ │ └── index.jsx
│ │ └── styled.js
│ ├── full-screen-overlay
│ │ └── index.jsx
│ ├── invalid-chain-id
│ │ ├── index.jsx
│ │ └── styled.js
│ ├── layout
│ │ ├── footer
│ │ │ ├── index.jsx
│ │ │ └── styled.js
│ │ ├── index.jsx
│ │ ├── styled.js
│ │ └── toolbar
│ │ │ ├── index.jsx
│ │ │ └── styled.js
│ ├── spinner
│ │ └── index.jsx
│ ├── switch
│ │ └── index.jsx
│ ├── token-icon
│ │ ├── index.jsx
│ │ └── styled.js
│ ├── token-modal
│ │ ├── index.jsx
│ │ └── styled.js
│ ├── token-select
│ │ ├── index.jsx
│ │ └── styled.js
│ ├── token-specifier
│ │ ├── index.jsx
│ │ └── styled.js
│ └── undecorated-link
│ │ └── index.jsx
├── env
│ └── index.js
├── i18n
│ └── messages
│ │ ├── en.json
│ │ └── it.json
├── images
│ ├── keep.png
│ ├── logo.png
│ ├── logo.svg
│ ├── nest.png
│ └── pnetwork.png
├── index.js
├── lightcone
│ ├── api
│ │ ├── CommissionRewardAPI.js
│ │ ├── LightconeAPI.js
│ │ ├── LiquidityMiningAPI.js
│ │ ├── localStorgeAPI.js
│ │ └── v1
│ │ │ ├── allowances
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── balances
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── depth
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── ethBalance
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── ethnonce
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── fee-rates
│ │ │ └── index.js
│ │ │ ├── marketinfo
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── onchainwithdrawal
│ │ │ ├── get.js
│ │ │ ├── index.js
│ │ │ └── post.js
│ │ │ ├── orders
│ │ │ ├── delete.js
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── price
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── recommendedGasPrice
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── tokenBalance
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── tokeninfo
│ │ │ ├── get.js
│ │ │ └── index.js
│ │ │ ├── transfer
│ │ │ ├── get.js
│ │ │ ├── index.js
│ │ │ └── post.js
│ │ │ └── userTrades
│ │ │ ├── get.js
│ │ │ └── index.js
│ ├── common
│ │ ├── formatter.js
│ │ ├── index.js
│ │ ├── request.js
│ │ ├── schemas.js
│ │ ├── utils.js
│ │ └── validator.js
│ ├── config
│ │ ├── abis
│ │ │ ├── contractWallet.json
│ │ │ ├── erc20.json
│ │ │ └── exchange.json
│ │ ├── config.json
│ │ └── index.js
│ ├── ethereum
│ │ ├── contracts
│ │ │ ├── AbiFunction.js
│ │ │ ├── Contract.js
│ │ │ ├── Contracts.js
│ │ │ └── index.js
│ │ └── metaMask.js
│ ├── sign
│ │ ├── babyjub.js
│ │ ├── bitstream.js
│ │ ├── constants.js
│ │ ├── eddsa.js
│ │ ├── exchange.js
│ │ ├── float.js
│ │ └── poseidon.js
│ └── wallet
│ │ ├── index.js
│ │ └── wallet.js
├── reducers
│ ├── i18n
│ │ └── index.js
│ ├── index.js
│ ├── loopring
│ │ └── index.js
│ └── web3
│ │ └── index.js
├── serviceWorker.js
├── utils
│ └── index.js
└── views
│ ├── app
│ ├── index.jsx
│ └── styled.js
│ └── swapper
│ ├── index.jsx
│ └── styled.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env.*
26 | .env*
27 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "husky": {
3 | "hooks": {
4 | "commit-msg": "lint:commit-message"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "singleQuote": false,
4 | "tabWidth": 4
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Loopring Swap
2 |
3 | An effort to implement order-book-transparent L2 token swaps powered by the Loopring protocol.
4 | Fast, cheap and painless: join the fast lane, today.
5 |
6 | ## Environment
7 |
8 | Before you can start hacking, some environment variables setting is going to be needed. Particularly:
9 |
10 | - `CHAIN_ID`: the chain id to which the app refers to. It is used to determine the Loopring'a API endpoint and the general app's environment (the only supported values are `1` for mainnet and `5` for Goerli).
11 | - `INFURA_URL`: a valid Infura Ethereum provider URL
12 | - `DEX_SMART_CONTRACT_ADDRESS`: the address of the Loopring DEX smart contract in the currently active network (defined by `CHAIN_ID`). It is required, but the value does not need to be precise when developing. It is used in the drawer menu to direct the user to the correct contract on Etherscan when clicking on the "DEX smart contract" item.
13 |
14 | The app uses `dotenv` to load environment variables during local development, so using customized variables is as simple as creating a `.env` file in the project's root and writing them there.
15 |
16 | This being a React app, the environment variables need to be prefixed by `REACT_APP` in order to be picked up.
17 |
18 | ## Development
19 |
20 | To start up a local instance of the app, just use the command `yarn start`.
21 |
22 | **Stay tuned for official beta v1 release, a stable v1 and a possible v2.**
23 |
24 | ## Screenshots
25 |
26 | Light theme | Dark theme
27 | -|-
28 |  | 
29 |  | 
30 |  | 
31 |
--------------------------------------------------------------------------------
/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ["@commitlint/config-conventional"] };
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loopring-swap",
3 | "version": "1.0.0-beta.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.30",
7 | "@fortawesome/free-brands-svg-icons": "^5.14.0",
8 | "@fortawesome/free-solid-svg-icons": "^5.14.0",
9 | "@fortawesome/react-fontawesome": "^0.1.11",
10 | "@mondora/env": "^1.3.0",
11 | "@myetherwallet/mewconnect-web-client": "^2.1.1-RC.4.6",
12 | "@testing-library/jest-dom": "^4.2.4",
13 | "@testing-library/react": "^9.3.2",
14 | "@testing-library/user-event": "^7.1.2",
15 | "@walletconnect/web3-provider": "^1.2.2",
16 | "async-validator": "^3.4.0",
17 | "authereum": "^0.0.4-beta.193",
18 | "axios": "^0.19.2",
19 | "bignumber.js": "^9.0.0",
20 | "blake-hash": "^2.0.0",
21 | "blake2b": "^2.1.3",
22 | "crypto-js": "^4.0.0",
23 | "ethereumjs-abi": "^0.6.8",
24 | "helmet": "^4.1.1",
25 | "moment": "^2.27.0",
26 | "react": "^16.13.1",
27 | "react-dom": "^16.13.1",
28 | "react-intl": "^5.4.7",
29 | "react-number-format": "^4.4.1",
30 | "react-redux": "^7.2.1",
31 | "react-scripts": "3.4.3",
32 | "react-switch": "^5.0.1",
33 | "react-toastify": "^6.0.8",
34 | "redux": "^4.0.5",
35 | "redux-thunk": "^2.3.0",
36 | "reflexbox": "^4.0.6",
37 | "snarkjs": "0.1.20",
38 | "styled-components": "^5.2.0",
39 | "use-debounce": "^3.4.3",
40 | "web3": "^1.2.11",
41 | "web3-utils": "^1.2.11",
42 | "web3modal": "^1.9.0"
43 | },
44 | "scripts": {
45 | "analyze": "source-map-explorer 'build/static/js/*.js'",
46 | "start": "react-scripts start",
47 | "build": "react-scripts build && copy _redirects ./build",
48 | "test": "react-scripts test",
49 | "eject": "react-scripts eject",
50 | "lint:commit-message": "commitlint -e",
51 | "predeploy": "yarn build",
52 | "deploy": "gh-pages -d build"
53 | },
54 | "eslintConfig": {
55 | "extends": "react-app"
56 | },
57 | "browserslist": {
58 | "production": [
59 | ">0.2%",
60 | "not dead",
61 | "not op_mini all"
62 | ],
63 | "development": [
64 | "last 1 chrome version",
65 | "last 1 firefox version",
66 | "last 1 safari version"
67 | ]
68 | },
69 | "devDependencies": {
70 | "@commitlint/cli": "^9.1.1",
71 | "@commitlint/config-conventional": "^9.1.1",
72 | "copy": "^0.3.2",
73 | "dotenv": "^8.2.0",
74 | "gh-pages": "^2.1.1",
75 | "husky": "^4.2.5"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | swap.loopring.io
--------------------------------------------------------------------------------
/public/Montserrat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/public/Montserrat-Bold.ttf
--------------------------------------------------------------------------------
/public/Montserrat-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/public/Montserrat-Medium.ttf
--------------------------------------------------------------------------------
/public/Montserrat-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/public/Montserrat-SemiBold.ttf
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/public/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
27 | Loopring Swap
28 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "icons": [
3 | {
4 | "src": "favicon.png",
5 | "sizes": "32x32",
6 | "type": "image/png"
7 | }
8 | ]
9 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | // [START app]
2 | const express = require('express');
3 | const helmet = require('helmet');
4 | const bodyParser = require('body-parser');
5 | const path = require('path');
6 |
7 | const os = require('os');
8 | const cluster = require('cluster');
9 | const clusterWorkerSize = Math.floor(os.cpus().length / 2);
10 |
11 | // Listen to the App Engine-specified port, or 8080 otherwise
12 | const PORT = process.env.PORT || 6999;
13 | let app;
14 |
15 | if (clusterWorkerSize > 1) {
16 | if (cluster.isMaster) {
17 | for (let i = 0; i < clusterWorkerSize; i++) {
18 | cluster.fork();
19 | }
20 |
21 | cluster.on('exit', function (worker) {
22 | console.log('Worker', worker.id, ' has exitted.');
23 | });
24 | } else {
25 | app = express();
26 | app.use(
27 | helmet.contentSecurityPolicy({
28 | directives: {
29 | 'default-src': [
30 | "'self'",
31 | 'data:',
32 | "'unsafe-inline'",
33 | "'unsafe-eval'",
34 | 'uat.loopring.io',
35 | 'wss://ws.uat.loopring.io',
36 | 'api.loopring.io',
37 | 'wss://ws.loopring.io',
38 | 'wss://bridge.walletconnect.org',
39 | 'wss://mainnet.infura.io',
40 | '*.infura.io',
41 | '*.bandchain.org',
42 | '*.authereum.com',
43 | 'wss://connect.mewapi.io',
44 | 'wss://connect2.mewapi.io',
45 | 'fonts.googleapis.com',
46 | 'fonts.gstatic.com',
47 | 'blob:',
48 | ],
49 | 'img-src': ["'self'", 'data:', 'cnzz.mmstat.com', '*.cnzz.com', 'loopring.io'],
50 | 'script-src': [
51 | "'self'",
52 | "'unsafe-inline'",
53 | "'unsafe-eval'",
54 | '*.cnzz.com',
55 | 'uat.loopring.io',
56 | 'ws.uat.loopring.io',
57 | 'api.loopring.io',
58 | 'ws.loopring.io',
59 | 'connect.mewapi.io',
60 | 'blob:',
61 | ],
62 | },
63 | })
64 | );
65 | app.use(helmet.dnsPrefetchControl());
66 | app.use(helmet.expectCt());
67 | app.use(helmet.frameguard());
68 | app.use(helmet.hidePoweredBy());
69 | app.use(helmet.hsts());
70 | app.use(helmet.ieNoOpen());
71 | app.use(helmet.noSniff());
72 | app.use(helmet.permittedCrossDomainPolicies());
73 | app.use(helmet.referrerPolicy());
74 | app.use(helmet.xssFilter());
75 |
76 | // One day: 86400
77 | // One hour: 3600
78 | const CACHE_CONTROL = 'public, max-age=3600, s-maxage=3600';
79 | var DIST_DIR = path.join(__dirname, 'build');
80 |
81 | app.use(bodyParser.json());
82 | app.use(bodyParser.urlencoded({ extended: true }));
83 |
84 | app.get('/', (req, res) => {
85 | res.set('Cache-Control', CACHE_CONTROL);
86 | res.sendFile(path.join(DIST_DIR, '/index.html'));
87 | });
88 |
89 | app.get('/manifest.json', (req, res) => {
90 | res.sendFile(path.join(DIST_DIR, '/manifest.json'));
91 | });
92 |
93 | app.get('/favicon.png', (req, res) => {
94 | res.sendFile(path.join(DIST_DIR, '/favicon.png'));
95 | });
96 |
97 | app.get('/favicon.svg', (req, res) => {
98 | res.sendFile(path.join(DIST_DIR, '/favicon.svg'));
99 | });
100 |
101 | app.use('/', express.static(DIST_DIR, { maxAge: '1d' }));
102 |
103 | app.get('*', (req, res) => {
104 | res.set('Cache-Control', CACHE_CONTROL);
105 | res.sendFile(path.join(DIST_DIR, '/index.html'));
106 | });
107 |
108 | app.listen(PORT, function () {
109 | console.log(
110 | `Express server listening on port ${PORT} and worker ${process.pid}`
111 | );
112 | });
113 | }
114 | }
115 |
116 | // [END app]
117 |
118 | module.exports = app;
119 |
--------------------------------------------------------------------------------
/src/actions/i18n/index.js:
--------------------------------------------------------------------------------
1 | export const SWITCH_LANGUAGE = "CHANGE_LANGUAGE";
2 |
3 | export const switchLanguage = (language) => ({
4 | type: SWITCH_LANGUAGE,
5 | language,
6 | });
7 |
--------------------------------------------------------------------------------
/src/actions/web3/index.js:
--------------------------------------------------------------------------------
1 | import { getWeb3Modal } from "../../views/app";
2 | import Web3 from "web3";
3 | import { logout } from "../loopring";
4 |
5 | export const INITIALIZE_WEB3_SUCCESS = "INITIALIZE_WEB3_SUCCESS";
6 | export const WEB3_ACCOUNT_CHANGED = "WEB3_ACCOUNT_CHANGED";
7 | export const CHAIN_ID_CHANGED = "CHAIN_ID_CHANGED";
8 |
9 | export const initializeWeb3 = () => async (dispatch) => {
10 | try {
11 | const web3Modal = getWeb3Modal();
12 | const provider = await web3Modal.connect();
13 | provider.autoRefreshOnNetworkChange = false;
14 | provider.on("chainChanged", (hexChainId) => {
15 | dispatch({
16 | type: CHAIN_ID_CHANGED,
17 | chainId: parseInt(hexChainId, 16),
18 | });
19 | dispatch(logout());
20 | });
21 | provider.on("accountsChanged", () => {
22 | dispatch(logout());
23 | });
24 | const web3 = new Web3(provider);
25 | dispatch({
26 | type: CHAIN_ID_CHANGED,
27 | chainId: await web3.eth.getChainId(),
28 | });
29 | const accounts = await web3.eth.getAccounts();
30 | dispatch({
31 | type: INITIALIZE_WEB3_SUCCESS,
32 | web3,
33 | selectedAccount: accounts[0],
34 | });
35 | } catch (error) {
36 | console.error("error initializing web3", error);
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/button/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled, { css } from "styled-components";
4 | import { Flex, Box } from "reflexbox";
5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6 | import { Spinner } from "../spinner";
7 |
8 | const sizeMap = {
9 | small: {
10 | height: 32,
11 | fontSize: 12,
12 | },
13 | medium: {
14 | height: 36,
15 | fontSize: 16,
16 | },
17 | large: {
18 | height: 48,
19 | fontSize: 16,
20 | },
21 | };
22 |
23 | const commonsStyles = css`
24 | display: flex;
25 | white-space: nowrap;
26 | justify-content: center;
27 | align-items: center;
28 | height: ${(props) => sizeMap[props.size].height}px;
29 | padding: 0 20px;
30 | font-size: ${(props) => sizeMap[props.size].fontSize}px;
31 | font-family: Montserrat, sans-serif;
32 | background: ${(props) =>
33 | props.disabled ? props.theme.disabled : props.theme.primary};
34 | color: ${(props) =>
35 | props.disabled ? props.theme.textDisabled : props.theme.textButton};
36 | border: none;
37 | border-radius: 12px;
38 | font-weight: 600;
39 | transition: all 0.3s ease;
40 | outline: none;
41 | cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
42 | text-decoration: none;
43 | `;
44 |
45 | const StyledButton = styled.button`
46 | ${commonsStyles}
47 | `;
48 |
49 | export const Button = ({
50 | children,
51 | faIcon,
52 | size,
53 | loading,
54 | disabled,
55 | ...rest
56 | }) => (
57 |
58 |
59 | {loading && !disabled ? (
60 |
66 |
70 |
71 | ) : (
72 | <>
73 | {faIcon && (
74 |
75 |
76 |
77 | )}
78 | {children}
79 | >
80 | )}
81 |
82 |
83 | );
84 |
85 | Button.propTypes = {
86 | faIcon: PropTypes.object,
87 | size: PropTypes.oneOf(["small", "medium", "large"]),
88 | loading: PropTypes.bool,
89 | disabled: PropTypes.bool,
90 | };
91 |
92 | Button.defaultProps = {
93 | size: "medium",
94 | };
95 |
--------------------------------------------------------------------------------
/src/components/chip/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { RootFlex } from "./styled";
4 |
5 | export const Chip = ({ selected, onClick, children }) => (
6 |
7 | {children}
8 |
9 | );
10 |
11 | Chip.propTypes = {
12 | children: PropTypes.node.isRequired,
13 | selected: PropTypes.bool.isRequired,
14 | onClick: PropTypes.func.isRequired,
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/chip/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex } from "reflexbox";
3 |
4 | export const RootFlex = styled(Flex)`
5 | border: solid 1px ${(props) => props.theme.border};
6 | border-radius: 12px;
7 | padding: 2px 8px;
8 | font-size: 12px;
9 | background: ${(props) =>
10 | props.selected ? props.theme.primary : props.theme.foreground};
11 | color: ${(props) =>
12 | props.selected ? props.theme.textInverted : props.theme.text};
13 | overflow: hidden;
14 | text-overflow: ellipsis;
15 | white-space: nowrap;
16 | transition: background 0.3s ease, border 0.3s ease, color 0.3s ease;
17 | cursor: pointer;
18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/drawer/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useLayoutEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { Box } from "reflexbox";
4 | import {
5 | faPlug,
6 | faLock,
7 | faLockOpen,
8 | faTimes,
9 | faUserPlus,
10 | } from "@fortawesome/free-solid-svg-icons";
11 | import { selectedTheme } from "../../views/app";
12 | import { FormattedMessage } from "react-intl";
13 | import {
14 | RootFlex,
15 | HeaderFlex,
16 | Icon,
17 | EllipsizedBox,
18 | CloseIcon,
19 | SummaryMessage,
20 | } from "./styled";
21 | import { Lrc } from "./sections/lrc";
22 | import { Support } from "./sections/support";
23 | import { TechnicalResources } from "./sections/technical-resources";
24 | import { ExchangeInfo } from "./sections/exchange-info";
25 | import { Settings } from "./sections/settings";
26 | import { Button } from "../button";
27 | import { useCallback } from "react";
28 | import { getShortenedEthereumAddress } from "../../utils";
29 |
30 | export const Drawer = ({
31 | open,
32 | onClose,
33 | onConnectWallet,
34 | selectedWeb3Account,
35 | onLogin,
36 | onLogout,
37 | loggedIn,
38 | darkTheme,
39 | onDarkThemeChange,
40 | selectedLanguage,
41 | onSelectedLanguageChange,
42 | }) => {
43 | const container = useRef(null);
44 |
45 | const [icon, setIcon] = useState(faLock);
46 | const [iconColor, setIconColor] = useState("");
47 | const [summaryMessageKey, setSummaryMessageKey] = useState("placeholder");
48 | const [buttonMessageKey, setButtonMessageKey] = useState("placeholder");
49 |
50 | useLayoutEffect(() => {
51 | if (loggedIn) {
52 | setIcon(faLockOpen);
53 | setIconColor(selectedTheme.success);
54 | setSummaryMessageKey("drawer.wallet.connect.logout");
55 | setButtonMessageKey("drawer.wallet.connect.action.logout");
56 | } else if (selectedWeb3Account) {
57 | setIcon(faLock);
58 | setIconColor(selectedTheme.error);
59 | setSummaryMessageKey("drawer.wallet.connect.login");
60 | setButtonMessageKey("drawer.wallet.connect.action.login");
61 | } else {
62 | setIcon(faPlug);
63 | setIconColor(selectedTheme.primary);
64 | setSummaryMessageKey("drawer.wallet.connect.summary");
65 | setButtonMessageKey("drawer.wallet.connect.action.connect");
66 | }
67 | }, [loggedIn, selectedWeb3Account]);
68 |
69 | const handleButtonClick = useCallback(() => {
70 | if (loggedIn) {
71 | onLogout();
72 | } else if (selectedWeb3Account) {
73 | onLogin();
74 | } else {
75 | onConnectWallet();
76 | }
77 | }, [
78 | loggedIn,
79 | onConnectWallet,
80 | onLogin,
81 | onLogout,
82 | selectedWeb3Account,
83 | ]);
84 |
85 | return (
86 |
92 |
93 |
94 | {selectedWeb3Account ? (
95 | getShortenedEthereumAddress(selectedWeb3Account)
96 | ) : (
97 |
98 | )}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
116 |
117 |
118 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | );
139 | };
140 |
141 | Drawer.propTypes = {
142 | open: PropTypes.bool.isRequired,
143 | onClose: PropTypes.func.isRequired,
144 | onConnectWallet: PropTypes.func.isRequired,
145 | selectedWeb3Account: PropTypes.string,
146 | onLogin: PropTypes.func.isRequired,
147 | onLogout: PropTypes.func.isRequired,
148 | loggedIn: PropTypes.bool.isRequired,
149 | darkTheme: PropTypes.bool.isRequired,
150 | onDarkThemeChange: PropTypes.func.isRequired,
151 | selectedLanguage: PropTypes.string.isRequired,
152 | onSelectedLanguageChange: PropTypes.func.isRequired,
153 | };
154 |
--------------------------------------------------------------------------------
/src/components/drawer/sections/exchange-info/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ListItemHeader, ListItemBox, ListItemIcon } from "../../styled";
3 | import { FormattedMessage } from "react-intl";
4 | import {
5 | faDollarSign,
6 | faClipboardCheck,
7 | faFileSignature,
8 | } from "@fortawesome/free-solid-svg-icons";
9 | import { UndecoratedLink } from "../../../undecorated-link";
10 |
11 | export const ExchangeInfo = () => {
12 | return (
13 | <>
14 |
15 |
16 |
17 |
22 |
23 | {" "}
24 |
25 |
26 |
27 |
32 |
33 | {" "}
34 |
35 |
36 |
37 |
42 |
43 | {" "}
44 |
45 |
46 |
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/drawer/sections/lrc/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ListItemHeader, ListItemBox, ListItemIcon } from "../../styled";
3 | import { FormattedMessage } from "react-intl";
4 | import { faMagnet } from "@fortawesome/free-solid-svg-icons";
5 | import { UndecoratedLink } from "../../../undecorated-link";
6 |
7 | export const Lrc = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
18 |
19 | {" "}
20 |
21 |
22 |
23 | >
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/drawer/sections/settings/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { ListItemHeader } from "../../styled";
4 | import { FormattedMessage } from "react-intl";
5 | import { Flex, Box } from "reflexbox";
6 | import { Switch } from "../../../switch";
7 | import { TextBox } from "./styled";
8 | import { Chip } from "../../../chip";
9 |
10 | export const Settings = ({
11 | darkTheme,
12 | onDarkThemeChange,
13 | selectedLanguage,
14 | onSelectedLanguageChange,
15 | }) => {
16 | const getLanguageChipClickHandler = (language) => () => {
17 | onSelectedLanguageChange(language);
18 | };
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
43 | IT
44 |
45 |
46 |
47 |
51 | EN
52 |
53 |
54 |
55 |
56 | >
57 | );
58 | };
59 |
60 | Settings.propTypes = {
61 | darkTheme: PropTypes.bool.isRequired,
62 | onDarkThemeChange: PropTypes.func.isRequired,
63 | selectedLanguage: PropTypes.string.isRequired,
64 | onSelectedLanguageChange: PropTypes.func.isRequired,
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/drawer/sections/settings/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Box } from "reflexbox";
3 |
4 | export const TextBox = styled(Box)`
5 | color: ${(props) => props.theme.text};
6 | transition: color 0.3s ease;
7 | display: flex;
8 | align-items: center;
9 | font-size: 12px;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/components/drawer/sections/support/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ListItemHeader, ListItemBox, ListItemIcon } from "../../styled";
3 | import { FormattedMessage } from "react-intl";
4 | import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
5 | import { faTelegram, faDiscord } from "@fortawesome/free-brands-svg-icons";
6 | import { UndecoratedLink } from "../../../undecorated-link";
7 |
8 | export const Support = () => {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
19 |
20 | {" "}
21 |
22 |
23 |
24 |
29 |
30 | {" "}
31 |
32 |
33 |
34 |
39 |
40 | {" "}
41 |
42 |
43 |
44 |
49 |
50 | {" "}
51 |
52 |
53 |
54 | >
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/drawer/sections/technical-resources/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ListItemHeader, ListItemBox, ListItemIcon } from "../../styled";
3 | import { FormattedMessage } from "react-intl";
4 | import {
5 | faFile,
6 | faPencilRuler,
7 | faRulerCombined,
8 | faCode,
9 | } from "@fortawesome/free-solid-svg-icons";
10 | import { UndecoratedLink } from "../../../undecorated-link";
11 | import { getEtherscanLink } from "../../../../lightcone/api/localStorgeAPI";
12 | import { CHAIN_ID, DEX_SMART_CONTRACT_ADDRESS } from "../../../../env";
13 |
14 | export const TechnicalResources = () => {
15 | return (
16 | <>
17 |
18 |
19 |
20 |
25 |
26 | {" "}
27 |
28 |
29 |
30 |
37 |
38 | {" "}
39 |
40 |
41 |
42 |
47 |
48 | {" "}
49 |
50 |
51 |
52 |
57 |
58 | {" "}
59 |
60 |
61 |
62 | >
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/src/components/drawer/styled.js:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2 | import { Flex, Box } from "reflexbox";
3 | import styled from "styled-components";
4 |
5 | export const RootFlex = styled(Flex)`
6 | width: 272px;
7 | position: fixed;
8 | top: 0;
9 | bottom: 0;
10 | right: 0;
11 | transition: transform 0.3s ease, background 0.3s ease, box-shadow 0.3s ease;
12 | transform: translateX(${(props) => (props.open ? "0" : "150%")});
13 | background: ${(props) => props.theme.background};
14 | box-shadow: 0px 30px 62px 0px ${(props) => props.theme.shadow};
15 | overflow-y: auto;
16 | `;
17 |
18 | export const HeaderFlex = styled(Flex)`
19 | width: 100%;
20 | height: 60px;
21 | min-height: 60px;
22 | align-items: center;
23 | justify-content: space-between;
24 | padding: 8px 16px;
25 | font-size: 20px;
26 | font-weight: 700;
27 | color: ${(props) => props.theme.text};
28 | transition: color 0.3s ease;
29 | `;
30 |
31 | export const SummaryMessage = styled.span`
32 | font-size: 12px;
33 | color: ${(props) => props.theme.text};
34 | transition: color 0.3s ease;
35 | `;
36 |
37 | export const EllipsizedBox = styled(Box)`
38 | overflow: hidden;
39 | text-overflow: ellipsis;
40 | white-space: nowrap;
41 | `;
42 |
43 | export const Icon = styled(FontAwesomeIcon)`
44 | font-size: 40px;
45 | `;
46 |
47 | export const CloseIcon = styled(FontAwesomeIcon)`
48 | cursor: pointer;
49 | `;
50 |
51 | export const ListItemHeader = styled(Box)`
52 | display: flex;
53 | align-items: center;
54 | width: 100%;
55 | padding: 0 20px;
56 | font-size: 12px;
57 | font-weight: 600;
58 | color: ${(props) => props.theme.textLight};
59 | margin-bottom: 16px;
60 | transition: color 0.3s ease;
61 | `;
62 |
63 | export const ListItemBox = styled(Box)`
64 | cursor: pointer;
65 | display: flex;
66 | align-items: center;
67 | width: 100%;
68 | padding: 0 20px;
69 | min-height: 44px;
70 | height: 44px;
71 | font-size: 12px;
72 | color: ${(props) => props.theme.text};
73 | background: ${(props) => props.theme.background};
74 | :hover {
75 | color: ${(props) => props.theme.textInverted};
76 | background: ${(props) => props.theme.primary};
77 | }
78 | transition: color 0.3s ease, background 0.3s ease;
79 | `;
80 |
81 | export const ListItemIcon = styled(FontAwesomeIcon)`
82 | margin-right: 8px;
83 | `;
84 |
--------------------------------------------------------------------------------
/src/components/full-screen-overlay/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const FullScreenOverlay = styled.div`
4 | position: fixed;
5 | top: 0;
6 | bottom: 0;
7 | left: 0;
8 | right: 0;
9 | background: ${(props) => props.theme.shadow};
10 | opacity: ${(props) => (props.open ? "1" : "0")};
11 | transform: translateY(${(props) => (props.open ? "0" : "150%")});
12 | transition: ${(props) =>
13 | props.open
14 | ? "opacity 0.3s ease"
15 | : "transform 0.3s ease 0.3s, opacity 0.3s ease"},
16 | background 0.3s ease;
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/invalid-chain-id/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Box } from "reflexbox";
3 | import { StyledFontAwesomeIcon, Text } from "./styled";
4 | import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
5 | import { FormattedMessage } from "react-intl";
6 | import { useState } from "react";
7 | import { useEffect } from "react";
8 | import { CHAIN_ID } from "../../env";
9 |
10 | export const InvalidChainId = () => {
11 | const [chainName, setChainName] = useState("");
12 |
13 | useEffect(() => {
14 | let chainName;
15 | switch (CHAIN_ID) {
16 | case 1: {
17 | chainName = "mainnet";
18 | break;
19 | }
20 | case 5: {
21 | chainName = "Goerli";
22 | break;
23 | }
24 | default: {
25 | chainName = "unknown";
26 | }
27 | }
28 | setChainName(chainName);
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/invalid-chain-id/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 |
4 | export const StyledFontAwesomeIcon = styled(FontAwesomeIcon)`
5 | font-size: 60px;
6 | color: ${(props) => props.theme.error};
7 | `;
8 |
9 | export const Text = styled.span`
10 | color: ${(props) => props.theme.error};
11 | `;
12 |
--------------------------------------------------------------------------------
/src/components/layout/footer/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FlexContainer, WarningText } from "./styled";
3 | import { version } from "../../../../package.json";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faGithub } from "@fortawesome/free-brands-svg-icons";
6 | import { UndecoratedLink } from "../../undecorated-link";
7 | import { FormattedMessage } from "react-intl";
8 | import { Box } from "reflexbox";
9 |
10 | export const Footer = () => (
11 |
12 |
13 | Powered by Ethereum & Loopring · v{version} ·{" "}
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/layout/footer/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex } from "reflexbox";
3 |
4 | export const FlexContainer = styled(Flex)`
5 | padding-left: 20px;
6 | padding-right: 20px;
7 | width: 100%;
8 | align-items: center;
9 | justify-content: center;
10 | font-size: 12px;
11 | `;
12 |
13 | export const WarningText = styled.span`
14 | color: ${(props) => props.theme.error};
15 | `;
16 |
--------------------------------------------------------------------------------
/src/components/layout/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Toolbar } from "./toolbar";
3 | import PropTypes from "prop-types";
4 | import { Root, Content } from "./styled";
5 | import { Footer } from "./footer";
6 |
7 | export const Layout = ({ children, onDrawerOpenClick }) => (
8 |
9 |
10 | {children}
11 |
12 |
13 | );
14 |
15 | Layout.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | onDrawerOpenClick: PropTypes.func.isRequired,
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/layout/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex, Box } from "reflexbox";
3 |
4 | export const Root = styled(Flex)`
5 | background: ${(props) => props.theme.background};
6 | color: ${(props) => props.theme.text};
7 | transition: background 0.3s ease, color 0.3s ease;
8 | width: 100%;
9 | height: 100%;
10 | `;
11 |
12 | export const Content = styled(Box)`
13 | width: 100%;
14 | height: 100%;
15 | padding: 24px 0px;
16 | `;
17 |
--------------------------------------------------------------------------------
/src/components/layout/toolbar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { FlexContainer, Logo, PointableIcon } from "./styled";
4 | import { Box, Flex } from "reflexbox";
5 | import logo from "../../../images/logo.png";
6 | import { faBars } from "@fortawesome/free-solid-svg-icons";
7 |
8 | export const Toolbar = ({ onDrawerOpenClick }) => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | Toolbar.propTypes = {
22 | onDrawerOpenClick: PropTypes.func.isRequired,
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/layout/toolbar/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex } from "reflexbox";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 |
5 | export const FlexContainer = styled(Flex)`
6 | padding-left: 20px;
7 | padding-right: 20px;
8 | width: 100%;
9 | height: 60px;
10 | align-items: center;
11 | justify-content: space-between;
12 | `;
13 |
14 | export const Logo = styled.img`
15 | height: 100%;
16 | `;
17 |
18 | export const PointableIcon = styled(FontAwesomeIcon)`
19 | font-size: 24px;
20 | cursor: pointer;
21 | `;
22 |
--------------------------------------------------------------------------------
/src/components/spinner/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled, { keyframes } from "styled-components";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faCircleNotch } from "@fortawesome/free-solid-svg-icons";
6 |
7 | const spin = keyframes`
8 | 0% {
9 | transform: rotate(0deg);
10 | }
11 | 100% {
12 | transform: rotate(360deg);
13 | }
14 | `;
15 |
16 | export const SpinningIcon = styled(FontAwesomeIcon)`
17 | color: ${(props) => {
18 | if (props.variant === "primary") {
19 | return props.theme.primary;
20 | } else if (props.variant === "buttonText") {
21 | return props.theme.textButton;
22 | }
23 | }};
24 | animation: ${spin} 1s linear 0s infinite;
25 | font-size: ${(props) => props.size}px;
26 | `;
27 |
28 | export const Spinner = ({ size, variant }) => {
29 | return ;
30 | };
31 |
32 | Spinner.propTypes = {
33 | size: PropTypes.number.isRequired,
34 | variant: PropTypes.oneOf(["primary", "buttonText"]),
35 | };
36 |
37 | Spinner.defaultProps = {
38 | variant: "primary",
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/switch/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactSwitch from "react-switch";
3 | import { selectedTheme } from "../../views/app";
4 |
5 | export const Switch = ({ value, onChange }) => {
6 | return (
7 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/token-icon/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { StyledImage } from "./styled";
4 | import keepLogo from "../../images/keep.png";
5 | import nestLogo from "../../images/nest.png";
6 | import pNetworkLogo from "../../images/pnetwork.png";
7 |
8 | export const TokenIcon = ({ address, size }) => {
9 | const getIconSource = () => {
10 | switch (address) {
11 | case "0x85Eee30c52B0b379b046Fb0F85F4f3Dc3009aFEC": {
12 | return keepLogo;
13 | }
14 | case "0x04abEdA201850aC0124161F037Efd70c74ddC74C": {
15 | return nestLogo;
16 | }
17 | case "0x89Ab32156e46F46D02ade3FEcbe5Fc4243B9AAeD": {
18 | return pNetworkLogo;
19 | }
20 | default: {
21 | // If the address is ETH, we simply fallback to the WETH icon.
22 | return `https://loopring.io/assets/images/ethereum/assets/${
23 | address === "0x0000000000000000000000000000000000000000"
24 | ? "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
25 | : address
26 | }/logo.png`;
27 | }
28 | }
29 | };
30 |
31 | return ;
32 | };
33 |
34 | TokenIcon.propTypes = {
35 | address: PropTypes.string.isRequired,
36 | size: PropTypes.number.isRequired,
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/token-icon/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledImage = styled.img`
4 | border-radius: 50%;
5 | width: ${props => props.size}px;
6 | height: ${props => props.size}px;
7 | `;
--------------------------------------------------------------------------------
/src/components/token-modal/styled.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 | import { Flex, Box } from "reflexbox";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 |
5 | export const OPEN_CLOSE_ANIMATION_DURATION = 300;
6 |
7 | export const RootFlex = styled(Flex)`
8 | position: fixed;
9 | top: 0;
10 | bottom: 0;
11 | left: 0;
12 | right: 0;
13 | background: transparent;
14 | justify-content: center;
15 | align-items: center;
16 | opacity: ${(props) => (props.open ? "1" : "0")};
17 | transform: translateY(${(props) => (props.open ? "0" : "100%")});
18 | transition: ${(props) =>
19 | props.open
20 | ? `opacity ${OPEN_CLOSE_ANIMATION_DURATION}ms ease`
21 | : `transform ${OPEN_CLOSE_ANIMATION_DURATION}ms ease ${OPEN_CLOSE_ANIMATION_DURATION}ms, opacity ${OPEN_CLOSE_ANIMATION_DURATION}ms ease`};
22 | z-index: 20;
23 | `;
24 |
25 | export const ContentFlex = styled(Flex)`
26 | background: ${(props) => props.theme.background};
27 | transition: color 0.3s ease, background 0.3s ease;
28 | border-radius: 12px;
29 | max-height: 64%;
30 | overflow: auto;
31 | box-shadow: 0px 30px 62px 0px ${(props) => props.theme.shadow};
32 | `;
33 |
34 | export const ListFlex = styled(Flex)`
35 | overflow: auto;
36 | `;
37 |
38 | export const SearchFlex = styled(Flex)`
39 | width: 100%;
40 | height: 48px;
41 | min-height: 48px;
42 | align-items: center;
43 | background: ${(props) => props.theme.background};
44 | padding-top: 4px;
45 | padding-left: 24px;
46 | padding-right: 24px;
47 | font-size: 20px;
48 | color: ${(props) => props.theme.text};
49 | box-shadow: 0px 4px 8px 4px ${(props) => props.theme.background};
50 | transition: color 0.3s ease, background 0.3s ease, box-shadow 0.3s ease;
51 | z-index: 12;
52 | `;
53 |
54 | export const Input = styled.input`
55 | font-size: 16px;
56 | color: ${(props) => props.theme.text};
57 | font-family: Montserrat, sans-serif;
58 | border: none;
59 | background: ${(props) => props.theme.background};
60 | outline: none;
61 | width: 100%;
62 | transition: color 0.3s ease, background 0.3s ease;
63 | ::placeholder {
64 | font-family: Montserrat, sans-serif;
65 | color: ${(props) => props.theme.placeholder};
66 | }
67 | `;
68 |
69 | export const RowFlex = styled(Flex)`
70 | transition: background 0.3s ease, color 0.3s ease;
71 | cursor: pointer;
72 | background: ${(props) =>
73 | props.selected ? props.theme.primary : props.theme.background};
74 | color: ${(props) =>
75 | props.selected ? props.theme.textInverted : props.theme.text};
76 | border-radius: 12px;
77 | ${(props) =>
78 | !props.selected &&
79 | css`
80 | :hover {
81 | background: ${(props) => props.theme.foreground};
82 | }
83 | `};
84 | `;
85 |
86 | export const PointableBox = styled(Box)`
87 | cursor: pointer;
88 | display: flex;
89 | align-items: center;
90 | `;
91 |
92 | export const EmptyIcon = styled(FontAwesomeIcon)`
93 | font-size: 60px;
94 | color: ${(props) => props.theme.error};
95 | `;
96 |
97 | export const EmptyTextBox = styled(Box)`
98 | color: ${(props) => props.theme.error};
99 | `;
100 |
101 | export const PrimaryTextBox = styled(Box)`
102 | font-size: 12px;
103 | color: ${(props) =>
104 | props.selected ? props.theme.textInverted : props.theme.text};
105 | transition: color 0.3s ease;
106 | `;
107 |
108 | export const SecondaryTextBox = styled(Box)`
109 | font-size: 12px;
110 | color: ${(props) =>
111 | props.selected ? props.theme.textInverted : props.theme.textLight};
112 | transition: color 0.3s ease;
113 | `;
114 |
--------------------------------------------------------------------------------
/src/components/token-select/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { TokenIcon } from "../token-icon";
4 | import { Box } from "reflexbox";
5 | import { RootFlex, ChevronIcon, LabelBox } from "./styled";
6 | import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
7 | import { FormattedMessage } from "react-intl";
8 | import { Spinner } from "../spinner";
9 |
10 | export const TokenSelect = ({ token, loading, onClick }) => (
11 |
12 | {loading ? (
13 | <>
14 |
15 |
16 |
17 |
18 |
19 |
20 | >
21 | ) : (
22 | <>
23 | {token ? (
24 | <>
25 |
26 |
30 |
31 | {token.symbol}
32 | >
33 | ) : (
34 |
35 |
36 |
37 | )}
38 |
39 |
40 |
41 | >
42 | )}
43 |
44 | );
45 |
46 | TokenSelect.propTypes = {
47 | token: PropTypes.shape({
48 | address: PropTypes.string,
49 | symbol: PropTypes.string.isRequired,
50 | }),
51 | onClick: PropTypes.func.isRequired,
52 | loading: PropTypes.bool.isRequired,
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/token-select/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex, Box } from "reflexbox";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 |
5 | export const RootFlex = styled(Flex)`
6 | border: solid 1px ${(props) => props.theme.border};
7 | border-radius: 12px;
8 | padding: 2px 8px;
9 | font-size: 20px;
10 | line-height: 20px;
11 | background: ${(props) => props.theme.foreground};
12 | overflow: hidden;
13 | text-overflow: ellipsis;
14 | white-space: nowrap;
15 | transition: background 0.3s ease, border 0.3s ease;
16 | cursor: pointer;
17 | `;
18 |
19 | export const LabelBox = styled(Box)`
20 | font-size: 16px;
21 | color: ${(props) => props.theme.text};
22 | transition: color 0.3s ease;
23 | `;
24 |
25 | export const ChevronIcon = styled(FontAwesomeIcon)`
26 | color: ${(props) => props.theme.primary};
27 | font-size: 12px;
28 | `;
29 |
--------------------------------------------------------------------------------
/src/components/token-specifier/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import PropTypes from "prop-types";
3 | import { RootFlex, HeaderText, Input } from "./styled";
4 | import { Box, Flex } from "reflexbox";
5 | import { FormattedMessage } from "react-intl";
6 | import { TokenSelect } from "../token-select";
7 | import { TokenModal } from "../token-modal";
8 |
9 | export const TokenSpecifier = ({
10 | variant,
11 | amount,
12 | token,
13 | onAmountChange,
14 | onBalancesRefresh,
15 | onTokenChange,
16 | supportedTokens,
17 | balances,
18 | loadingSupportedTokens,
19 | loadingBalances,
20 | loggedIn,
21 | }) => {
22 | const [modalOpen, setModalOpen] = useState(false);
23 |
24 | const handleAmountChange = useCallback(
25 | (wrappedAmount) => {
26 | onAmountChange(wrappedAmount.value);
27 | },
28 | [onAmountChange]
29 | );
30 |
31 | const handleSelectClick = useCallback(() => {
32 | setModalOpen(true);
33 | }, []);
34 |
35 | const handleModalClose = useCallback(() => {
36 | setModalOpen(false);
37 | }, []);
38 |
39 | const handleTokenChange = useCallback(
40 | (token) => {
41 | onTokenChange(token);
42 | },
43 | [onTokenChange]
44 | );
45 |
46 | return (
47 | <>
48 |
49 |
50 |
51 |
52 |
53 |
54 |
62 |
63 |
64 |
69 |
70 |
71 |
72 |
83 | >
84 | );
85 | };
86 |
87 | TokenSpecifier.propTypes = {
88 | variant: PropTypes.oneOf(["from", "to"]),
89 | amount: PropTypes.string.isRequired,
90 | token: PropTypes.object,
91 | changing: PropTypes.bool,
92 | onAmountChange: PropTypes.func.isRequired,
93 | onBalancesRefresh: PropTypes.func.isRequired,
94 | onTokenChange: PropTypes.func.isRequired,
95 | supportedTokens: PropTypes.array.isRequired,
96 | loadingSupportedTokens: PropTypes.bool.isRequired,
97 | loadingBalances: PropTypes.bool.isRequired,
98 | loggedIn: PropTypes.bool.isRequired,
99 | error: PropTypes.bool.isRequired,
100 | };
101 |
--------------------------------------------------------------------------------
/src/components/token-specifier/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex, Box } from "reflexbox";
3 | import NumberFormat from "react-number-format";
4 |
5 | export const RootFlex = styled(Flex)`
6 | background: ${(props) => props.theme.background};
7 | border: solid 1px ${(props) => props.theme.border};
8 | border-radius: 12px;
9 | padding: 12px 12px 8px 12px;
10 | flex-direction: column;
11 | width: 100%;
12 | transition: background 0.3s ease, border 0.3s ease;
13 | `;
14 |
15 | export const HeaderText = styled(Box)`
16 | font-size: 16px;
17 | `;
18 |
19 | export const Input = styled(NumberFormat)`
20 | font-size: 32px;
21 | color: ${(props) => props.theme.text};
22 | font-family: Montserrat, sans-serif;
23 | border: none;
24 | background: ${(props) => props.theme.background};
25 | transition: background 0.3s ease, color 0.3s ease;
26 | outline: none;
27 | line-height: 32px;
28 | width: 100%;
29 | height: 32px;
30 | ::placeholder {
31 | transition: color 0.3s ease;
32 | color: ${(props) => props.theme.placeholder};
33 | }
34 | `;
35 |
--------------------------------------------------------------------------------
/src/components/undecorated-link/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const UndecoratedLink = styled.a`
4 | text-decoration: none;
5 | color: inherit;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/env/index.js:
--------------------------------------------------------------------------------
1 | import env from "@mondora/env";
2 |
3 | const NODE_ENV = env("NODE_ENV", { required: true });
4 | if (NODE_ENV !== "production") {
5 | require("dotenv").config();
6 | }
7 |
8 | export const INFURA_URL = env("REACT_APP_INFURA_URL", {
9 | required: true,
10 | });
11 |
12 | export const CHAIN_ID = env("REACT_APP_CHAIN_ID", {
13 | required: true,
14 | parse: parseInt,
15 | });
16 |
17 | export const LOOPRING_API_HOST = `${
18 | CHAIN_ID === 1 ? "api" : "uat"
19 | }.loopring.io`;
20 |
21 | export const DEX_SMART_CONTRACT_ADDRESS = env(
22 | "REACT_APP_DEX_SMART_CONTRACT_ADDRESS",
23 | { required: true }
24 | );
25 |
--------------------------------------------------------------------------------
/src/i18n/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "placeholder": "foo",
3 | "drawer.wallet.connect.summary": "Connect to your Ethereum account to start using Loopring Swap.",
4 | "drawer.wallet.connect.header.connect": "Connect Wallet",
5 | "drawer.wallet.connect.action.connect": "Connect Wallet",
6 | "drawer.wallet.connect.action.login": "Unlock L2 account",
7 | "drawer.wallet.connect.action.logout": "Lock",
8 | "drawer.wallet.connect.login": "You need to unlock your layer-2 account to start swapping.",
9 | "drawer.wallet.connect.logout": "You have unlocked your layer-2 account. Enjoy our non-custodial & high-performance swapping experience.",
10 | "drawer.wallet.connect.register": "Please activate a Loopring layer-2 account to start swapping.",
11 | "drawer.wallet.connect.action.register": "Activate layer-2 account",
12 | "drawer.wallet.connect.list.header.settings": "Settings",
13 | "drawer.wallet.connect.list.header.lrc": "LRC",
14 | "drawer.wallet.connect.list.header.support": "Support",
15 | "drawer.wallet.connect.list.header.technical.resources": "Technical resources",
16 | "drawer.wallet.connect.list.header.exchange.info": "Exchange info",
17 | "drawer.wallet.connect.list.item.staking": "Staking",
18 | "drawer.wallet.connect.list.item.faq": "FAQ",
19 | "drawer.wallet.connect.list.item.wechat": "WeChat",
20 | "drawer.wallet.connect.list.item.telegram": "Telegram",
21 | "drawer.wallet.connect.list.item.discord": "Discord",
22 | "drawer.wallet.connect.list.item.bug.report": "Bug report",
23 | "drawer.wallet.connect.list.item.exchange.api": "Exchange API",
24 | "drawer.wallet.connect.list.item.smart.contract": "DEX smart contract",
25 | "drawer.wallet.connect.list.item.loopring.protocol": "Loopring protocol",
26 | "drawer.wallet.connect.list.item.fee.schedule": "Fee schedule",
27 | "drawer.wallet.connect.list.item.token.listing": "Token listing",
28 | "drawer.wallet.connect.list.item.terms.privacy": "Terms and privacy",
29 | "drawer.wallet.connect.list.item.source.code": "Source code",
30 | "drawer.wallet.connect.list.settings.theme.dark": "Dark theme",
31 | "token.specifier.from": "From",
32 | "token.specifier.to": "To",
33 | "swapper.action.swap": "Swap",
34 | "swapper.action.connect": "Authenticate",
35 | "token.select.action.select": "Select",
36 | "token.select.loading": "Loading",
37 | "swapper.price": "Price",
38 | "swapper.slippage": "Slippage",
39 | "swapper.fee": "Fees",
40 | "swapper.error.amount.liquidity": "Insufficient liquidity",
41 | "swapper.error.amount.balance": "Insufficient balance",
42 | "swapper.error.amount.minimum": "Swap volume is too low",
43 | "swapper.error.amount.maximum": "Swap volume is too high",
44 | "token.modal.empty": "Nothing matches your search...",
45 | "token.modal.searchbar.placeholder": "Search...",
46 | "swap.success": "The swap was correctly submitted. It should soon be reflected in your balances.",
47 | "drawer.wallet.connect.list.settings.language": "Language",
48 | "footer.warning": "Source code not audited by Loopring, use at your own risk.",
49 | "invalid.chain.id.message": "You're currently on an invalid network. Please switch to {chainName}",
50 |
51 | "success.register": "The registration transaction was correctly broadcast to the network. On confirmation, in a few minutes, you'll be able to use Loopring Swap.",
52 | "warn.account.not.found": "Seems like this account is not yet on our platform. Please register on https://loopring.io first.",
53 | "warn.register.existing.account": "Seems like you already own a Loopring account.",
54 | "error.login": "There was an error logging in. Please try again later.",
55 | "error.supported.tokens": "There was an error fetching the supported tokens. Please try again later.",
56 | "error.supported.markets": "There was an error fetching the supported markets. Please try again later.",
57 | "error.user.balances": "There was an error fetching your balances. Please try again later.",
58 | "error.swap.data": "There was an error fetching the current market swap data. Please try again later.",
59 | "error.swap.size": "There's currently not enough liquidity to process the swap. Please try again later.",
60 | "error.swap": "An error occurred performing the swap. Please try again later.",
61 | "error.auth.status": "There was an error checking your auth status. Please try again later.",
62 | "error.register": "There was an error registering. Please try again later."
63 | }
64 |
--------------------------------------------------------------------------------
/src/i18n/messages/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "placeholder": "foo",
3 | "drawer.wallet.connect.summary": "Collega il tuo portafoglio Ethereum per iniziare ad usare Loopring Swap.",
4 | "drawer.wallet.connect.header.connect": "Collega portafoglio",
5 | "drawer.wallet.connect.action.connect": "Collega portafoglio",
6 | "drawer.wallet.connect.action.login": "Autenticati",
7 | "drawer.wallet.connect.action.logout": "Logout",
8 | "drawer.wallet.connect.login": "Autenticati per iniziare a swappare.",
9 | "drawer.wallet.connect.logout": "Ti sei autenticato. Goditi la nostra esperienza di swapping non-custodial e dalle alte prestazioni.",
10 | "drawer.wallet.connect.register": "Per favore, clicca il bottone qui sotto per registrarti a Loopring Swap.",
11 | "drawer.wallet.connect.action.register": "Registrati",
12 | "drawer.wallet.connect.list.header.settings": "Impostazioni",
13 | "drawer.wallet.connect.list.header.lrc": "LRC",
14 | "drawer.wallet.connect.list.header.support": "Supporto",
15 | "drawer.wallet.connect.list.header.technical.resources": "Risorse tecniche",
16 | "drawer.wallet.connect.list.header.exchange.info": "Informazioni exchange",
17 | "drawer.wallet.connect.list.item.staking": "Staking",
18 | "drawer.wallet.connect.list.item.faq": "FAQ",
19 | "drawer.wallet.connect.list.item.wechat": "WeChat",
20 | "drawer.wallet.connect.list.item.telegram": "Telegram",
21 | "drawer.wallet.connect.list.item.discord": "Discord",
22 | "drawer.wallet.connect.list.item.bug.report": "Segnala un bug",
23 | "drawer.wallet.connect.list.item.exchange.api": "API exchange",
24 | "drawer.wallet.connect.list.item.smart.contract": "Smart contract DEX",
25 | "drawer.wallet.connect.list.item.loopring.protocol": "Protocollo Loopring",
26 | "drawer.wallet.connect.list.item.fee.schedule": "Commissioni",
27 | "drawer.wallet.connect.list.item.token.listing": "Quotazione token",
28 | "drawer.wallet.connect.list.item.terms.privacy": "Privacy policy",
29 | "drawer.wallet.connect.list.item.source.code": "Codice sorgente",
30 | "drawer.wallet.connect.list.settings.theme.dark": "Tema scuro",
31 | "token.specifier.from": "Da",
32 | "token.specifier.to": "A",
33 | "swapper.action.swap": "Swap",
34 | "swapper.action.connect": "Autenticati",
35 | "token.select.action.select": "Seleziona",
36 | "token.select.loading": "Caricamento",
37 | "swapper.price": "Prezzo",
38 | "swapper.slippage": "Slippage",
39 | "swapper.fee": "Commissioni",
40 | "swapper.error.amount.liquidity": "Liquidità del pair insufficiente",
41 | "swapper.error.amount.balance": "Bilancio insufficiente",
42 | "swapper.error.amount.minimum": "Il volume dello swap è troppo basso",
43 | "swapper.error.amount.maximum": "Il volume dello swap è troppo alto",
44 | "token.modal.empty": "Nessun risultato corrisponde alla tua ricerca...",
45 | "token.modal.searchbar.placeholder": "Cerca...",
46 | "swap.success": "Lo swap è stato sottoposto correttamente. Tra poco dovrebbe essere riflettuto nei tuoi bilanci.",
47 | "drawer.wallet.connect.list.settings.language": "Lingua",
48 | "footer.warning": "Codice sorgente non verificato da Loopring, usa a tuo rischio.",
49 | "invalid.chain.id.message": "La rete Ethereum non è valida. È richiesto essere su {chainName}",
50 |
51 | "success.register": "La transazione di registrazione è stata correttamente trasmessa. Una volta confermata, in alcuni minuti, potrai utilizzare Loopring Swap.",
52 | "warn.account.not.found": "Sembra che questo account non sia ancora sulla nostra piattaforma. sRegistrati su https://loopring.io",
53 | "warn.register.existing.account": "Sembra che tu possegga già un account Loopring.",
54 | "error.login": "Si è verificato un errore durante l'autenticatione. Per favore riprova più tardi.",
55 | "error.supported.tokens": "Si è verificato un errore nel recuperare i token supportati. Per favore riprova più tardi.",
56 | "error.supported.markets": "Si è verificato un errore nel recuperare i mercati supportati. Per favore riprova più tardi.",
57 | "error.user.balances": "Si è verificato un errore nel recuperare i bilanci. Per favore riprova più tardi.",
58 | "error.swap.data": "Si è verificato un errore nel recuperare i dati relativi allo swap. Per favore riprova più tardi.",
59 | "error.swap.size": "Attualmente non c'è abbastanza liquidità per effettuare lo swap. Per favore riprova più tardi.",
60 | "error.swap": "Si è verificato un errore durante l'operazione di swap. Per favore riprova più tardi.",
61 | "error.auth.status": "Si è verificato un errore nel verificare is tuo stato di autenticazione. Per favore riprova più tardi.",
62 | "error.register": "Si è verificato un errore nella registrazione. Per favore riprova più tardi."
63 | }
64 |
--------------------------------------------------------------------------------
/src/images/keep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/src/images/keep.png
--------------------------------------------------------------------------------
/src/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/src/images/logo.png
--------------------------------------------------------------------------------
/src/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/nest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/src/images/nest.png
--------------------------------------------------------------------------------
/src/images/pnetwork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loopring/loopring-swap/b55803ec0105a7f22abcbcdf04a77276e03e08b8/src/images/pnetwork.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { App } from "./views/app";
4 | import { Provider } from "react-redux";
5 | import { createStore, compose, applyMiddleware } from "redux";
6 | import thunk from "redux-thunk";
7 | import { reducers } from "./reducers";
8 |
9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
10 | const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
11 |
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | document.getElementById("root")
17 | );
18 |
--------------------------------------------------------------------------------
/src/lightcone/api/CommissionRewardAPI.js:
--------------------------------------------------------------------------------
1 | import config from "../config";
2 | import request from "../common/request";
3 |
4 | export async function getCommissionRewardRank(
5 | rewardType,
6 | tokenSymbol,
7 | top,
8 | tokens
9 | ) {
10 | const token = config.getTokenBySymbol(tokenSymbol, tokens);
11 | const params = {
12 | tokenId: token.tokenId,
13 | rewardType,
14 | top,
15 | };
16 |
17 | const headers = {};
18 |
19 | const response = await request({
20 | method: "GET",
21 | url: "/api/v2/sidecar/commissionRewardRank",
22 | headers: headers,
23 | params,
24 | });
25 |
26 | const rewards = response["data"];
27 |
28 | let updatedRewards = [];
29 | for (let i = 0; i < rewards.length; i = i + 1) {
30 | const reward = rewards[i];
31 | let updatedReward = { ...reward };
32 | updatedReward["rank"] = Number(reward["rank"]) + 1;
33 | updatedReward["reward"] = config.fromWEI(
34 | token.symbol,
35 | reward["reward"],
36 | tokens
37 | );
38 | updatedRewards.push(updatedReward);
39 | }
40 |
41 | return updatedRewards;
42 | }
43 |
--------------------------------------------------------------------------------
/src/lightcone/api/LightconeAPI.js:
--------------------------------------------------------------------------------
1 | import { signSetReferrer } from "../sign/exchange";
2 | import config from "../config";
3 | import request from "../common/request";
4 |
5 | const dropTrailingZeroes = (num) => {
6 | const numString = String(num);
7 | if (numString) {
8 | const numTrailingZeroes = countTrailingZeroes(numString);
9 | const dropedZeroes = numString.substring(
10 | 0,
11 | numString.length - numTrailingZeroes
12 | );
13 | if (dropedZeroes.endsWith(".")) {
14 | return dropedZeroes.substring(0, dropedZeroes.length - 1);
15 | } else {
16 | return dropedZeroes;
17 | }
18 | } else {
19 | return numString;
20 | }
21 | };
22 |
23 | const countTrailingZeroes = (numString) => {
24 | let numZeroes = 0;
25 | for (let digit of numString.split("").reverse()) {
26 | if (digit === "0" || digit === 0) {
27 | numZeroes = numZeroes + 1;
28 | } else {
29 | return numZeroes;
30 | }
31 | }
32 | return numZeroes;
33 | };
34 |
35 | export function getRefreshDurationInMillionSeconds() {
36 | return 30 * 1000;
37 | }
38 |
39 | export function getApiDocsURL() {
40 | return `${config.getServer()}/docs/`;
41 | }
42 |
43 | export async function getApiKey(data, signed) {
44 | const signature = signed.Rx + "," + signed.Ry + "," + signed.s;
45 | const headers = {
46 | "X-API-SIG": signature,
47 | };
48 |
49 | const response = await request({
50 | method: "GET",
51 | url: "/api/v2/apiKey",
52 | headers: headers,
53 | params: data,
54 | });
55 | return response["data"];
56 | }
57 |
58 | export async function applyApiKey(data, signed) {
59 | const signature = signed.Rx + "," + signed.Ry + "," + signed.s;
60 | const headers = {
61 | "X-API-SIG": signature,
62 | };
63 | const response = await request({
64 | method: "POST",
65 | url: "/api/v2/apiKey",
66 | headers: headers,
67 | data,
68 | });
69 |
70 | return response["data"];
71 | }
72 |
73 | export async function getDexNonce(data) {
74 | return await request({
75 | method: "GET",
76 | url: "/api/v2/dexNonce",
77 | param: data,
78 | });
79 | }
80 |
81 | export async function submitOrderToLightcone(data, apiKey) {
82 | const headers = {
83 | "X-API-KEY": apiKey,
84 | };
85 | return await request({
86 | method: "POST",
87 | url: "/api/v2/order",
88 | headers: headers,
89 | data,
90 | });
91 | }
92 |
93 | // Return order id
94 | export async function getOrderId(accountId, tokenSId, apiKey) {
95 | const params = {
96 | accountId: accountId,
97 | tokenSId: tokenSId,
98 | };
99 | const headers = {
100 | "X-API-KEY": apiKey,
101 | };
102 | const response = await request({
103 | method: "GET",
104 | url: "/api/v2/orderId",
105 | headers: headers,
106 | params,
107 | });
108 |
109 | return response["data"];
110 | }
111 |
112 | export async function lightconeGetAccount(owner) {
113 | const params = {
114 | owner,
115 | };
116 |
117 | const response = await request({
118 | method: "GET",
119 | url: "/api/v2/account",
120 | params,
121 | });
122 |
123 | return response["data"];
124 | }
125 |
126 | export async function getTimestamp() {
127 | return await request({
128 | method: "POST",
129 | url: "/api/v2/timestamp",
130 | });
131 | }
132 |
133 | export async function getTrade(market, limit) {
134 | const params = {
135 | market,
136 | limit,
137 | };
138 | const response = await request({
139 | method: "GET",
140 | url: "/api/v2/trade",
141 | params,
142 | });
143 |
144 | return response["data"]["trades"];
145 | }
146 |
147 | export function arrToTrade(arr) {
148 | return {
149 | timestamp: Number(arr[0]),
150 | tradeId: Number(arr[1]),
151 | side: arr[2],
152 | size: arr[3],
153 | price: arr[4],
154 | market: arr[5],
155 | fee: arr[6],
156 | };
157 | }
158 |
159 | export async function getDepositHistory(
160 | accountId,
161 | tokenSymbol,
162 | limit,
163 | offset,
164 | apiKey,
165 | tokens
166 | ) {
167 | const params = {
168 | accountId,
169 | limit,
170 | offset,
171 | allType: true,
172 | start: 0,
173 | end: Date.now() * 1000,
174 | };
175 |
176 | if (typeof tokenSymbol !== "undefined") {
177 | params.tokenSymbol = tokenSymbol;
178 | }
179 |
180 | const headers = {
181 | "X-API-KEY": apiKey,
182 | };
183 |
184 | const response = await request({
185 | method: "GET",
186 | url: "/api/v2/user/deposits",
187 | headers: headers,
188 | params,
189 | });
190 |
191 | const data = response["data"];
192 | const totalNum = data["totalNum"];
193 | const transactions = data["transactions"];
194 | const updatedTransactions = mapTransactions(transactions, tokens);
195 | return {
196 | totalNum,
197 | transactions: updatedTransactions,
198 | limit,
199 | offset,
200 | };
201 | }
202 |
203 | export function mapAmountInUI(baseToken, amount, tokens) {
204 | let amountInUI = config.fromWEI(baseToken.symbol, amount, tokens);
205 | if (parseFloat(amountInUI) === 0) {
206 | let amountInDecimals = config.fromWEI(
207 | baseToken.symbol,
208 | amount,
209 | tokens,
210 | {
211 | precision: baseToken.decimals,
212 | }
213 | );
214 | amountInDecimals = dropTrailingZeroes(amountInDecimals);
215 | if (parseFloat(amountInDecimals) !== 0) {
216 | return amountInDecimals;
217 | }
218 | }
219 |
220 | return amountInUI;
221 | }
222 |
223 | function mapTransactions(transactions, tokens) {
224 | let updatedTransactions = [];
225 | for (let i = 0; i < transactions.length; i = i + 1) {
226 | let transaction = transactions[i];
227 |
228 | const baseToken = config.getTokenBySymbol(transaction.symbol, tokens);
229 | let amountInUI = mapAmountInUI(baseToken, transaction.amount, tokens);
230 | let realAmountInUI = transaction.realAmount
231 | ? mapAmountInUI(baseToken, transaction.realAmount, tokens)
232 | : "";
233 | // Why this is feeAmount?
234 | const feeInUI = config.fromWEI("ETH", transaction.feeAmount, tokens);
235 |
236 | const txHashInUI =
237 | transaction.txHash.substring(0, 7) +
238 | "..." +
239 | transaction.txHash.slice(-7);
240 |
241 | const distributeHashInUI = transaction.distributeHash
242 | ? transaction.distributeHash.substring(0, 7) +
243 | "..." +
244 | transaction.distributeHash.slice(-7)
245 | : "";
246 |
247 | const updatedTransaction = {
248 | ...transaction,
249 | tokenName: baseToken.name,
250 | amountInUI,
251 | realAmountInUI,
252 | feeInUI,
253 | txHashInUI,
254 | distributeHashInUI,
255 | };
256 | updatedTransactions.push(updatedTransaction);
257 | }
258 | return updatedTransactions;
259 | }
260 |
261 | export async function getWithdrawalHistory(
262 | accountId,
263 | tokenSymbol,
264 | limit,
265 | offset,
266 | apiKey,
267 | tokens
268 | ) {
269 | const params = {
270 | accountId,
271 | limit,
272 | offset,
273 | start: 0,
274 | end: Date.now() * 1000,
275 | };
276 |
277 | if (typeof tokenSymbol !== "undefined") {
278 | params.tokenSymbol = tokenSymbol;
279 | }
280 |
281 | const headers = {
282 | "X-API-KEY": apiKey,
283 | };
284 |
285 | const response = await request({
286 | method: "GET",
287 | url: "/api/v2/user/withdrawals",
288 | headers: headers,
289 | params,
290 | });
291 |
292 | const data = response["data"];
293 | const totalNum = data["totalNum"];
294 | const transactions = data["transactions"];
295 | const updatedTransactions = mapTransactions(transactions, tokens);
296 | return {
297 | totalNum,
298 | transactions: updatedTransactions,
299 | limit,
300 | offset,
301 | };
302 | }
303 |
304 | export async function getTicker(markets, tokens) {
305 | const params = {
306 | market: markets.reduce((acc, cur) => acc + "," + cur),
307 | };
308 |
309 | const response = await request({
310 | method: "GET",
311 | url: "/api/v2/ticker",
312 | params,
313 | });
314 | return mapTicker(
315 | response["data"].map((arr) => arrToTicker(arr)),
316 | tokens
317 | );
318 | }
319 |
320 | function mapTicker(tickers, configTokens) {
321 | let updatedTickers = [];
322 | for (let i = 0; i < tickers.length; i = i + 1) {
323 | const ticker = tickers[i];
324 |
325 | const tokens = ticker.market.split("-");
326 | const baseToken = tokens[0];
327 | const quoteToken = tokens[1];
328 | const open = parseFloat(ticker["open"]);
329 | const close = parseFloat(ticker["close"]);
330 | let percentChange24h = (((close - open) / open) * 100).toFixed(2);
331 | percentChange24h =
332 | percentChange24h !== "NaN" ? percentChange24h : "0.00";
333 | if (close - open > 0) {
334 | percentChange24h = `+ ${percentChange24h}`;
335 | }
336 |
337 | const updatedTicker = {
338 | ...ticker,
339 | percentChange24h,
340 | size: config.fromWEI(baseToken, ticker.size, configTokens),
341 | volume: config.fromWEI(quoteToken, ticker.volume, configTokens, {
342 | precision: 2,
343 | }),
344 | };
345 | updatedTickers.push(updatedTicker);
346 | }
347 | return updatedTickers;
348 | }
349 |
350 | export function arrToTicker(arr) {
351 | return {
352 | market: arr[0],
353 | timestamp: Number(arr[1]),
354 | size: arr[2],
355 | volume: arr[3],
356 | open: arr[4],
357 | high: arr[5],
358 | low: arr[6],
359 | close: arr[7],
360 | count: Number(arr[8]),
361 | bid: arr[9],
362 | ask: arr[10],
363 | };
364 | }
365 |
366 | export async function getExchangeInfo() {
367 | const data = {};
368 |
369 | const response = await request({
370 | method: "GET",
371 | url: "/api/v2/exchange/info",
372 | data,
373 | });
374 |
375 | return response["data"];
376 | }
377 |
378 | export async function setRefer(info, keyPair) {
379 | const signed = signSetReferrer(info, keyPair);
380 | const signature = signed.Rx + "," + signed.Ry + "," + signed.s;
381 | let data;
382 | if (info.referrer) {
383 | data = {
384 | address: info.address,
385 | referrer: info.referrer,
386 | publicKeyX: keyPair.publicKeyX,
387 | publicKeyY: keyPair.publicKeyY,
388 | };
389 | } else {
390 | data = {
391 | address: info.address,
392 | promotionCode: info.promotionCode,
393 | publicKeyX: keyPair.publicKeyX,
394 | publicKeyY: keyPair.publicKeyY,
395 | };
396 | }
397 |
398 | const headers = {
399 | "X-API-SIG": signature,
400 | };
401 |
402 | const response = await request({
403 | method: "POST",
404 | url: "/api/v2/refer",
405 | headers: headers,
406 | data,
407 | });
408 |
409 | return response["data"];
410 | }
411 |
--------------------------------------------------------------------------------
/src/lightcone/api/LiquidityMiningAPI.js:
--------------------------------------------------------------------------------
1 | import config from "../config";
2 | import request from "../common/request";
3 |
4 | export async function getLiquidityMiningTotal(accountId, apiKey, tokens) {
5 | const params = {
6 | accountId,
7 | };
8 |
9 | const headers = {
10 | "X-API-KEY": apiKey,
11 | };
12 |
13 | const response = await request({
14 | method: "GET",
15 | url: "/api/v2/sidecar/liquidityMiningTotal",
16 | headers: headers,
17 | params,
18 | });
19 |
20 | const rewards = response["data"];
21 |
22 | let updatedRewards = [];
23 | for (let i = 0; i < rewards.length; i = i + 1) {
24 | const reward = rewards[i];
25 | let updatedReward = { ...reward };
26 | const tokenId = updatedReward["tokenId"];
27 | const token = config.getTokenByTokenId(tokenId, tokens);
28 | updatedReward["amount"] = config.fromWEI(
29 | token.symbol,
30 | reward["amount"],
31 | tokens
32 | );
33 | updatedReward["issued"] = config.fromWEI(
34 | token.symbol,
35 | reward["issued"],
36 | tokens
37 | );
38 | updatedReward["token"] = token;
39 | updatedRewards.push(updatedReward);
40 | }
41 |
42 | return updatedRewards;
43 | }
44 |
45 | export async function getLiquidityMining(market, accountId, apiKey, tokens) {
46 | const params = {
47 | market,
48 | accountId,
49 | size: 120,
50 | };
51 |
52 | const headers = {
53 | "X-API-KEY": apiKey,
54 | };
55 |
56 | const response = await request({
57 | method: "GET",
58 | url: "/api/v2/sidecar/liquidityMining",
59 | headers: headers,
60 | params,
61 | });
62 |
63 | const rewards = response["data"];
64 |
65 | let updatedRewards = [];
66 | for (let i = 0; i < rewards.length; i = i + 1) {
67 | try {
68 | const reward = rewards[i];
69 | let updatedReward = { ...reward };
70 | updatedReward["rank"] = 0;
71 | const tokenId = updatedReward["tokenId"];
72 | const token = config.getTokenByTokenId(tokenId, tokens);
73 | updatedReward["amount"] = config.fromWEI(
74 | token.symbol,
75 | reward["amount"],
76 | tokens
77 | );
78 | updatedRewards.push(updatedReward);
79 | } catch (error) {
80 | continue;
81 | }
82 | }
83 |
84 | return updatedRewards;
85 | }
86 |
87 | export async function getLiquidityMiningRank(market, top, tokens) {
88 | const params = {
89 | market,
90 | top,
91 | };
92 |
93 | const headers = {};
94 |
95 | const response = await request({
96 | method: "GET",
97 | url: "/api/v2/sidecar/liquidityMiningRank",
98 | headers: headers,
99 | params,
100 | });
101 |
102 | const rewards = response["data"];
103 |
104 | let updatedRewards = [];
105 | for (let i = 0; i < rewards.length; i = i + 1) {
106 | const reward = rewards[i];
107 | let updatedReward = { ...reward };
108 | updatedReward["rank"] = Number(reward["rank"]) + 1;
109 | const tokenId = updatedReward["tokenId"];
110 | const token = config.getTokenByTokenId(tokenId, tokens);
111 | updatedReward["reward"] = config.fromWEI(
112 | token.symbol,
113 | reward["reward"],
114 | tokens
115 | );
116 | updatedRewards.push(updatedReward);
117 | }
118 |
119 | return updatedRewards;
120 | }
121 |
--------------------------------------------------------------------------------
/src/lightcone/api/localStorgeAPI.js:
--------------------------------------------------------------------------------
1 | function getAccounts() {
2 | const accountStr = localStorage.getItem("accounts");
3 | return accountStr ? new Map(JSON.parse(accountStr)) : new Map();
4 | }
5 |
6 | const SESSION_TIMEOUT_MINUTES = 60;
7 |
8 | export function getAccountFromLocal(address) {
9 | const now = Date.now();
10 | const accounts = getAccounts();
11 | const account = accounts.get(address.toLowerCase());
12 |
13 | // If it's unknown wallet type, remove
14 | const unknownWalletType = !(
15 | account &&
16 | account.walletType &&
17 | (account.walletType === "MetaMask" ||
18 | account.walletType === "WalletConnect")
19 | );
20 | if (
21 | !account ||
22 | !account.time ||
23 | now >= account.time + SESSION_TIMEOUT_MINUTES * 60 * 1000 ||
24 | unknownWalletType
25 | ) {
26 | removeAccountFromLocal(address);
27 | removeAccountKeyFromSession(address);
28 | return null;
29 | } else {
30 | delete account.time;
31 | account.accountKey = getAccountKeyFromSession(address);
32 | account.keyNonce = account.keyNonce ? account.keyNonce : 0;
33 | delete account.accountKeyCipher;
34 | return account;
35 | }
36 | }
37 |
38 | export function saveAccountToLocal(account) {
39 | try {
40 | const localAccount = { ...account };
41 | const accounts = getAccounts();
42 | saveAccountKeyToSession(localAccount.address, localAccount.accountKey);
43 | delete localAccount.accountKey;
44 | delete localAccount.accountKeyCipher;
45 |
46 | accounts.set(localAccount.address.toLowerCase(), {
47 | ...localAccount,
48 | time: Date.now(),
49 | });
50 | localStorage.setItem("accounts", JSON.stringify([...accounts]));
51 | } catch (e) {}
52 | }
53 |
54 | export function removeAccountFromLocal(address) {
55 | if (address) {
56 | const accounts = getAccounts();
57 | // Convert address to lower case
58 | accounts.delete(address.toLowerCase());
59 | localStorage.setItem("accounts", JSON.stringify([...accounts]));
60 | removeAccountKeyFromSession(address);
61 | }
62 | }
63 |
64 | export function getAccountKeys() {
65 | const keyStr = sessionStorage.getItem("accountKeys");
66 | return keyStr ? new Map(JSON.parse(keyStr)) : new Map();
67 | }
68 |
69 | export function saveAccountKeyToSession(address, accountKey) {
70 | try {
71 | const keys = getAccountKeys();
72 | keys.set(address.toLowerCase(), {
73 | accountKey,
74 | time: Date.now(),
75 | });
76 | sessionStorage.setItem("accountKeys", JSON.stringify([...keys]));
77 | } catch (e) {}
78 | }
79 |
80 | export function removeAccountKeyFromSession(address) {
81 | if (address) {
82 | const keys = getAccountKeys();
83 | keys.delete(address.toLowerCase());
84 | sessionStorage.setItem("accountKeys", JSON.stringify([...keys]));
85 | }
86 | }
87 |
88 | export function getAccountKeyFromSession(address) {
89 | const now = Date.now();
90 | const keys = getAccountKeys();
91 | const key = keys.get(address.toLowerCase());
92 |
93 | if (
94 | !key ||
95 | !key.time ||
96 | now >= key.time + SESSION_TIMEOUT_MINUTES * 60 * 1000
97 | ) {
98 | removeAccountKeyFromSession(address);
99 | return null;
100 | } else {
101 | return key.accountKey;
102 | }
103 | }
104 |
105 | export function saveShowAllOpenOrders() {
106 | localStorage.setItem("showAllOpenOrders", "true");
107 | }
108 |
109 | export function getShowAllOpenOrders() {
110 | return localStorage.getItem("showAllOpenOrders");
111 | }
112 |
113 | export function removeShowAllOpenOrders() {
114 | localStorage.removeItem("showAllOpenOrders");
115 | }
116 |
117 | export function saveHideLowBalanceAssets() {
118 | localStorage.setItem("hideLowBalanceAssets", "true");
119 | }
120 |
121 | export function getHideLowBalanceAssets() {
122 | return localStorage.getItem("hideLowBalanceAssets");
123 | }
124 |
125 | export function removeHideLowBalanceAssets() {
126 | localStorage.removeItem("hideLowBalanceAssets");
127 | }
128 |
129 | export function saveWalletType(walletType) {
130 | localStorage.setItem("walletType", walletType);
131 | }
132 |
133 | export function getWalletType() {
134 | return localStorage.getItem("walletType");
135 | }
136 |
137 | export function removeWalletType() {
138 | localStorage.removeItem("walletType");
139 | }
140 |
141 | export function saveLoginRecord() {
142 | localStorage.setItem("hasRecord", "true");
143 | }
144 |
145 | export function getLoginRecord() {
146 | return localStorage.getItem("hasRecord");
147 | }
148 |
149 | export function removeLoginRecord() {
150 | localStorage.removeItem("hasRecord");
151 | }
152 |
153 | function getUpdateRecords() {
154 | const recordStr = localStorage.getItem("updateRecords");
155 | return recordStr ? new Map(JSON.parse(recordStr)) : new Map();
156 | }
157 |
158 | export function getUpdateRecordByAddress(address) {
159 | const records = getUpdateRecords();
160 | return records.get(address.toLowerCase());
161 | }
162 |
163 | export function saveUpdateRecord(record) {
164 | const records = getUpdateRecords();
165 | records.set(record.address.toLowerCase(), record);
166 | localStorage.setItem("updateRecords", JSON.stringify([...records]));
167 | }
168 |
169 | export function removeUpdateRecord(address) {
170 | const records = getUpdateRecords();
171 | records.delete(address.toLowerCase());
172 | localStorage.setItem("updateRecords", JSON.stringify([...records]));
173 | }
174 |
175 | export function saveLanguage(value) {
176 | if (value === "en" || value === "zh") {
177 | localStorage.setItem("language", value);
178 | }
179 | }
180 |
181 | export function getLanguage() {
182 | let language = localStorage.getItem("language") || getLanguageFromBrowser();
183 | if (language === "zh") {
184 | language = "zh";
185 | } else {
186 | language = "en";
187 | }
188 | return language;
189 | }
190 |
191 | export function saveCurrency(value) {
192 | if (value === "USD" || value === "CNY") {
193 | localStorage.setItem("currency", value);
194 | }
195 | }
196 |
197 | export function getCurrency() {
198 | let currency = localStorage.getItem("currency");
199 | if (currency === "USD" || currency === "CNY") {
200 | return currency;
201 | } else {
202 | return getLanguage() === "zh" ? "CNY" : "USD";
203 | }
204 | }
205 |
206 | export function saveLastTradePage(lastTradePage) {
207 | localStorage.setItem("lastTradePage", lastTradePage);
208 | }
209 |
210 | export function getLastTradePage() {
211 | const lastTradePage = localStorage.getItem("lastTradePage");
212 | if (lastTradePage) {
213 | return lastTradePage;
214 | } else {
215 | return "LRC-USDT";
216 | }
217 | }
218 |
219 | export function saveLastOrderPage(lastOrderPage) {
220 | localStorage.setItem("lastOrderPage", lastOrderPage);
221 | }
222 |
223 | export function getLastOrderPage() {
224 | const lastOrderPage = localStorage.getItem("lastOrderPage");
225 | if (lastOrderPage) {
226 | return lastOrderPage;
227 | } else {
228 | return "open-orders";
229 | }
230 | }
231 |
232 | export function saveLastAccountPage(lastAccountPage) {
233 | localStorage.setItem("lastAccountPage", lastAccountPage);
234 | }
235 |
236 | export function getLastAccountPage() {
237 | const lastAccountPage = localStorage.getItem("lastAccountPage");
238 | if (lastAccountPage) {
239 | return lastAccountPage;
240 | } else {
241 | return "balances";
242 | }
243 | }
244 |
245 | export function saveThemeName(themeName) {
246 | localStorage.setItem("theme", themeName);
247 | }
248 |
249 | export function getReferralId() {
250 | const reg = new RegExp("^([1-9][0-9]{0,6})$");
251 | const str = localStorage.getItem("referral");
252 | if (str && !Number.isNaN(str) && reg.test(str)) {
253 | return str;
254 | } else return null;
255 | }
256 |
257 | export function setReferralId(id) {
258 | localStorage.setItem("referral", id);
259 | }
260 |
261 | export function removeReferralId() {
262 | localStorage.removeItem("referral");
263 | }
264 |
265 | export function getThemeName() {
266 | const themeName = localStorage.getItem("theme");
267 | if (themeName !== "dark" && themeName !== "light") {
268 | return "auto";
269 | } else return themeName;
270 | }
271 |
272 | function getLanguageFromBrowser() {
273 | var language =
274 | (window.navigator.languages && window.navigator.languages[0]) || // Chrome / Firefox
275 | window.navigator.language || // All browsers
276 | window.navigator.userLanguage; // IE <= 10
277 | let shortLang = language;
278 | if (shortLang.indexOf("-") !== -1) {
279 | shortLang = shortLang.split("-")[0];
280 | } else if (shortLang.indexOf("_") !== -1) {
281 | shortLang = shortLang.split("_")[0];
282 | }
283 | return shortLang;
284 | }
285 |
286 | export function getEtherscanLink(chainId) {
287 | const lan = getLanguage();
288 |
289 | switch (chainId) {
290 | case 1:
291 | if (lan === "zh") {
292 | return "https://cn.etherscan.com";
293 | } else {
294 | return "https://etherscan.io";
295 | }
296 | case 5:
297 | return "https://goerli.etherscan.io";
298 | default:
299 | return "";
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/allowances/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 | import config from "../../../config";
3 |
4 | export async function getAllowance(owner, symbol, tokens) {
5 | let token = config.getTokenBySymbol(symbol, tokens);
6 | let tokenAddress = token.address;
7 | const params = {
8 | owner,
9 | token: tokenAddress,
10 | };
11 |
12 | const response = await request({
13 | method: "GET",
14 | url: "/api/v2/allowances",
15 | params,
16 | });
17 |
18 | return response["data"];
19 | }
20 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/allowances/index.js:
--------------------------------------------------------------------------------
1 | import { getAllowance } from "./get";
2 |
3 | export { getAllowance };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/balances/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | // tokenIds is skip to get all tokens
4 | export async function getBalances(accountId, apiKey, skip, limit) {
5 | const params = {
6 | accountId,
7 | skip,
8 | limit,
9 | };
10 | const headers = {
11 | "X-API-KEY": apiKey,
12 | };
13 | const response = await request({
14 | method: "GET",
15 | url: "/api/v2/user/balances",
16 | headers: headers,
17 | params,
18 | });
19 |
20 | return response["data"];
21 | }
22 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/balances/index.js:
--------------------------------------------------------------------------------
1 | import { getBalances } from "./get";
2 |
3 | export { getBalances };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/depth/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 | import BigNumber from "bignumber.js";
3 | import config from "../../../config";
4 |
5 | export async function getDepth(market, level, limit, configTokens) {
6 | const params = {
7 | market,
8 | level,
9 | limit,
10 | };
11 | const response = await request({
12 | method: "GET",
13 | url: "/api/v2/depth",
14 | params,
15 | });
16 |
17 | const tokens = market.split("-");
18 | const baseToken = tokens[0];
19 |
20 | const depth = response["data"];
21 | const bids = depth["bids"].map((arr) => arrToDepth(arr));
22 | let updatedBids = [];
23 | let aggregatedBidSize = BigNumber(0);
24 | for (let i = 0; i < bids.length; i = i + 1) {
25 | const bid = bids[i];
26 | aggregatedBidSize = aggregatedBidSize.plus(bid.size);
27 | let updatedBid = {
28 | price: bid.price,
29 | size: bid.size,
30 | aggregatedSize: config.fromWEI(
31 | baseToken,
32 | aggregatedBidSize,
33 | configTokens
34 | ),
35 | volume: bid.volume,
36 | count: bid.count,
37 | sizeInNumber: config.fromWEI(baseToken, bid.size, configTokens),
38 | };
39 | updatedBids.push(updatedBid);
40 | }
41 |
42 | const asks = depth["asks"].map((arr) => arrToDepth(arr));
43 | let updatedAsks = [];
44 | let aggregateAskSize = BigNumber(0);
45 | for (let i = 0; i < asks.length; i = i + 1) {
46 | const ask = asks[i];
47 | aggregateAskSize = aggregateAskSize.plus(ask.size);
48 | let updatedAsk = {
49 | price: ask.price,
50 | size: ask.size,
51 | aggregatedSize: config.fromWEI(baseToken, aggregateAskSize, configTokens),
52 | volume: ask.volume,
53 | count: ask.count,
54 | sizeInNumber: config.fromWEI(baseToken, ask.size, configTokens),
55 | };
56 | updatedAsks.push(updatedAsk);
57 | }
58 |
59 | return {
60 | bids: updatedBids,
61 | asks: updatedAsks,
62 | version: depth["version"],
63 | };
64 | }
65 |
66 | export function arrToDepth(arr) {
67 | return {
68 | price: arr[0],
69 | size: arr[1],
70 | volume: arr[2],
71 | count: Number(arr[3]),
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/depth/index.js:
--------------------------------------------------------------------------------
1 | import { arrToDepth, getDepth } from "./get";
2 |
3 | export { getDepth, arrToDepth };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/ethBalance/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export async function getEthBalance(address) {
4 | const params = {
5 | owner: address,
6 | };
7 |
8 | const response = await request({
9 | method: "GET",
10 | url: "/api/v2/ethBalances",
11 | params,
12 | });
13 |
14 | return response["data"];
15 | }
16 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/ethBalance/index.js:
--------------------------------------------------------------------------------
1 | import { getEthBalance } from "./get";
2 |
3 | export { getEthBalance };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/ethnonce/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export async function getEthNonce(owner) {
4 | const params = {
5 | owner,
6 | };
7 | const response = await request({
8 | method: "GET",
9 | url: "/api/v2/ethNonce",
10 | params,
11 | });
12 |
13 | return response["data"];
14 | }
15 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/ethnonce/index.js:
--------------------------------------------------------------------------------
1 | import { getEthNonce } from "./get";
2 |
3 | export { getEthNonce };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/fee-rates/index.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export const getFeeRates = async (markets, accountId, apiKey) => {
4 | const headers = {
5 | "X-API-KEY": apiKey,
6 | };
7 | const params = {
8 | accountId,
9 | markets: markets.join(","),
10 | };
11 | const response = await request({
12 | method: "GET",
13 | url: "/api/v2/user/feeRates",
14 | headers: headers,
15 | params,
16 | });
17 | return response["data"];
18 | };
19 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/marketinfo/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 | // import config from '../../../config';
3 |
4 | export async function getMarketInfo() {
5 | const response = await request({
6 | method: "GET",
7 | url: "/api/v2/exchange/markets",
8 | });
9 |
10 | let markets = response["data"];
11 | let updatedMarkets = removeDelistedMarkets(markets);
12 | response["data"] = updatedMarkets;
13 | return response["data"];
14 | }
15 |
16 | function removeDelistedMarkets(markets) {
17 | let delistedMarkets = ["TRB-ETH"];
18 | let updatedMarkets = [];
19 | for (let i = 0; i < markets.length; i = i + 1) {
20 | let market = markets[i];
21 | var isDelisted = false;
22 | for (let j = 0; j < delistedMarkets.length; j = j + 1) {
23 | if (market.market.includes(delistedMarkets[j])) {
24 | isDelisted = true;
25 | }
26 | }
27 | if (!isDelisted) {
28 | updatedMarkets.push(market);
29 | }
30 | }
31 | return updatedMarkets;
32 | }
33 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/marketinfo/index.js:
--------------------------------------------------------------------------------
1 | import { getMarketInfo } from "./get";
2 |
3 | export { getMarketInfo };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/onchainwithdrawal/get.js:
--------------------------------------------------------------------------------
1 | import { request } from '../../../common';
2 |
3 | export async function getDistributeInfo(requestId, apiKey) {
4 | const headers = {
5 | 'X-API-KEY': apiKey,
6 | };
7 | const params = { requestId };
8 |
9 | const response = await request({
10 | method: 'GET',
11 | url: '/api/v2/getDistributeInfo',
12 | headers: headers,
13 | params,
14 | });
15 |
16 | return response['data'];
17 | }
18 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/onchainwithdrawal/index.js:
--------------------------------------------------------------------------------
1 | import { sendWithdrawTransaction, updateDistributeHash } from './post';
2 | import { getDistributeInfo } from './get';
3 |
4 | export { sendWithdrawTransaction, updateDistributeHash, getDistributeInfo };
5 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/onchainwithdrawal/post.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export async function sendWithdrawTransaction(signedEthereumTx) {
4 | const data = {
5 | data: signedEthereumTx,
6 | };
7 | return await request({
8 | method: "POST",
9 | url: "/api/v2/sendEthTx",
10 | data,
11 | });
12 | }
13 |
14 | export async function updateDistributeHash(
15 | requestId,
16 | hash,
17 | publicKeyX,
18 | publicKeyY
19 | ) {
20 | const data = {
21 | requestId,
22 | txHash: hash,
23 | publicKeyX,
24 | publicKeyY,
25 | };
26 |
27 | return await request({
28 | method: 'POST',
29 | url: '/api/v2/updateDistributeHash',
30 | data,
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/orders/delete.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export async function cancelOrders(
4 | accountId,
5 | orderHash,
6 | clientOrderId,
7 | signed,
8 | apiKey
9 | ) {
10 | const params = {
11 | accountId: accountId,
12 | orderHash: orderHash,
13 | clientOrderId: clientOrderId,
14 | };
15 | const signature = signed.Rx + "," + signed.Ry + "," + signed.s;
16 | const headers = {
17 | "X-API-KEY": apiKey,
18 | "X-API-SIG": signature,
19 | };
20 | return await request({
21 | method: "DELETE",
22 | url: "/api/v2/orders",
23 | headers: headers,
24 | params,
25 | });
26 | }
27 |
28 | export async function cancelAllOrders(accountId, signed, apiKey) {
29 | const params = {
30 | accountId: accountId,
31 | };
32 | const signature = signed.Rx + "," + signed.Ry + "," + signed.s;
33 | const headers = {
34 | "X-API-KEY": apiKey,
35 | "X-API-SIG": signature,
36 | };
37 | return await request({
38 | method: "DELETE",
39 | url: "/api/v2/orders",
40 | headers: headers,
41 | params,
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/orders/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 | import config from "../../../config";
3 |
4 | export async function getOrders({
5 | accountId,
6 | limit,
7 | offset,
8 | market,
9 | statuses,
10 | apiKey,
11 | tokens,
12 | }) {
13 | const headers = {
14 | "X-API-KEY": apiKey,
15 | };
16 | const params = {
17 | accountId,
18 | limit,
19 | offset,
20 | start: 0,
21 | end: Date.now() * 1000,
22 | };
23 | if (typeof market !== "undefined") {
24 | params.market = market;
25 | }
26 |
27 | if (typeof statuses !== "undefined") {
28 | params.status = statuses.reduce((a, c) => a + "," + c);
29 | }
30 |
31 | const response = await request({
32 | method: "GET",
33 | url: "/api/v2/orders",
34 | headers: headers,
35 | params,
36 | });
37 |
38 | const data = response["data"];
39 | const totalNum = data["totalNum"];
40 | const orders = data["orders"];
41 | const updatedOrders = map(orders, tokens);
42 | return {
43 | accountId,
44 | orders: updatedOrders,
45 | totalNum,
46 | limit,
47 | offset,
48 | };
49 | }
50 |
51 | // Map API modal to UI modal
52 | // Used in WebSocketService
53 | export function map(orders, configTokens) {
54 | let updatedOrders = [];
55 | for (let i = 0; i < orders.length; i = i + 1) {
56 | const order = orders[i];
57 | let updatedOrder = { ...order };
58 | const market = updatedOrder.market;
59 | const tokens = market.split("-");
60 | const baseToken = tokens[0];
61 | const quoteToken = tokens[1];
62 | let sizeInBigNumber = "0";
63 | let filledInBigNumber = "0";
64 |
65 | updatedOrder["baseToken"] = baseToken;
66 | updatedOrder["quoteToken"] = quoteToken;
67 |
68 | // Used in table directly
69 | updatedOrder["sizeInString"] = config.fromWEI(
70 | baseToken,
71 | order.size,
72 | configTokens
73 | );
74 |
75 | updatedOrder["filledSizeInString"] = config.fromWEI(
76 | baseToken,
77 | order.filledSize,
78 | configTokens
79 | );
80 |
81 | let feeInString = "";
82 | if (Number(order.filledSize) === 0) {
83 | feeInString = "-";
84 | } else {
85 | const feeToken =
86 | order.side.toLowerCase() === "buy" ? baseToken : quoteToken;
87 | feeInString = config.fromWEI(feeToken, order.filledFee, configTokens, {
88 | precision: 8,
89 | });
90 | }
91 | updatedOrder["feeInString"] = feeInString;
92 |
93 | //因为前端side == sell, buy = false; side = buy, buy = true,因此都按照size计算。
94 | sizeInBigNumber = order["size"];
95 | filledInBigNumber = order["filledSize"];
96 | if (order.status === "processed") {
97 | updatedOrder["filled"] = `100%`;
98 | } else {
99 | const sizeFromWei = config.fromWEI(
100 | baseToken,
101 | sizeInBigNumber,
102 | configTokens
103 | );
104 | const filledSize = config.fromWEI(
105 | baseToken,
106 | filledInBigNumber,
107 | configTokens
108 | );
109 | const filledInNumber = Math.floor((filledSize / sizeFromWei) * 100);
110 | updatedOrder["filled"] = `${filledInNumber}%`;
111 | }
112 |
113 | const token = config.getTokenBySymbol(quoteToken, configTokens);
114 | const total = Number(
115 | parseFloat(updatedOrder.sizeInString) * parseFloat(order.price)
116 | );
117 | const totalInString = total.toFixed(token.precision);
118 | updatedOrder["totalInString"] = totalInString;
119 |
120 | updatedOrders.push(updatedOrder);
121 | }
122 | return updatedOrders;
123 | }
124 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/orders/index.js:
--------------------------------------------------------------------------------
1 | import { cancelAllOrders, cancelOrders } from "./delete";
2 | import { getOrders, map } from "./get";
3 |
4 | export { getOrders, cancelOrders, cancelAllOrders, map };
5 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/price/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export async function getPrice(legal) {
4 | const response = await request({
5 | method: "GET",
6 | url: "/api/v2/price",
7 | params: {
8 | legal,
9 | },
10 | });
11 | return response["data"];
12 | }
13 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/price/index.js:
--------------------------------------------------------------------------------
1 | import { getPrice } from "./get";
2 |
3 | export { getPrice };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/recommendedGasPrice/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 |
3 | export async function getRecommendedGasPrice() {
4 | const response = await request({
5 | method: "GET",
6 | url: "/api/v2/recommendedGasPrice",
7 | });
8 |
9 | return response["data"];
10 | }
11 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/recommendedGasPrice/index.js:
--------------------------------------------------------------------------------
1 | import { getRecommendedGasPrice } from "./get";
2 |
3 | export { getRecommendedGasPrice };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/tokenBalance/get.js:
--------------------------------------------------------------------------------
1 | import { request } from "../../../common";
2 | import config from "../../../config";
3 |
4 | export async function getTokenBalance(owner, symbol, tokens) {
5 | let token = config.getTokenBySymbol(symbol, tokens);
6 | let tokenAddress = token.address;
7 | const params = {
8 | owner,
9 | token: tokenAddress,
10 | };
11 |
12 | const response = await request({
13 | method: "GET",
14 | url: "/api/v2/tokenBalances",
15 | params,
16 | });
17 |
18 | return response["data"];
19 | }
20 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/tokenBalance/index.js:
--------------------------------------------------------------------------------
1 | import { getTokenBalance } from "./get";
2 |
3 | export { getTokenBalance };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/tokeninfo/get.js:
--------------------------------------------------------------------------------
1 | import { formatter, request } from "../../../common";
2 |
3 | export async function getTokenInfo() {
4 | const data = {};
5 | const response = await request({
6 | method: "GET",
7 | url: "/api/v2/exchange/tokens",
8 | data,
9 | });
10 |
11 | let tokens = response["data"];
12 | let updatedTokens = disableDelistedTokens(tokens);
13 | response["data"] = updatedTokens;
14 |
15 | return response["data"].map((token) => {
16 | token.address = formatter.formatAddress(token.address);
17 | return token;
18 | });
19 | }
20 |
21 | function disableDelistedTokens(tokens) {
22 | let delistedTokens = ["TRB"];
23 |
24 | let updatedTokens = [];
25 | for (let i = 0; i < tokens.length; i = i + 1) {
26 | let token = tokens[i];
27 | token["depositEnabled"] = true;
28 | for (let j = 0; j < delistedTokens.length; j = j + 1) {
29 | if (token.symbol.includes(delistedTokens[j])) {
30 | token["depositEnabled"] = false;
31 | }
32 | }
33 | updatedTokens.push(token);
34 | }
35 | return updatedTokens;
36 | }
37 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/tokeninfo/index.js:
--------------------------------------------------------------------------------
1 | import { getTokenInfo } from "./get";
2 |
3 | export { getTokenInfo };
4 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/transfer/get.js:
--------------------------------------------------------------------------------
1 | import { mapAmountInUI } from "../../LightconeAPI";
2 | import config from "../../../config";
3 | import request from "../../../common/request";
4 |
5 | export async function getTransferHistory(
6 | accountId,
7 | tokenSymbol,
8 | limit,
9 | offset,
10 | apiKey,
11 | tokens
12 | ) {
13 | const params = {
14 | accountId,
15 | limit,
16 | offset,
17 | start: 0,
18 | end: Date.now() * 1000,
19 | };
20 |
21 | if (typeof tokenSymbol !== "undefined") {
22 | params.tokenSymbol = tokenSymbol;
23 | }
24 |
25 | const headers = {
26 | "X-API-KEY": apiKey,
27 | };
28 |
29 | const response = await request({
30 | method: "GET",
31 | url: "/api/v2/user/transfers",
32 | headers: headers,
33 | params,
34 | });
35 |
36 | const data = response["data"];
37 | const totalNum = data["totalNum"];
38 | const transactions = data["transactions"];
39 |
40 | return {
41 | totalNum,
42 | transactions: mapTransactions(transactions, tokens),
43 | limit,
44 | offset,
45 | };
46 | }
47 |
48 | function mapTransactions(transactions, tokens) {
49 | return transactions.map((tran) => {
50 | const baseToken = config.getTokenBySymbol(tran.symbol, tokens);
51 | const amountInUI = mapAmountInUI(baseToken, tran.amount, tokens);
52 | const feeInUI = mapAmountInUI(baseToken, tran.feeAmount, tokens);
53 | const recipientInUI = tran.receiverAddress
54 | ? tran.receiverAddress.substring(0, 7) +
55 | "..." +
56 | tran.receiverAddress.slice(-7)
57 | : "";
58 |
59 | const senderInUI = tran.senderAddress
60 | ? tran.senderAddress.substring(0, 7) +
61 | "..." +
62 | tran.senderAddress.slice(-7)
63 | : "";
64 |
65 | return {
66 | ...tran,
67 | amountInUI,
68 | feeInUI,
69 | recipientInUI,
70 | senderInUI,
71 | tokenName: baseToken.name,
72 | };
73 | });
74 | }
75 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/transfer/index.js:
--------------------------------------------------------------------------------
1 | import { getTransferHistory } from "./get";
2 | import { submitTransfer } from "./post";
3 |
4 | export { getTransferHistory, submitTransfer };
5 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/transfer/post.js:
--------------------------------------------------------------------------------
1 | import request from "../../../common/request";
2 |
3 | export async function submitTransfer(transfer, ecdsaSig, apiKey) {
4 | const headers = {
5 | "X-API-KEY": apiKey,
6 | "X-API-SIG": ecdsaSig,
7 | };
8 |
9 | const response = await request({
10 | method: "POST",
11 | url: "/api/v2/transfer",
12 | headers: headers,
13 | data: transfer,
14 | });
15 |
16 | return response["data"];
17 | }
18 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/userTrades/get.js:
--------------------------------------------------------------------------------
1 | import { arrToTrade } from "../../LightconeAPI";
2 | import { request } from "../../../common";
3 | import config from "../../../config";
4 |
5 | export async function getUserTrades({
6 | accountId,
7 | limit,
8 | offset,
9 | market,
10 | apiKey,
11 | tokens,
12 | }) {
13 | const headers = {
14 | "X-API-KEY": apiKey,
15 | };
16 | const params = {
17 | accountId,
18 | limit,
19 | offset,
20 | start: 0,
21 | end: Date.now() * 1000,
22 | };
23 | if (typeof market !== "undefined") {
24 | params.market = market;
25 | }
26 |
27 | const response = await request({
28 | method: "GET",
29 | url: `/api/v2/user/trades`,
30 | headers: headers,
31 | params,
32 | });
33 |
34 | const data = response["data"];
35 | const totalNum = data["totalNum"];
36 | const trades = data["trades"].map((arr) => arrToTrade(arr));
37 | const updatedTrades = map(trades, tokens);
38 | return {
39 | accountId,
40 | trades: updatedTrades,
41 | totalNum,
42 | limit,
43 | offset,
44 | };
45 | }
46 |
47 | // Map API modal to UI modal
48 | function map(trades, configTokens) {
49 | let updatedTrades = [];
50 | for (let i = 0; i < trades.length; i = i + 1) {
51 | const trade = trades[i];
52 | let updatedTrade = { ...trade };
53 | const market = updatedTrade.market;
54 | const tokens = market.split("-");
55 | const baseToken = tokens[0];
56 | const quoteToken = tokens[1];
57 |
58 | updatedTrade["baseToken"] = baseToken;
59 | updatedTrade["quoteToken"] = quoteToken;
60 |
61 | // Used in table directly
62 | const sizeInString = config.fromWEI(baseToken, trade.size, configTokens);
63 | updatedTrade["sizeInString"] = sizeInString;
64 |
65 | let feeInString = "";
66 | if (Number(trade.filledSize) === 0) {
67 | feeInString = "-";
68 | } else {
69 | const feeToken =
70 | trade.side.toLowerCase() === "buy" ? baseToken : quoteToken;
71 | feeInString = config.fromWEI(feeToken, trade.fee, configTokens, {
72 | precision: 8,
73 | });
74 | }
75 | updatedTrade["feeInString"] = feeInString;
76 |
77 | const token = config.getTokenBySymbol(quoteToken, configTokens);
78 | const total = Number(
79 | parseFloat(updatedTrade.sizeInString) * parseFloat(trade.price)
80 | );
81 | const totalInString = total.toFixed(token.precision);
82 | updatedTrade["totalInString"] = totalInString;
83 |
84 | updatedTrades.push(updatedTrade);
85 | }
86 | return updatedTrades;
87 | }
88 |
--------------------------------------------------------------------------------
/src/lightcone/api/v1/userTrades/index.js:
--------------------------------------------------------------------------------
1 | import { getUserTrades } from "./get";
2 |
3 | export { getUserTrades };
4 |
--------------------------------------------------------------------------------
/src/lightcone/common/formatter.js:
--------------------------------------------------------------------------------
1 | import * as ethUtil from "ethereumjs-util";
2 | import BN from "bn.js";
3 | import BigNumber from "bignumber.js";
4 |
5 | BigNumber.config({
6 | EXPONENTIAL_AT: 20,
7 | RANGE: [-100000, 10000000],
8 | ROUNDING_MODE: 1,
9 | });
10 |
11 | /**
12 | *
13 | * @param mixed Buffer|number|string (hex string must be with '0x' prefix)
14 | * @returns {Buffer}
15 | */
16 | export function toBuffer(mixed) {
17 | if (mixed instanceof Buffer) {
18 | return mixed;
19 | } else {
20 | return ethUtil.toBuffer(mixed);
21 | }
22 | }
23 |
24 | /**
25 | *
26 | * @param num number|string (hex string must be with '0x' prefix)
27 | * @param places number of zeros to pad
28 | * @returns {Buffer}
29 | */
30 | export function zeroPad(num, places) {
31 | return toBuffer(String(num).padStart(places, "0"));
32 | }
33 |
34 | /**
35 | *
36 | * @param mixed number | BigNumber | BN | Buffer | string
37 | * @returns {string}
38 | */
39 | export function toHex(mixed) {
40 | if (
41 | typeof mixed === "number" ||
42 | mixed instanceof BigNumber ||
43 | mixed instanceof BN
44 | ) {
45 | return addHexPrefix(mixed.toString(16));
46 | }
47 |
48 | if (mixed instanceof Buffer) {
49 | return addHexPrefix(mixed.toString("hex"));
50 | }
51 |
52 | if (typeof mixed === "string") {
53 | const regex = new RegExp(/^0x[0-9a-fA-F]*$/);
54 | return regex.test(mixed)
55 | ? mixed
56 | : addHexPrefix(toBuffer(mixed).toString("hex"));
57 | }
58 | throw new Error("Unsupported type");
59 | }
60 |
61 | /**
62 | *
63 | * @param mixed number | BigNumber | BN | Buffer | string
64 | * @returns {number}
65 | */
66 | export function toNumber(mixed) {
67 | if (typeof mixed === "number") {
68 | return mixed;
69 | }
70 |
71 | if (mixed instanceof BigNumber || mixed instanceof BN) {
72 | return mixed.toNumber();
73 | }
74 |
75 | if (typeof mixed === "string") {
76 | return Number(mixed);
77 | }
78 |
79 | throw new Error("Unsupported type");
80 | }
81 |
82 | /**
83 | *
84 | * @param mixed number | BigNumber | BN | Buffer | string
85 | * @returns {BigNumber}
86 | */
87 | export function toBig(mixed) {
88 | if (mixed instanceof BigNumber) {
89 | return mixed;
90 | }
91 |
92 | if (typeof mixed === "number") {
93 | return new BigNumber(mixed.toString());
94 | }
95 |
96 | if (typeof mixed === "string") {
97 | return new BigNumber(mixed);
98 | }
99 |
100 | throw new Error("Unsupported type");
101 | }
102 |
103 | /**
104 | *
105 | * @param mixed number | BigNumber | BN | Buffer | string
106 | * @returns {BN}
107 | */
108 | export function toBN(mixed) {
109 | return mixed instanceof BN ? mixed : new BN(toBig(mixed).toString(10), 10);
110 | }
111 |
112 | /**
113 | *
114 | * @param value number | BigNumber | Buffer | string
115 | * @returns {BN}
116 | */
117 | export function fromGWEI(value) {
118 | return new BigNumber(toBig(value).times(1e9).toFixed(0));
119 | }
120 |
121 | /**
122 | *
123 | * @param value number | BigNumber | Buffer | string
124 | * @returns {BN}
125 | */
126 | export function toGWEI(value) {
127 | return toBig(value).div(1e9);
128 | }
129 |
130 | /**
131 | * Returns formatted hex string of a given private key
132 | * @param mixed Buffer| string
133 | * @returns {string}
134 | */
135 | export function formatKey(mixed) {
136 | if (mixed instanceof Buffer) {
137 | return mixed.toString("hex");
138 | }
139 |
140 | if (typeof mixed === "string") {
141 | return mixed.startsWith("0x") ? mixed.slice(2) : mixed;
142 | }
143 | throw new Error("Unsupported type");
144 | }
145 |
146 | /**
147 | * Returns hex string of a given address
148 | * @param mixed Buffer | string
149 | * @returns {string}
150 | */
151 | export function formatAddress(mixed) {
152 | if (mixed instanceof Buffer) {
153 | return ethUtil.toChecksumAddress("0x" + mixed.toString("hex"));
154 | }
155 |
156 | if (typeof mixed === "string") {
157 | return ethUtil.toChecksumAddress(
158 | mixed.startsWith("0x") ? mixed : "0x" + mixed
159 | );
160 | }
161 | throw new Error("Unsupported type");
162 | }
163 |
164 | /**
165 | * Returns hex string with '0x' prefix
166 | * @param input
167 | * @returns {string}
168 | */
169 | export function addHexPrefix(input) {
170 | if (typeof input === "string") {
171 | return input.startsWith("0x") ? input : "0x" + input;
172 | }
173 | throw new Error("Unsupported type");
174 | }
175 |
176 | /**
177 | * Returns hex string without '0x' prefix
178 | * @param input string
179 | * @returns {string}
180 | */
181 | export function clearHexPrefix(input) {
182 | if (typeof input === "string") {
183 | return input.startsWith("0x") ? input.slice(2) : input;
184 | }
185 | throw new Error("Unsupported type");
186 | }
187 |
188 | /**
189 | *
190 | * @param hex
191 | * @returns {string}
192 | */
193 | export function padLeftEven(hex) {
194 | return hex.length % 2 !== 0 ? `0${hex}` : hex;
195 | }
196 |
197 | /**
198 | * Returns symbol of a given kind of currency
199 | * @param settingsCurrency
200 | * @returns {*}
201 | */
202 | export function getDisplaySymbol(settingsCurrency) {
203 | switch (settingsCurrency) {
204 | case "CNY":
205 | return "¥";
206 | case "USD":
207 | return "$";
208 | default:
209 | return "";
210 | }
211 | }
212 |
213 | /**
214 | * Returns number in string with a given precision
215 | * @param number number | BigNumber
216 | * @param precision number
217 | * @param ceil bool round up
218 | * @returns {string}
219 | */
220 | export function toFixed(number, precision, ceil) {
221 | precision = precision || 0;
222 | if (number instanceof BigNumber) {
223 | const rm = ceil ? 0 : 1;
224 | return number.toFixed(precision, rm);
225 | }
226 |
227 | if (typeof number === "number") {
228 | return ceil
229 | ? (
230 | Math.ceil(number * Number("1e" + precision)) /
231 | Number("1e" + precision)
232 | ).toFixed(precision)
233 | : (
234 | Math.floor(number * Number("1e" + precision)) /
235 | Number("1e" + precision)
236 | ).toFixed(precision);
237 | }
238 |
239 | throw new Error("Unsupported type");
240 | }
241 |
--------------------------------------------------------------------------------
/src/lightcone/common/index.js:
--------------------------------------------------------------------------------
1 | import * as formatter from "./formatter";
2 | import * as utils from "./utils";
3 | import request from "./request";
4 | import validate from "./validator";
5 |
6 | export { formatter, request, validate, utils };
7 |
--------------------------------------------------------------------------------
/src/lightcone/common/request.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import config from "../config";
3 |
4 | export const SERVER_URL = config.getServer();
5 |
6 | /**
7 | * @description Supports single request and batch request;
8 | * @param options
9 | * @param parse
10 | * @returns {Promise}
11 | */
12 | export default function request(options, parse) {
13 | options.timeout = options.timeout || 30000;
14 | options.baseURL = options.baseURL || SERVER_URL;
15 |
16 | return axios(options).then((response) => {
17 | if (parse) {
18 | return parse(response);
19 | } else {
20 | return parseResponse(response);
21 | }
22 | });
23 | }
24 |
25 | function parseResponse(response) {
26 | const data = response["data"];
27 | const resultInfo = data["resultInfo"];
28 | const errorData = data["error"];
29 |
30 | // Some API endpoints in the old versions don't have resultInfo.
31 | if (resultInfo) {
32 | const code = resultInfo["code"];
33 | if (code !== 0) {
34 | const errorMessage = resultInfo["message"];
35 | throw Error(errorMessage);
36 | }
37 | return data;
38 | } else if (errorData) {
39 | const code = errorData["code"];
40 | if (code !== 0) {
41 | const errorMessage = `${errorData["code"]} ${errorData["message"]}`;
42 | throw Error(errorMessage);
43 | }
44 | return data;
45 | } else {
46 | return data;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/lightcone/common/schemas.js:
--------------------------------------------------------------------------------
1 | let schemas = {
2 | STRING: {
3 | type: "string",
4 | required: true,
5 | },
6 | URL: {
7 | type: "url",
8 | required: true,
9 | },
10 | ADDRESS: {
11 | type: "string",
12 | required: true,
13 | pattern: /^0x[0-9a-fA-F]{40}$/g,
14 | },
15 | HEX: {
16 | type: "string",
17 | required: true,
18 | pattern: /^0x[0-9a-fA-F]*$/g,
19 | },
20 | ETH_DATA: {
21 | type: "string",
22 | required: true,
23 | pattern: /^0x[0-9a-fA-F]{1,64}$/g,
24 | },
25 | TX_HASH: {
26 | type: "string",
27 | required: true,
28 | pattern: /^0x[0-9a-fA-F]{64}$/g,
29 | },
30 | RPC_TAG: {
31 | type: "enum",
32 | required: true,
33 | enum: ["latest", "earliest", "pending"],
34 | },
35 | TX: {
36 | type: "object",
37 | required: true,
38 | fields: {
39 | from: {
40 | type: "string",
41 | required: true,
42 | pattern: /^0x[0-9a-fA-F]{40}$/g,
43 | },
44 | to: {
45 | type: "string",
46 | required: true,
47 | pattern: /^0x[0-9a-fA-F]{40}$/g,
48 | },
49 | value: {
50 | type: "string",
51 | required: true,
52 | pattern: /^[0-9]*$/g,
53 | },
54 | gas: {
55 | type: "string",
56 | required: true,
57 | pattern: /^[1-9][0-9]*$/g,
58 | },
59 | gasPrice: {
60 | type: "string",
61 | required: true,
62 | pattern: /^[1-9][0-9]*$/g,
63 | },
64 | chainId: {
65 | type: "number",
66 | required: true,
67 | },
68 | nonce: {
69 | type: "string",
70 | required: true,
71 | pattern: /^[0-9]*$/g,
72 | },
73 | data: {
74 | type: "string",
75 | required: true,
76 | pattern: /^0x[0-9a-fA-F]*$/g,
77 | },
78 | signed: {
79 | type: "string",
80 | },
81 | },
82 | },
83 | };
84 |
85 | export default schemas;
86 |
--------------------------------------------------------------------------------
/src/lightcone/common/utils.js:
--------------------------------------------------------------------------------
1 | import { hashPersonalMessage, keccak } from "ethereumjs-util";
2 | import { toBig, toHex } from "./formatter";
3 | import crypto from "crypto";
4 |
5 | /**
6 | * trim head space and tail space
7 | * @param str string
8 | */
9 | export function trim(str) {
10 | return str.replace(/(^\s+)|(\s+$)/g, "");
11 | }
12 |
13 | /**
14 | * trim all spaces
15 | * @param str
16 | */
17 | export function trimAll(str) {
18 | return trim(str).replace(/\s/g, "");
19 | }
20 |
21 | export function keccakHash(str) {
22 | return toHex(keccak(str));
23 | }
24 |
25 | export async function pbkdf2Hash(password, address, salt = "Loopring") {
26 | return new Promise((resolve) => {
27 | crypto.pbkdf2(
28 | password,
29 | salt + address.toLowerCase(),
30 | 4000000,
31 | 32,
32 | "sha256",
33 | function (err, result) {
34 | if (err) {
35 | resolve({ error: err });
36 | } else {
37 | resolve({ hash: toHex(result) });
38 | }
39 | }
40 | );
41 | });
42 | }
43 |
44 | export function calculateGas(gasPrice, gasLimit) {
45 | return toBig(gasPrice).times(gasLimit).div(1e9);
46 | }
47 |
48 | export default {
49 | hashPersonalMessage,
50 | trim,
51 | trimAll,
52 | keccakHash,
53 | calculateGas,
54 | pbkdf2Hash,
55 | };
56 |
--------------------------------------------------------------------------------
/src/lightcone/common/validator.js:
--------------------------------------------------------------------------------
1 | import Schema from "async-validator";
2 | import schemas from "./schemas";
3 |
4 | let handleErrors = (errors) => {
5 | let msgs = errors.map((err) => err.message).join();
6 | throw new Error(`data type invalid: ${msgs} \n`);
7 | };
8 |
9 | export default async function validate(payload) {
10 | let { type, value, onError, onSuccess } = payload;
11 | let source = {};
12 | let schema = {};
13 |
14 | if (typeof value === "undefined") {
15 | throw new Error(`data type invalid: ${type} should not be undefined`);
16 | }
17 | if (value === null) {
18 | throw new Error(`data type invalid: ${type} should not be null`);
19 | }
20 |
21 | if (schemas[type]) {
22 | schema[type] = schemas[type];
23 | source[type] = value;
24 | }
25 |
26 | let validator = new Schema(schema);
27 | await validator.validate(source, (errors, fields) => {
28 | if (errors) {
29 | if (onError) {
30 | onError(errors, fields);
31 | } else {
32 | handleErrors(errors, fields);
33 | }
34 | } else {
35 | if (onSuccess) {
36 | onSuccess();
37 | }
38 | }
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/src/lightcone/config/abis/contractWallet.json:
--------------------------------------------------------------------------------
1 | [{
2 | "inputs": [
3 | {
4 | "internalType": "bytes",
5 | "name": "_data",
6 | "type": "bytes"
7 | },
8 | {
9 | "internalType": "bytes",
10 | "name": "_signature",
11 | "type": "bytes"
12 | }
13 | ],
14 | "name": "isValidSignature",
15 | "outputs": [
16 | {
17 | "internalType": "bytes4",
18 | "name": "magicValue",
19 | "type": "bytes4"
20 | }
21 | ],
22 | "stateMutability": "view",
23 | "type": "function"
24 | },
25 | {
26 | "inputs": [
27 | {
28 | "internalType": "bytes",
29 | "name": "_data",
30 | "type": "bytes32"
31 | },
32 | {
33 | "internalType": "bytes",
34 | "name": "_signature",
35 | "type": "bytes"
36 | }
37 | ],
38 | "name": "isValidSignature",
39 | "outputs": [
40 | {
41 | "internalType": "bytes4",
42 | "name": "magicValue",
43 | "type": "bytes4"
44 | }
45 | ],
46 | "stateMutability": "view",
47 | "type": "function"
48 | }
49 | ]
50 |
--------------------------------------------------------------------------------
/src/lightcone/config/abis/erc20.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": true,
4 | "inputs": [],
5 | "name": "name",
6 | "outputs": [
7 | {
8 | "name": "",
9 | "type": "string"
10 | }
11 | ],
12 | "payable": false,
13 | "stateMutability": "view",
14 | "type": "function"
15 | },
16 | {
17 | "constant": false,
18 | "inputs": [
19 | {
20 | "name": "_spender",
21 | "type": "address"
22 | },
23 | {
24 | "name": "_value",
25 | "type": "uint256"
26 | }
27 | ],
28 | "name": "approve",
29 | "outputs": [
30 | {
31 | "name": "",
32 | "type": "bool"
33 | }
34 | ],
35 | "payable": false,
36 | "stateMutability": "nonpayable",
37 | "type": "function"
38 | },
39 | {
40 | "constant": false,
41 | "inputs": [
42 | {
43 | "name": "_from",
44 | "type": "address"
45 | },
46 | {
47 | "name": "_to",
48 | "type": "address"
49 | },
50 | {
51 | "name": "_value",
52 | "type": "uint256"
53 | }
54 | ],
55 | "name": "transferFrom",
56 | "outputs": [
57 | {
58 | "name": "",
59 | "type": "bool"
60 | }
61 | ],
62 | "payable": false,
63 | "stateMutability": "nonpayable",
64 | "type": "function"
65 | },
66 | {
67 | "constant": true,
68 | "inputs": [],
69 | "name": "decimals",
70 | "outputs": [
71 | {
72 | "name": "",
73 | "type": "uint8"
74 | }
75 | ],
76 | "payable": false,
77 | "stateMutability": "view",
78 | "type": "function"
79 | },
80 | {
81 | "constant": true,
82 | "inputs": [
83 | {
84 | "name": "_owner",
85 | "type": "address"
86 | }
87 | ],
88 | "name": "balanceOf",
89 | "outputs": [
90 | {
91 | "name": "balance",
92 | "type": "uint256"
93 | }
94 | ],
95 | "payable": false,
96 | "stateMutability": "view",
97 | "type": "function"
98 | },
99 | {
100 | "constant": true,
101 | "inputs": [],
102 | "name": "symbol",
103 | "outputs": [
104 | {
105 | "name": "",
106 | "type": "string"
107 | }
108 | ],
109 | "payable": false,
110 | "stateMutability": "view",
111 | "type": "function"
112 | },
113 | {
114 | "constant": false,
115 | "inputs": [
116 | {
117 | "name": "_to",
118 | "type": "address"
119 | },
120 | {
121 | "name": "_value",
122 | "type": "uint256"
123 | }
124 | ],
125 | "name": "transfer",
126 | "outputs": [
127 | {
128 | "name": "",
129 | "type": "bool"
130 | }
131 | ],
132 | "payable": false,
133 | "stateMutability": "nonpayable",
134 | "type": "function"
135 | },
136 | {
137 | "constant": true,
138 | "inputs": [
139 | {
140 | "name": "_owner",
141 | "type": "address"
142 | },
143 | {
144 | "name": "_spender",
145 | "type": "address"
146 | }
147 | ],
148 | "name": "allowance",
149 | "outputs": [
150 | {
151 | "name": "",
152 | "type": "uint256"
153 | }
154 | ],
155 | "payable": false,
156 | "stateMutability": "view",
157 | "type": "function"
158 | },
159 | {
160 | "payable": true,
161 | "stateMutability": "payable",
162 | "type": "fallback"
163 | },
164 | {
165 | "anonymous": false,
166 | "inputs": [
167 | {
168 | "indexed": true,
169 | "name": "owner",
170 | "type": "address"
171 | },
172 | {
173 | "indexed": true,
174 | "name": "spender",
175 | "type": "address"
176 | },
177 | {
178 | "indexed": false,
179 | "name": "value",
180 | "type": "uint256"
181 | }
182 | ],
183 | "name": "Approval",
184 | "type": "event"
185 | },
186 | {
187 | "anonymous": false,
188 | "inputs": [
189 | {
190 | "indexed": true,
191 | "name": "from",
192 | "type": "address"
193 | },
194 | {
195 | "indexed": true,
196 | "name": "to",
197 | "type": "address"
198 | },
199 | {
200 | "indexed": false,
201 | "name": "value",
202 | "type": "uint256"
203 | }
204 | ],
205 | "name": "Transfer",
206 | "type": "event"
207 | }
208 | ]
209 |
--------------------------------------------------------------------------------
/src/lightcone/config/abis/exchange.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": false,
4 | "inputs": [
5 | {
6 | "internalType": "uint256",
7 | "name": "pubKeyX",
8 | "type": "uint256"
9 | },
10 | {
11 | "internalType": "uint256",
12 | "name": "pubKeyY",
13 | "type": "uint256"
14 | },
15 | {
16 | "internalType": "bytes",
17 | "name": "permission",
18 | "type": "bytes"
19 | }
20 | ],
21 | "name": "createOrUpdateAccount",
22 | "outputs": [
23 | {
24 | "internalType": "uint24",
25 | "name": "accountID",
26 | "type": "uint24"
27 | },
28 | {
29 | "internalType": "bool",
30 | "name": "isAccountNew",
31 | "type": "bool"
32 | },
33 | {
34 | "internalType": "bool",
35 | "name": "isAccountUpdated",
36 | "type": "bool"
37 | }
38 | ],
39 | "payable": true,
40 | "stateMutability": "payable",
41 | "type": "function"
42 | },
43 | {
44 | "constant": false,
45 | "inputs": [
46 | {
47 | "internalType": "address",
48 | "name": "tokenAddress",
49 | "type": "address"
50 | },
51 | {
52 | "internalType": "uint96",
53 | "name": "amount",
54 | "type": "uint96"
55 | }
56 | ],
57 | "name": "deposit",
58 | "outputs": [],
59 | "payable": true,
60 | "stateMutability": "payable",
61 | "type": "function"
62 | },
63 | {
64 | "constant": false,
65 | "inputs": [
66 | {
67 | "internalType": "address",
68 | "name": "recipient",
69 | "type": "address"
70 | },
71 | {
72 | "internalType": "address",
73 | "name": "tokenAddress",
74 | "type": "address"
75 | },
76 | {
77 | "internalType": "uint96",
78 | "name": "amount",
79 | "type": "uint96"
80 | }
81 | ],
82 | "name": "depositTo",
83 | "outputs": [],
84 | "payable": true,
85 | "stateMutability": "payable",
86 | "type": "function"
87 | },
88 | {
89 | "constant": true,
90 | "inputs": [],
91 | "name": "getFees",
92 | "outputs": [
93 | {
94 | "internalType": "uint256",
95 | "name": "_accountCreationFeeETH",
96 | "type": "uint256"
97 | },
98 | {
99 | "internalType": "uint256",
100 | "name": "_accountUpdateFeeETH",
101 | "type": "uint256"
102 | },
103 | {
104 | "internalType": "uint256",
105 | "name": "_depositFeeETH",
106 | "type": "uint256"
107 | },
108 | {
109 | "internalType": "uint256",
110 | "name": "_withdrawalFeeETH",
111 | "type": "uint256"
112 | }
113 | ],
114 | "payable": false,
115 | "stateMutability": "view",
116 | "type": "function"
117 | },
118 | {
119 | "constant": false,
120 | "inputs": [
121 | {
122 | "internalType": "address",
123 | "name": "tokenAddress",
124 | "type": "address"
125 | },
126 | {
127 | "internalType": "uint96",
128 | "name": "amount",
129 | "type": "uint96"
130 | }
131 | ],
132 | "name": "withdraw",
133 | "outputs": [],
134 | "payable": true,
135 | "stateMutability": "payable",
136 | "type": "function"
137 | },{
138 | "constant": false,
139 | "inputs": [
140 | {
141 | "internalType": "uint256",
142 | "name": "pubKeyX",
143 | "type": "uint256"
144 | },
145 | {
146 | "internalType": "uint256",
147 | "name": "pubKeyY",
148 | "type": "uint256"
149 | },
150 | {
151 | "internalType": "address",
152 | "name": "tokenAddress",
153 | "type": "address"
154 | },
155 | {
156 | "internalType": "uint96",
157 | "name": "amount",
158 | "type": "uint96"
159 | },
160 | {
161 | "internalType": "bytes",
162 | "name": "permission",
163 | "type": "bytes"
164 | }
165 | ],
166 | "name": "updateAccountAndDeposit",
167 | "outputs": [
168 | {
169 | "internalType": "uint24",
170 | "name": "accountID",
171 | "type": "uint24"
172 | },
173 | {
174 | "internalType": "bool",
175 | "name": "isAccountNew",
176 | "type": "bool"
177 | },
178 | {
179 | "internalType": "bool",
180 | "name": "isAccountUpdated",
181 | "type": "bool"
182 | }
183 | ],
184 | "payable": true,
185 | "stateMutability": "payable",
186 | "type": "function"
187 | }
188 | ]
189 |
--------------------------------------------------------------------------------
/src/lightcone/config/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultGasPrice": 21,
3 | "defaultGasLimit": "0x7a120",
4 | "label": 211,
5 | "fee": "0.01",
6 | "feeToken": "ETH",
7 | "maxFeeBips": 50,
8 | "maxAmount": "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
9 | "prodServerUrl": "api.loopring.io",
10 | "prodWsUrl":"ws.loopring.io",
11 | "defaultChannelId":"hebao::subchannel::0001",
12 | "txs": [
13 | {
14 | "type": "approve",
15 | "gas": 100000
16 | },
17 | {
18 | "type": "withdraw",
19 | "gas": 200000
20 | },
21 | {
22 | "type": "deposit",
23 | "gas": 500000
24 | },
25 | {
26 | "type": "create",
27 | "gas": 500000
28 | },
29 | {
30 | "type": "depositTo",
31 | "gas": 500000
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/src/lightcone/config/index.js:
--------------------------------------------------------------------------------
1 | import { toBig, toFixed } from "../common/formatter";
2 | import { LOOPRING_API_HOST } from "../../env";
3 | const config = require("./config.json");
4 |
5 | function getRelayerHost(restUrl = true) {
6 | return restUrl ? LOOPRING_API_HOST : getWsServer();
7 | }
8 |
9 | function getServer() {
10 | return "https://" + getRelayerHost();
11 | }
12 |
13 | function getWsServer() {
14 | return "wss://" + getRelayerHost(false) + "/v2/ws";
15 | }
16 |
17 | function getChannelId() {
18 | return config.defaultChannelId;
19 | }
20 |
21 | function getLabel() {
22 | return config.label;
23 | }
24 |
25 | function getMaxFeeBips() {
26 | return config.maxFeeBips;
27 | }
28 |
29 | function getTokenBySymbol(symbol, tokens) {
30 | if (typeof symbol === "undefined") {
31 | return {};
32 | }
33 | return (
34 | tokens.find(
35 | (token) => token.symbol.toLowerCase() === symbol.toLowerCase()
36 | ) || {}
37 | );
38 | }
39 |
40 | function getTokenByAddress(address, tokens) {
41 | if (typeof address === "undefined") {
42 | return {};
43 | }
44 | return tokens.find(
45 | (token) => token.address.toLowerCase() === address.toLowerCase()
46 | );
47 | }
48 |
49 | function getTokenByTokenId(tokenId, tokens) {
50 | if (typeof tokenId === "undefined") {
51 | return {};
52 | }
53 | return tokens.find((token) => token.tokenId === tokenId);
54 | }
55 |
56 | function fromWEI(symbol, valueInWEI, tokens, { precision, ceil } = {}) {
57 | const token = getTokenBySymbol(symbol, tokens);
58 | const precisionToFixed = precision ? precision : token.precision;
59 | const value = toBig(valueInWEI).div("1e" + token.decimals);
60 | return toFixed(value, precisionToFixed, ceil);
61 | }
62 |
63 | function toWEI(symbol, value, tokens) {
64 | const token = getTokenBySymbol(symbol, tokens);
65 | if (typeof token === "undefined") {
66 | return 0;
67 | }
68 | return toBig(value)
69 | .times("1e" + token.decimals)
70 | .toFixed(0);
71 | }
72 |
73 | function getMarketByPair(pair, markets) {
74 | if (pair) {
75 | return markets.find((m) => m.market === pair);
76 | }
77 | }
78 |
79 | function isSupportedMarket(market, markets) {
80 | return !!getMarketByPair(market, markets);
81 | }
82 |
83 | function getMarketsByTokenR(token, markets) {
84 | return markets.filter((item) => item.split("-")[1] === token);
85 | }
86 |
87 | function getMarketsByTokenL(token, markets) {
88 | return markets.filter((item) => item.split("-")[0] === token);
89 | }
90 |
91 | function getTokenSupportedMarkets(token) {
92 | const leftMarket = getMarketsByTokenL(token);
93 | const rightMarket = getMarketsByTokenR(token);
94 | return [...leftMarket, ...rightMarket];
95 | }
96 |
97 | function getGasLimitByType(type) {
98 | if (type) {
99 | return config.txs.find((tx) => type === tx.type);
100 | }
101 | }
102 |
103 | function getFeeByType(type, fees) {
104 | if (type) {
105 | return fees.find((fee) => type === fee.type);
106 | }
107 | }
108 |
109 | function getMaxAmountInWEI() {
110 | return config.maxAmount;
111 | }
112 |
113 | export default {
114 | getRelayerHost,
115 | getServer,
116 | getWsServer,
117 | getTokenBySymbol,
118 | getTokenByAddress,
119 | getTokenByTokenId,
120 | getMarketByPair,
121 | getGasLimitByType,
122 | getFeeByType,
123 | getChannelId,
124 | getLabel,
125 | isSupportedMarket,
126 | getMarketsByTokenR,
127 | getTokenSupportedMarkets,
128 | getMaxFeeBips,
129 | getMaxAmountInWEI,
130 | fromWEI,
131 | toWEI,
132 | };
133 |
--------------------------------------------------------------------------------
/src/lightcone/ethereum/contracts/AbiFunction.js:
--------------------------------------------------------------------------------
1 | import {
2 | addHexPrefix,
3 | clearHexPrefix,
4 | toBuffer,
5 | toHex,
6 | } from "../../common/formatter";
7 | import { methodID, rawDecode, rawEncode } from "ethereumjs-abi";
8 | import BN from "bn.js";
9 |
10 | export default class AbiFunction {
11 | constructor({ inputs, name, outputs, constant }) {
12 | this.name = name;
13 | this.inputTypes = inputs.map(({ type }) => type);
14 | this.inputs = inputs;
15 | this.outputTypes = outputs.map(({ type }) => type);
16 | this.outputs = outputs;
17 | this.constant = constant;
18 | this.methodAbiHash = toHex(methodID(name, this.inputTypes));
19 | }
20 |
21 | /**
22 | * @description Returns encoded methodId and inputs
23 | * @param inputs Object, examples {owner:"0x000...}
24 | * @returns {string}
25 | */
26 | encodeInputs(inputs) {
27 | const abiInputs = this.parseInputs(inputs);
28 | return (
29 | this.methodAbiHash +
30 | clearHexPrefix(toHex(rawEncode(this.inputTypes, abiInputs)))
31 | );
32 | }
33 |
34 | /**
35 | * @description decode ethereum jsonrpc response result
36 | * @param outputs
37 | * @returns {*}
38 | */
39 | decodeOutputs(outputs) {
40 | return this.parseOutputs(rawDecode(this.outputTypes, toBuffer(outputs)));
41 | }
42 |
43 | /**
44 | * @description decode encoded inputs
45 | * @param encoded
46 | * @returns {*}
47 | */
48 | decodeEncodedInputs(encoded) {
49 | return this.parseOutputs(
50 | rawDecode(this.inputTypes, toBuffer(addHexPrefix(encoded)))
51 | );
52 | }
53 |
54 | parseInputs(inputs = {}) {
55 | return this.inputs.map(({ name, type }) => {
56 | if (inputs[name] === undefined) {
57 | throw new Error(`Parameter ${name} of type ${type} is required!`);
58 | }
59 | return inputs[name];
60 | });
61 | }
62 |
63 | parseOutputs(outputs) {
64 | return outputs.map((output) => {
65 | if (output instanceof BN) {
66 | return toHex(output);
67 | }
68 | return output;
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/lightcone/ethereum/contracts/Contract.js:
--------------------------------------------------------------------------------
1 | import { methodID } from "ethereumjs-abi";
2 | import { toHex } from "../../common/formatter";
3 | import AbiFunction from "./AbiFunction";
4 |
5 | export default class Contract {
6 | constructor(abi) {
7 | const funAbi = abi.filter(({ type }) => type === "function");
8 | this.abiFunctions = funAbi.reduce((acc, item) => {
9 | const inputTypes = item.inputs.map(({ type }) => type);
10 | const key = `${item.name}(${inputTypes.toString()})`;
11 | const methodHash = methodID(item.name, inputTypes);
12 | return {
13 | ...acc,
14 | [item.name]: new AbiFunction(item),
15 | [key]: new AbiFunction(item),
16 | [methodHash]: new AbiFunction(item),
17 | };
18 | }, {});
19 | }
20 |
21 | /**
22 | * @description Encodes inputs data according to ethereum abi
23 | * @param method string can be full method or just method name, examples: 'balanceOf' or balanceOf(address)
24 | * @param inputs array
25 | * @returns {*|string}
26 | */
27 | encodeInputs(method, inputs) {
28 | const abiFunction = this.abiFunctions[method];
29 | if (abiFunction) {
30 | return abiFunction.encodeInputs(inputs);
31 | } else {
32 | throw new Error(`No ${method} method according to abi `);
33 | }
34 | }
35 |
36 | /**
37 | * @description Decodes outputs
38 | * @param method string can be full method or just method name, examples: 'balanceOf' or balanceOf(address)
39 | * @param outputs string
40 | * @returns {*}
41 | */
42 | decodeOutputs(method, outputs) {
43 | const abiFunction = this.abiFunctions[method];
44 | if (abiFunction) {
45 | return abiFunction.decodeOutputs(outputs);
46 | } else {
47 | throw new Error(`No ${method} method according to abi `);
48 | }
49 | }
50 |
51 | /**
52 | * @description Decode encoded method and inputs
53 | * @param encode string | Buffer
54 | * @returns {*}
55 | */
56 | decodeEncodeInputs(encode) {
57 | encode = toHex(encode);
58 | const methodId = encode.slice(0, 10);
59 | const abiFunction = this.abiFunctions[methodId];
60 | if (abiFunction) {
61 | return abiFunction.decodeEncodedInputs(encode.slice(10));
62 | } else {
63 | throw new Error(`No corresponding method according to abi `);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/lightcone/ethereum/contracts/Contracts.js:
--------------------------------------------------------------------------------
1 | import Contract from "./Contract";
2 |
3 | const erc20Abi = require("../../config/abis/erc20.json");
4 | const exchangeAbi = require("../../config/abis/exchange.json");
5 | const contractWalletAbi = require("../../config/abis/contractWallet.json");
6 |
7 | const ERC20Token = new Contract(erc20Abi);
8 | const ExchangeContract = new Contract(exchangeAbi);
9 | const ContractWallet = new Contract(contractWalletAbi);
10 |
11 | export default {
12 | ERC20Token,
13 | ExchangeContract,
14 | ContractWallet,
15 | };
16 |
--------------------------------------------------------------------------------
/src/lightcone/ethereum/contracts/index.js:
--------------------------------------------------------------------------------
1 | import AbiFunction from "./AbiFunction";
2 | import Contract from "./Contract";
3 | import Contracts from "./Contracts";
4 |
5 | export default {
6 | AbiFunction,
7 | Contract,
8 | Contracts,
9 | };
10 |
--------------------------------------------------------------------------------
/src/lightcone/ethereum/metaMask.js:
--------------------------------------------------------------------------------
1 | import { addHexPrefix, toBuffer, toHex, toNumber } from "../common/formatter";
2 | import { hashPersonalMessage, keccak256 } from "ethereumjs-util";
3 | import { validate } from "../common";
4 | import ABI from "../ethereum/contracts";
5 | import Transaction from "ethereumjs-tx";
6 |
7 | /**
8 | * @description sign hash
9 | * @param web3
10 | * @param account
11 | * @param hash
12 | * @returns {Promise.<*>}
13 | */
14 | export async function sign(web3, account, hash) {
15 | await validate({ value: account, type: "ETH_ADDRESS" });
16 |
17 | return new Promise((resolve) => {
18 | web3.eth.sign(hash, account, function (err, result) {
19 | if (!err) {
20 | const r = result.slice(0, 66);
21 | const s = addHexPrefix(result.slice(66, 130));
22 | const v = toNumber(addHexPrefix(result.slice(130, 132)));
23 | resolve({ result: { r, s, v } });
24 | } else {
25 | const errorMsg = err.message.substring(0, err.message.indexOf(" at "));
26 | resolve({ error: { message: errorMsg } });
27 | }
28 | });
29 | });
30 | }
31 |
32 | /**
33 | * @description sign message
34 | * @param web3
35 | * @param account
36 | * @param message
37 | * @returns {Promise}
38 | */
39 | export async function signMessage(web3, account, message) {
40 | const hash = toHex(hashPersonalMessage(keccak256(message)));
41 | return await sign(web3, account, hash);
42 | }
43 |
44 | export async function personalSign(web3, account, msg) {
45 | return new Promise((resolve) => {
46 | web3.eth.personal.sign(msg, account, "", async function (err, result) {
47 | if (!err) {
48 | const valid = await ecRecover(web3, account, msg, result);
49 | if (valid.result) {
50 | resolve({ sig: result });
51 | } else {
52 | const walletValid = await contractWalletValidate(
53 | web3,
54 | account,
55 | msg,
56 | result
57 | );
58 | // console.log(JSON.stringify(walletValid));
59 | if (walletValid.result) {
60 | resolve({ sig: result });
61 | } else {
62 | const walletValid2 = await contractWalletValidate2(
63 | web3,
64 | account,
65 | msg,
66 | result
67 | );
68 | console.log(JSON.stringify(walletValid2));
69 | if (walletValid2.result) {
70 | resolve({ sig: result });
71 | } else {
72 | resolve({ error: "invalid sig" });
73 | }
74 | }
75 | }
76 | } else resolve({ error: err });
77 | });
78 | });
79 | }
80 |
81 | export async function ecRecover(web3, account, msg, sig) {
82 | return new Promise((resolve) => {
83 | web3.eth.personal.ecRecover(msg, sig, function (err, address) {
84 | if (!err)
85 | resolve({
86 | result: address.toLowerCase() === account.toLowerCase(),
87 | });
88 | else resolve({ error: err });
89 | });
90 | });
91 | }
92 |
93 | export async function contractWalletValidate(web3, account, msg, sig) {
94 | return new Promise((resolve) => {
95 | const hash = hashPersonalMessage(toBuffer(msg));
96 | const data = ABI.Contracts.ContractWallet.encodeInputs(
97 | "isValidSignature(bytes,bytes)",
98 | {
99 | _data: hash,
100 | _signature: toBuffer(sig),
101 | }
102 | );
103 |
104 | web3.eth.call(
105 | {
106 | to: account, // contract addr
107 | data: data,
108 | },
109 | function (err, result) {
110 | if (!err) {
111 | const valid = ABI.Contracts.ContractWallet.decodeOutputs(
112 | "isValidSignature(bytes,bytes)",
113 | result
114 | );
115 | resolve({
116 | result: toHex(toBuffer(valid[0])) === data.slice(0, 10),
117 | });
118 | } else resolve({ error: err });
119 | }
120 | );
121 | });
122 | }
123 |
124 | export async function contractWalletValidate2(web3, account, msg, sig) {
125 | return new Promise((resolve) => {
126 | const hash = hashPersonalMessage(toBuffer(msg));
127 | const data = ABI.Contracts.ContractWallet.encodeInputs(
128 | "isValidSignature(bytes32,bytes)",
129 | {
130 | _data: hash,
131 | _signature: toBuffer(sig),
132 | }
133 | );
134 |
135 | web3.eth.call(
136 | {
137 | to: account, // contract addr
138 | data: data,
139 | },
140 | function (err, result) {
141 | if (!err) {
142 | const valid = ABI.Contracts.ContractWallet.decodeOutputs(
143 | "isValidSignature(bytes32,bytes)",
144 | result
145 | );
146 | resolve({
147 | result: toHex(toBuffer(valid[0])) === data.slice(0, 10),
148 | });
149 | } else resolve({ error: err });
150 | }
151 | );
152 | });
153 | }
154 |
155 | /**
156 | * @description Signs ethereum tx
157 | * @param web3
158 | * @param account
159 | * @param rawTx
160 | * @returns {Promise.<*>}
161 | */
162 | export async function signEthereumTx(web3, account, rawTx) {
163 | await validate({ value: rawTx, type: "TX" });
164 | const ethTx = new Transaction(rawTx);
165 | const hash = toHex(ethTx.hash(false));
166 | const response = await sign(web3, account, hash);
167 | if (!response["error"]) {
168 | const signature = response["result"];
169 | signature.v += ethTx.getChainId() * 2 + 8;
170 | Object.assign(ethTx, signature);
171 | return { result: toHex(ethTx.serialize()) };
172 | } else {
173 | throw new Error(response["error"]["message"]);
174 | }
175 | }
176 |
177 | /**
178 | * @description Sends ethereum tx through MetaMask
179 | * @param web3
180 | * @param tx
181 | * @returns {*}
182 | */
183 | export async function sendTransaction(web3, tx) {
184 | await validate({ type: "TX", value: tx });
185 | delete tx.gasPrice;
186 | const response = await new Promise((resolve) => {
187 | web3.eth.sendTransaction(tx, function (err, transactionHash) {
188 | if (!err) {
189 | resolve({ result: transactionHash });
190 | } else {
191 | resolve({ error: { message: err.message } });
192 | }
193 | });
194 | });
195 |
196 | if (response["result"]) {
197 | return response;
198 | } else {
199 | throw new Error(response["error"]["message"]);
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/lightcone/sign/babyjub.js:
--------------------------------------------------------------------------------
1 | import { bigInt, bn128 } from "snarkjs";
2 |
3 | const Base8 = [
4 | bigInt(
5 | "16540640123574156134436876038791482806971768689494387082833631921987005038935"
6 | ),
7 | bigInt(
8 | "20819045374670962167435360035096875258406992893633759881276124905556507972311"
9 | ),
10 | ];
11 | const order = bigInt(
12 | "21888242871839275222246405745257275088614511777268538073601725287587578984328"
13 | );
14 | const subOrder = order.shr(3);
15 | const p = bn128.r;
16 |
17 | function addPoint(a, b) {
18 | const q = bn128.r;
19 | const cta = bigInt("168700");
20 | const d = bigInt("168696");
21 | const res = [];
22 |
23 | res[0] = bigInt(
24 | bigInt(a[0])
25 | .mul(b[1])
26 | .add(bigInt(b[0]).mul(a[1]))
27 | .mul(
28 | bigInt(
29 | bigInt("1").add(d.mul(a[0]).mul(b[0]).mul(a[1]).mul(b[1]))
30 | ).inverse(q)
31 | )
32 | ).affine(q);
33 | res[1] = bigInt(
34 | bigInt(a[1])
35 | .mul(b[1])
36 | .sub(cta.mul(a[0]).mul(b[0]))
37 | .mul(
38 | bigInt(
39 | bigInt("1").sub(d.mul(a[0]).mul(b[0]).mul(a[1]).mul(b[1]))
40 | ).inverse(q)
41 | )
42 | ).affine(q);
43 |
44 | return res;
45 | }
46 |
47 | function mulPointEscalar(base, e) {
48 | let res = [bigInt("0"), bigInt("1")];
49 | let rem = bigInt(e);
50 | let exp = base;
51 |
52 | while (!rem.isZero()) {
53 | if (rem.isOdd()) {
54 | res = addPoint(res, exp);
55 | }
56 | exp = addPoint(exp, exp);
57 | rem = rem.shr(1);
58 | }
59 |
60 | return res;
61 | }
62 |
63 | function inSubgroup(P) {
64 | if (!inCurve(P)) return false;
65 | const res = mulPointEscalar(P, exports.subOrder);
66 | return res[0].equals(bigInt(0)) && res[1].equals(bigInt(1));
67 | }
68 |
69 | function inCurve(P) {
70 | const F = bn128.Fr;
71 |
72 | const a = bigInt("168700");
73 | const d = bigInt("168696");
74 |
75 | const x2 = F.square(P[0]);
76 | const y2 = F.square(P[1]);
77 |
78 | if (!F.equals(F.add(F.mul(a, x2), y2), F.add(F.one, F.mul(F.mul(x2, y2), d))))
79 | return false;
80 |
81 | return true;
82 | }
83 |
84 | function packPoint(P) {
85 | const buff = bigInt.leInt2Buff(P[1], 32);
86 | if (P[0].greater(exports.p.shr(1))) {
87 | buff[31] = buff[31] | 0x80;
88 | }
89 | return buff;
90 | }
91 |
92 | function unpackPoint(_buff) {
93 | const F = bn128.Fr;
94 |
95 | const buff = Buffer.from(_buff);
96 | let sign = false;
97 | const P = new Array(2);
98 | if (buff[31] & 0x80) {
99 | sign = true;
100 | buff[31] = buff[31] & 0x7f;
101 | }
102 | P[1] = bigInt.leBuff2int(buff);
103 | if (P[1].greaterOrEquals(exports.p)) return null;
104 |
105 | const a = bigInt("168700");
106 | const d = bigInt("168696");
107 |
108 | const y2 = F.square(P[1]);
109 |
110 | let x = F.sqrt(F.div(F.sub(F.one, y2), F.sub(a, F.mul(d, y2))));
111 |
112 | if (x == null) return null;
113 |
114 | if (sign) x = F.neg(x);
115 |
116 | P[0] = F.affine(x);
117 |
118 | return P;
119 | }
120 |
121 | export default {
122 | addPoint,
123 | mulPointEscalar,
124 | inCurve,
125 | inSubgroup,
126 | packPoint,
127 | unpackPoint,
128 | Base8,
129 | order,
130 | subOrder,
131 | p,
132 | };
133 |
--------------------------------------------------------------------------------
/src/lightcone/sign/bitstream.js:
--------------------------------------------------------------------------------
1 | import { BigNumber } from "bignumber.js";
2 | import BN from "bn.js";
3 | import abi from "ethereumjs-abi";
4 |
5 | const assert = require("assert");
6 |
7 | class Bitstream {
8 | constructor(initialData = "") {
9 | this.data = initialData;
10 | if (this.data.startsWith("0x")) {
11 | this.data = this.data.slice(2);
12 | }
13 | }
14 |
15 | getData() {
16 | if (this.data.length === 0) {
17 | return "0x";
18 | } else {
19 | return "0x" + this.data;
20 | }
21 | }
22 |
23 | getBytes32Array() {
24 | if (this.data.length === 0) {
25 | return [];
26 | } else {
27 | assert.equal(
28 | this.length() % 32,
29 | 0,
30 | "Bitstream not compatible with bytes32[]"
31 | );
32 | return this.data.match(/.{1,64}/g).map((element) => "0x" + element);
33 | }
34 | }
35 |
36 | addBigNumber(x, numBytes = 32) {
37 | const formattedData = this.padString(x.toString(16), numBytes * 2);
38 | return this.insert(formattedData);
39 | }
40 |
41 | addBN(x, numBytes = 32) {
42 | const formattedData = this.padString(x.toString(16), numBytes * 2);
43 | return this.insert(formattedData);
44 | }
45 |
46 | addNumber(x, numBytes = 4) {
47 | // Check if we need to encode this number as negative
48 | if (x < 0) {
49 | const encoded = abi.rawEncode(["int256"], [x.toString(10)]);
50 | const hex = encoded.toString("hex").slice(-(numBytes * 2));
51 | return this.addHex(hex);
52 | } else {
53 | return this.addBigNumber(new BigNumber(x), numBytes);
54 | }
55 | }
56 |
57 | addAddress(x, numBytes = 20) {
58 | const formattedData = this.padString(x.slice(2), numBytes * 2);
59 | return this.insert(formattedData);
60 | }
61 |
62 | addHex(x) {
63 | if (x.startsWith("0x")) {
64 | return this.insert(x.slice(2));
65 | } else {
66 | return this.insert(x);
67 | }
68 | }
69 |
70 | extractUint8(offset) {
71 | return parseInt(this.extractData(offset, 1), 16);
72 | }
73 |
74 | extractUint16(offset) {
75 | return parseInt(this.extractData(offset, 2), 16);
76 | }
77 |
78 | extractUint24(offset) {
79 | return parseInt(this.extractData(offset, 3), 16);
80 | }
81 |
82 | extractUint32(offset) {
83 | return parseInt(this.extractData(offset, 4), 16);
84 | }
85 |
86 | extractUint40(offset) {
87 | return parseInt(this.extractData(offset, 5), 16);
88 | }
89 |
90 | extractUint48(offset) {
91 | return parseInt(this.extractData(offset, 6), 16);
92 | }
93 |
94 | extractUint56(offset) {
95 | return new BN(this.extractData(offset, 7), 16);
96 | }
97 |
98 | extractUint64(offset) {
99 | return new BN(this.extractData(offset, 8), 16);
100 | }
101 |
102 | extractUint(offset) {
103 | return new BN(this.extractData(offset, 32), 16);
104 | }
105 |
106 | extractAddress(offset) {
107 | return "0x" + this.extractData(offset, 20);
108 | }
109 |
110 | extractBytes1(offset) {
111 | return this.extractBytesX(offset, 1);
112 | }
113 |
114 | extractBytes32(offset) {
115 | return this.extractBytesX(offset, 32);
116 | }
117 |
118 | extractBytesX(offset, length) {
119 | return new Buffer(this.extractData(offset, length), "hex");
120 | }
121 |
122 | extractChar(offset) {
123 | return this.extractData(offset, 1);
124 | }
125 |
126 | extractData(offset, length) {
127 | const start = offset * 2;
128 | const end = start + length * 2;
129 | if (this.data.length < end) {
130 | throw new Error(
131 | "substring index out of range:[" + start + ", " + end + "]"
132 | );
133 | }
134 | return this.data.slice(start, end);
135 | }
136 |
137 | // Returns the number of bytes of data
138 | length() {
139 | return this.data.length / 2;
140 | }
141 |
142 | insert(x) {
143 | const offset = this.length();
144 | this.data += x;
145 | return offset;
146 | }
147 | }
148 |
149 | function padString(x, targetLength) {
150 | if (x.length > targetLength) {
151 | throw Error(
152 | "0x" +
153 | x +
154 | " is too big to fit in the requested length (" +
155 | targetLength +
156 | ")"
157 | );
158 | }
159 | while (x.length < targetLength) {
160 | x = "0" + x;
161 | }
162 | return x;
163 | }
164 |
165 | export { Bitstream, padString };
166 |
--------------------------------------------------------------------------------
/src/lightcone/sign/constants.js:
--------------------------------------------------------------------------------
1 | import BN from "bn.js";
2 |
3 | const TREE_DEPTH_TRADING_HISTORY = 14;
4 | const TREE_DEPTH_ACCOUNTS = 20;
5 | const TREE_DEPTH_TOKENS = 8;
6 | const NUM_BITS_ACCOUNTID = TREE_DEPTH_ACCOUNTS;
7 | const NUM_BITS_ORDERID = 20;
8 | const NUM_BITS_LABEL = 32;
9 | const MAX_NUM_TOKENS = 2 ** 8;
10 | const MAX_AMOUNT = new BN(2).pow(new BN(96)).sub(new BN(1));
11 |
12 | const Float28Encoding = {
13 | numBitsExponent: 5,
14 | numBitsMantissa: 23,
15 | exponentBase: 10,
16 | };
17 | const Float24Encoding = {
18 | numBitsExponent: 5,
19 | numBitsMantissa: 19,
20 | exponentBase: 10,
21 | };
22 | const Float16Encoding = {
23 | numBitsExponent: 5,
24 | numBitsMantissa: 11,
25 | exponentBase: 10,
26 | };
27 |
28 | const emptyBytes = [];
29 | const zeroAddress = "0x" + "00".repeat(20);
30 | const scalarField = new BN(
31 | "21888242871839275222246405745257275088548364400416034343698204186575808495617",
32 | 10
33 | );
34 |
35 | export default {
36 | TREE_DEPTH_TRADING_HISTORY,
37 | TREE_DEPTH_ACCOUNTS,
38 | TREE_DEPTH_TOKENS,
39 | NUM_BITS_ACCOUNTID,
40 | NUM_BITS_ORDERID,
41 | NUM_BITS_LABEL,
42 | MAX_NUM_TOKENS,
43 | MAX_AMOUNT,
44 | Float28Encoding,
45 | Float24Encoding,
46 | Float16Encoding,
47 | emptyBytes,
48 | zeroAddress,
49 | scalarField,
50 | };
51 |
--------------------------------------------------------------------------------
/src/lightcone/sign/eddsa.js:
--------------------------------------------------------------------------------
1 | // Taken and modified from
2 | // https://github.com/iden3/circomlib
3 |
4 | import { bigInt } from "snarkjs";
5 | import { createHash } from "./poseidon";
6 |
7 | import babyJub from "./babyjub";
8 | import createBlakeHash from "blake-hash";
9 |
10 | function generateKeyPair(seed) {
11 | const secretKey = bigInt.leBuff2int(seed).mod(babyJub.subOrder);
12 | const publicKey = babyJub.mulPointEscalar(babyJub.Base8, secretKey);
13 | return {
14 | publicKeyX: publicKey[0].toString(10),
15 | publicKeyY: publicKey[1].toString(10),
16 | secretKey: secretKey.toString(10),
17 | };
18 | }
19 |
20 | function generatePubKeyFromPrivate(secretKey) {
21 | const publicKey = babyJub.mulPointEscalar(babyJub.Base8, bigInt(secretKey));
22 | return {
23 | publicKeyX: publicKey[0].toString(10),
24 | publicKeyY: publicKey[1].toString(10),
25 | };
26 | }
27 |
28 | function sign(strKey, msg) {
29 | const key = bigInt(strKey);
30 | const prv = bigInt.leInt2Buff(key, 32);
31 |
32 | const h1 = createBlakeHash("blake512").update(prv).digest();
33 | const msgBuff = bigInt.leInt2Buff(bigInt(msg), 32);
34 | const rBuff = createBlakeHash("blake512")
35 | .update(Buffer.concat([h1.slice(32, 64), msgBuff]))
36 | .digest();
37 | let r = bigInt.leBuff2int(rBuff);
38 | r = r.mod(babyJub.subOrder);
39 |
40 | const A = babyJub.mulPointEscalar(babyJub.Base8, key);
41 | const R8 = babyJub.mulPointEscalar(babyJub.Base8, r);
42 |
43 | const hasher = createHash(6, 6, 52);
44 | const hm = hasher([R8[0], R8[1], A[0], A[1], msg]);
45 | const S = r.add(hm.mul(key)).mod(babyJub.subOrder);
46 |
47 | return {
48 | Rx: R8[0].toString(),
49 | Ry: R8[1].toString(),
50 | s: S.toString(),
51 | };
52 | }
53 |
54 | function verify(msg, sig, pubKey) {
55 | const A = [bigInt(pubKey[0]), bigInt(pubKey[1])];
56 | const R = [bigInt(sig.Rx), bigInt(sig.Ry)];
57 | const S = bigInt(sig.s);
58 |
59 | // Check parameters
60 | if (!babyJub.inCurve(R)) return false;
61 | if (!babyJub.inCurve(A)) return false;
62 | if (S >= babyJub.subOrder) return false;
63 |
64 | const hasher = createHash(6, 6, 52);
65 | const hm = hasher([R[0], R[1], A[0], A[1], bigInt(msg)]);
66 |
67 | const Pleft = babyJub.mulPointEscalar(babyJub.Base8, S);
68 | let Pright = babyJub.mulPointEscalar(A, hm);
69 | Pright = babyJub.addPoint(R, Pright);
70 |
71 | if (!Pleft[0].equals(Pright[0])) return false;
72 | if (!Pleft[1].equals(Pright[1])) return false;
73 |
74 | return true;
75 | }
76 |
77 | export default {
78 | generateKeyPair,
79 | sign,
80 | verify,
81 | generatePubKeyFromPrivate,
82 | };
83 |
--------------------------------------------------------------------------------
/src/lightcone/sign/float.js:
--------------------------------------------------------------------------------
1 | import BN from "bn.js";
2 | const assert = require("assert");
3 |
4 | export function toFloat(value, encoding) {
5 | const maxExponent = (1 << encoding.numBitsExponent) - 1;
6 | const maxMantissa = (1 << encoding.numBitsMantissa) - 1;
7 | const maxExponentValue = new BN(encoding.exponentBase).pow(
8 | new BN(maxExponent)
9 | );
10 | const maxValue = new BN(maxMantissa).mul(maxExponentValue);
11 | assert(
12 | value.lte(maxValue),
13 | "Value too large: " +
14 | value.toString(10) +
15 | ", max value: " +
16 | maxValue.toString(10)
17 | );
18 |
19 | let exponent = 0;
20 | let r = value.div(new BN(maxMantissa));
21 | let d = new BN(1);
22 | while (
23 | r.gte(new BN(encoding.exponentBase)) ||
24 | d.mul(new BN(maxMantissa)).lt(value)
25 | ) {
26 | r = r.div(new BN(encoding.exponentBase));
27 | exponent += 1;
28 | d = d.mul(new BN(encoding.exponentBase));
29 | }
30 | const mantissa = value.div(d).toNumber();
31 |
32 | assert(exponent <= maxExponent, "Exponent too large");
33 | assert(mantissa <= maxMantissa, "Mantissa too large");
34 | const f = (exponent << encoding.numBitsMantissa) + mantissa;
35 | return f;
36 | }
37 |
38 | export function fromFloat(f, encoding) {
39 | const exponent = f >> encoding.numBitsMantissa;
40 | const mantissa = f & ((1 << encoding.numBitsMantissa) - 1);
41 | return new BN(mantissa).mul(
42 | new BN(encoding.exponentBase).pow(new BN(exponent))
43 | );
44 | }
45 |
46 | export function roundToFloatValue(value, encoding) {
47 | const f = toFloat(value, encoding);
48 | const floatValue = fromFloat(f, encoding);
49 | assert(
50 | floatValue.lte(value),
51 | "float value can never be higher than the original value"
52 | );
53 | return floatValue;
54 | }
55 |
--------------------------------------------------------------------------------
/src/lightcone/sign/poseidon.js:
--------------------------------------------------------------------------------
1 | // Taken from
2 | // https://github.com/iden3/circomlib
3 |
4 | import { bigInt, bn128 } from "snarkjs";
5 | import blake2b from "blake2b";
6 | const assert = require("assert");
7 |
8 | const F = bn128.Fr;
9 | const SEED = "poseidon";
10 | const NROUNDSF = 8;
11 | const NROUNDSP = 57;
12 | const T = 6;
13 |
14 | function getPseudoRandom(seed, n) {
15 | const res = [];
16 | let input = Buffer.from(seed);
17 | let h = blake2b(32).update(input).digest();
18 | while (res.length < n) {
19 | const n = F.affine(bigInt.leBuff2int(h));
20 | res.push(n);
21 | h = blake2b(32).update(h).digest();
22 | }
23 |
24 | return res;
25 | }
26 |
27 | function allDifferent(v) {
28 | for (let i = 0; i < v.length; i++) {
29 | if (v[i].isZero()) return false;
30 | for (let j = i + 1; j < v.length; j++) {
31 | if (v[i].equals(v[j])) return false;
32 | }
33 | }
34 | return true;
35 | }
36 |
37 | export function getMatrix(t, seed, nRounds) {
38 | if (typeof seed === "undefined") seed = SEED;
39 | if (typeof nRounds === "undefined") nRounds = NROUNDSF + NROUNDSP;
40 | if (typeof t === "undefined") t = T;
41 | let nonce = "0000";
42 | let cmatrix = getPseudoRandom(seed + "_matrix_" + nonce, t * 2);
43 | while (!allDifferent(cmatrix)) {
44 | nonce = Number(nonce) + 1 + "";
45 | while (nonce.length < 4) nonce = "0" + nonce;
46 | cmatrix = getPseudoRandom(seed + "_matrix_" + nonce, t * 2);
47 | }
48 |
49 | const M = new Array(t);
50 | for (let i = 0; i < t; i++) {
51 | M[i] = new Array(t);
52 | for (let j = 0; j < t; j++) {
53 | M[i][j] = F.affine(F.inverse(F.sub(cmatrix[i], cmatrix[t + j])));
54 | }
55 | }
56 | return M;
57 | }
58 |
59 | export function getConstants(t, seed, nRounds) {
60 | if (typeof seed === "undefined") seed = SEED;
61 | if (typeof nRounds === "undefined") nRounds = NROUNDSF + NROUNDSP;
62 | if (typeof t === "undefined") t = T;
63 | return getPseudoRandom(seed + "_constants", nRounds);
64 | }
65 |
66 | function ark(state, c) {
67 | for (let j = 0; j < state.length; j++) {
68 | state[j] = F.add(state[j], c);
69 | }
70 | }
71 |
72 | function sigma(a) {
73 | return F.mul(a, F.square(F.square(a, a)));
74 | }
75 |
76 | function mix(state, M) {
77 | const newState = new Array(state.length);
78 | for (let i = 0; i < state.length; i++) {
79 | newState[i] = F.zero;
80 | for (let j = 0; j < state.length; j++) {
81 | newState[i] = F.add(newState[i], F.mul(M[i][j], state[j]));
82 | }
83 | }
84 | for (let i = 0; i < state.length; i++) state[i] = newState[i];
85 | }
86 |
87 | export function createHash(t, nRoundsF, nRoundsP, seed) {
88 | if (typeof seed === "undefined") seed = SEED;
89 | if (typeof nRoundsF === "undefined") nRoundsF = NROUNDSF;
90 | if (typeof nRoundsP === "undefined") nRoundsP = NROUNDSP;
91 | if (typeof t === "undefined") t = T;
92 |
93 | assert(nRoundsF % 2 === 0);
94 | const C = getConstants(t, seed, nRoundsF + nRoundsP);
95 | const M = getMatrix(t, seed, nRoundsF + nRoundsP);
96 | return function (inputs) {
97 | let state = [];
98 | assert(inputs.length < t);
99 | assert(inputs.length > 0);
100 | for (let i = 0; i < inputs.length; i++) state[i] = bigInt(inputs[i]);
101 | for (let i = inputs.length; i < t; i++) state[i] = F.zero;
102 |
103 | for (let i = 0; i < nRoundsF + nRoundsP; i++) {
104 | ark(state, C[i]);
105 | if (i < nRoundsF / 2 || i >= nRoundsF / 2 + nRoundsP) {
106 | for (let j = 0; j < t; j++) state[j] = sigma(state[j]);
107 | } else {
108 | state[0] = sigma(state[0]);
109 | }
110 | mix(state, M);
111 | }
112 | return F.affine(state[0]);
113 | };
114 | }
115 |
--------------------------------------------------------------------------------
/src/lightcone/wallet/index.js:
--------------------------------------------------------------------------------
1 | import Wallet from "./wallet";
2 |
3 | export default Wallet;
4 |
--------------------------------------------------------------------------------
/src/lightcone/wallet/wallet.js:
--------------------------------------------------------------------------------
1 | import * as exchange from "../sign/exchange";
2 | import * as fm from "../common/formatter";
3 | import {
4 | personalSign,
5 | sendTransaction,
6 | signEthereumTx,
7 | } from "../ethereum/metaMask";
8 | import { sha256 } from "ethereumjs-util";
9 | import Contracts from "../ethereum/contracts/Contracts";
10 | import config from "../config";
11 |
12 | const assert = require("assert");
13 |
14 | export default class Wallet {
15 | keyMessage = "Sign this message to access Loopring Exchange: ";
16 | transferMessage = "Sign this message to authorize Loopring Pay: ";
17 |
18 | constructor(walletType, web3, address, accountId = -1, keyPair = null) {
19 | this.walletType = walletType;
20 | this.web3 = web3;
21 | this.address = address;
22 | this.accountId = accountId;
23 | this.keyPair = keyPair;
24 | }
25 |
26 | /**
27 | * Approve Zero
28 | * @param tokenAddress: approve token symbol to zero
29 | * @param nonce: Ethereum nonce of this address
30 | * @param gasPrice: gas price in gwei
31 | * @param sendByMetaMask
32 | */
33 | async approveZero(
34 | tokenAddress,
35 | exchangeAddress,
36 | chainId,
37 | nonce,
38 | gasPrice,
39 | sendByMetaMask = false
40 | ) {
41 | const rawTx = {
42 | from: this.address,
43 | to: tokenAddress,
44 | value: "0",
45 | data: Contracts.ERC20Token.encodeInputs("approve", {
46 | _spender: exchangeAddress,
47 | _value: "0x0",
48 | }),
49 | chainId: chainId,
50 | nonce: nonce.toString(),
51 | gasPrice: fm.fromGWEI(gasPrice).toString(),
52 | gas: config.getGasLimitByType("approve").gas.toString(),
53 | };
54 |
55 | const response = sendByMetaMask
56 | ? await sendTransaction(this.web3, rawTx)
57 | : await signEthereumTx(this.web3, this.address, rawTx);
58 | return response["result"];
59 | }
60 |
61 | /**
62 | * Approve Max
63 | * @param tokenAddress: approve token symbol to max
64 | * @param nonce: Ethereum nonce of this address
65 | * @param gasPrice: gas price in gwei
66 | * @param sendByMetaMask
67 | */
68 | async approveMax(
69 | tokenAddress,
70 | exchangeAddress,
71 | chainId,
72 | nonce,
73 | gasPrice,
74 | sendByMetaMask = false
75 | ) {
76 | const rawTx = {
77 | from: this.address,
78 | to: tokenAddress,
79 | value: "0",
80 | data: Contracts.ERC20Token.encodeInputs("approve", {
81 | _spender: exchangeAddress,
82 | _value: config.getMaxAmountInWEI(),
83 | }),
84 | chainId: chainId,
85 | nonce: nonce.toString(),
86 | gasPrice: fm.fromGWEI(gasPrice).toString(),
87 | gas: config.getGasLimitByType("approve").gas.toString(),
88 | };
89 | const response = sendByMetaMask
90 | ? await sendTransaction(this.web3, rawTx)
91 | : await signEthereumTx(this.web3, this.address, rawTx);
92 |
93 | return response["result"];
94 | }
95 |
96 | /**
97 | * generate key pair of account in DEX
98 | */
99 | async generateKeyPair(exchangeAddress, keyNonce) {
100 | assert(this.address !== null);
101 |
102 | const result = await personalSign(
103 | this.web3,
104 | this.address,
105 | this.keyMessage + exchangeAddress + " with key nonce: " + keyNonce
106 | );
107 |
108 | if (!result.error) {
109 | return {
110 | keyPair: exchange.generateKeyPair(sha256(fm.toBuffer(result.sig))),
111 | };
112 | } else return result;
113 | }
114 |
115 | /**
116 | * verify EDDSA Private Key of account in DEX
117 | */
118 | async verify(exchangeAddress, keyNonce) {
119 | assert(this.address !== null);
120 |
121 | const result = await personalSign(
122 | this.web3,
123 | this.address,
124 | this.keyMessage + exchangeAddress + " with key nonce: " + keyNonce
125 | );
126 |
127 | if (result.error) {
128 | throw new Error(result.error.message);
129 | }
130 |
131 | return exchange.verify(
132 | this.keyPair.publicKeyX,
133 | this.keyPair.publicKeyY,
134 | sha256(fm.toBuffer(result.sig))
135 | );
136 | }
137 |
138 | /**
139 | * create Or Update Account in DEX
140 | * @param gasPrice: in gwei
141 | * @param nonce: Ethereum nonce of this address
142 | * @param permission: user permission
143 | * @param sendByMetaMask
144 | */
145 |
146 | /**
147 | *
148 | * exchangeAddress,
149 | fee,
150 | chainId,
151 | publicX,
152 | publicY,
153 | token,
154 | amount,
155 | permission,
156 | nonce,
157 | gasPrice
158 | *
159 | * */
160 | async createOrUpdateAccount(keyPair, payload, sendByMetaMask = false) {
161 | payload["from"] = this.address;
162 |
163 | const tx = exchange.createAccountAndDeposit({
164 | ...payload,
165 | publicX: keyPair.publicKeyX,
166 | publicY: keyPair.publicKeyY,
167 | });
168 |
169 | const response = sendByMetaMask
170 | ? await sendTransaction(this.web3, tx)
171 | : await signEthereumTx(this.web3, this.address, tx);
172 |
173 | return response["result"];
174 | }
175 |
176 | /**
177 | * Deposit to Dex
178 | * @param payload
179 | * @param sendByMetaMask
180 | */
181 | async depositTo(payload, sendByMetaMask = false) {
182 | payload["from"] = this.address;
183 | const tx = exchange.deposit(payload);
184 | const response = sendByMetaMask
185 | ? await sendTransaction(this.web3, tx)
186 | : await signEthereumTx(this.web3, this.address, tx);
187 | return response["result"];
188 | }
189 |
190 | /**
191 | * On-chain Withdrawal from Dex
192 | * @param payload
193 | * @param sendByMetaMask
194 | */
195 | async onchainWithdrawal(payload, sendByMetaMask) {
196 | payload["from"] = this.address;
197 | const tx = exchange.onChainWithdraw(payload);
198 | const response = sendByMetaMask
199 | ? await sendTransaction(this.web3, tx)
200 | : await signEthereumTx(this.web3, this.address, tx);
201 | return response["result"];
202 | }
203 |
204 | /**
205 | * Off-chain Withdrawal from Dex
206 | * @param accountId: account ID in exchange
207 | * @param nonce: DEX nonce of account
208 | * @param token: token symbol or address to withdraw
209 | * @param amount: amount to withdraw, in decimal string. e.g. '15'
210 | * @param tokenF: fee token symbol or address to withdraw
211 | * @param amountF: withdrawal fee, in decimal string. e.g. '15'
212 | * @param label: label used in protocol
213 | */
214 | offchainWithdrawal(accountId, nonce, token, amount, tokenF, amountF, label) {
215 | const withdraw = {
216 | accountId: this.accountId,
217 | nonce: nonce,
218 | token: token,
219 | amount: amount,
220 | tokenF: tokenF,
221 | amountF: amountF,
222 | label: label,
223 | };
224 | return exchange.signWithdrawal(withdraw, this.keyPair);
225 | }
226 |
227 | /**
228 | * Get signed order, should be submitted by frontend itself TEMPORARY
229 | * @param tokenS: symbol or hex address of token sell
230 | * @param tokenB: symbol or hex address of token buy
231 | * @param amountS: amount of token sell, in string number
232 | * @param amountB: amount of token buy, in string number
233 | * @param orderId: next order ID, needed by order signature
234 | * @param validSince: valid beginning period of this order, SECOND in timestamp
235 | * @param validUntil: valid ending period of this order, SECOND in timestamp
236 | * @param label label used in protocol
237 | * @param buy boolean
238 | * @param channelId
239 | */
240 | submitOrder(
241 | tokens,
242 | exchangeId,
243 | tokenS,
244 | tokenB,
245 | amountS,
246 | amountB,
247 | orderId,
248 | validSince,
249 | validUntil,
250 | label,
251 | buy,
252 | channelId
253 | ) {
254 | const order = {
255 | exchangeId,
256 | owner: this.address,
257 | accountId: this.accountId,
258 | tokenS: tokenS,
259 | tokenB: tokenB,
260 | amountS: amountS,
261 | amountB: amountB,
262 | orderId: orderId,
263 | validSince: Math.floor(validSince),
264 | validUntil: Math.floor(validUntil),
265 | label: label,
266 | buy: buy,
267 | channelId,
268 | };
269 |
270 | return exchange.signOrder(order, this.keyPair, tokens);
271 | }
272 |
273 | /**
274 | * Cancel order in Dex
275 | * @param nonce: DEX nonce of account
276 | * @param orderToken: token symbol or address of cancel
277 | * @param orderId: specified order id to cancel
278 | * @param tokenF: amountF token symbol or address of cancel
279 | * @param amountF: cancel amountF, e.g. '15'
280 | * @param label: [OPTIONAL] label used in protocol
281 | */
282 | submitCancel(nonce, orderToken, orderId, tokenF, amountF, label) {
283 | const cancel = {
284 | accountId: this.accountId,
285 | nonce: nonce,
286 | orderToken: orderToken,
287 | orderId: orderId,
288 | tokenF: tokenF,
289 | amountF: amountF,
290 | label: label,
291 | };
292 |
293 | return exchange.signCancel(cancel, this.keyPair);
294 | }
295 |
296 | /**
297 | * Get Api Key signature
298 | */
299 | getApiKey() {
300 | const request = {
301 | accountId: this.accountId,
302 | };
303 | return exchange.signGetApiKey(request, this.keyPair);
304 | }
305 |
306 | applyApiKey() {
307 | const request = {
308 | accountId: this.accountId,
309 | };
310 | return exchange.signApplyApiKey(request, this.keyPair);
311 | }
312 |
313 | /**
314 | * Get Api Key signature
315 | * @param orderHash: [OPTIONAL] specified order hash to cancel
316 | * @param clientOrderId: [OPTIONAL] specified client order ID to cancel
317 | */
318 | submitFlexCancel(orderHash = "", clientOrderId = "") {
319 | const request = {
320 | accountId: this.accountId,
321 | orderHash: orderHash,
322 | clientOrderId: clientOrderId,
323 | };
324 | return exchange.signFlexCancel(request, this.keyPair);
325 | }
326 |
327 | /**
328 | * Generate Hebao Guadian message
329 | */
330 | async generateHebaoGuadianMessage(type, message) {
331 | assert(this.address !== null);
332 |
333 | const result = await personalSign(this.web3, this.address, message);
334 |
335 | if (result.error) {
336 | throw new Error(result.error.message);
337 | }
338 |
339 | return {
340 | address: this.address,
341 | sig: result["sig"],
342 | type,
343 | };
344 | }
345 |
346 | async signTransfer(transfer, tokens) {
347 | transfer.sender = this.accountId;
348 | const signedTransfer = exchange.signTransfer(
349 | transfer,
350 | this.keyPair,
351 | tokens
352 | );
353 |
354 | const encodeTransfer = exchange.encodeTransfer(signedTransfer);
355 | const result = await personalSign(
356 | this.web3,
357 | this.address,
358 | `${this.transferMessage}${encodeTransfer}`
359 | );
360 |
361 | if (!result.error) {
362 | return {
363 | ecdsaSig: result.sig,
364 | transfer: signedTransfer,
365 | };
366 | } else return result;
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/src/reducers/i18n/index.js:
--------------------------------------------------------------------------------
1 | import { SWITCH_LANGUAGE } from "../../actions/i18n";
2 |
3 | const initialState = {
4 | selectedLanguage: "en",
5 | };
6 |
7 | export const i18nReducer = (state = initialState, action) => {
8 | const { type } = action;
9 | switch (type) {
10 | case SWITCH_LANGUAGE: {
11 | return {
12 | ...state,
13 | selectedLanguage: action.language,
14 | };
15 | }
16 | default: {
17 | return state;
18 | }
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { web3Reducer } from "./web3";
3 | import { loopringReducer } from "./loopring";
4 | import { i18nReducer } from "./i18n";
5 |
6 | export const reducers = combineReducers({
7 | web3: web3Reducer,
8 | loopring: loopringReducer,
9 | i18n: i18nReducer,
10 | });
11 |
--------------------------------------------------------------------------------
/src/reducers/loopring/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOGIN_SUCCESS,
3 | GET_SUPPORTED_TOKENS_SUCCESS,
4 | GET_SUPPORTED_TOKENS_START,
5 | GET_SUPPORTED_TOKENS_END,
6 | GET_USER_BALANCES_SUCCESS,
7 | GET_USER_BALANCES_START,
8 | GET_USER_BALANCES_END,
9 | GET_SWAP_DATA_START,
10 | GET_SWAP_DATA_END,
11 | GET_SWAP_DATA_SUCCESS,
12 | GET_SUPPORTED_MARKETS_START,
13 | GET_SUPPORTED_MARKETS_END,
14 | GET_SUPPORTED_MARKETS_SUCCESS,
15 | POST_SWAP_START,
16 | POST_SWAP_END,
17 | LOGOUT,
18 | RESET_SWAP_DATA,
19 | } from "../../actions/loopring";
20 |
21 | const initialState = {
22 | account: null,
23 | wallet: null,
24 | exchange: null,
25 | supportedTokens: {
26 | loadings: 0,
27 | data: [],
28 | },
29 | supportedMarkets: {
30 | loadings: 0,
31 | data: [],
32 | },
33 | balances: {
34 | loadings: 0,
35 | data: [],
36 | },
37 | swap: {
38 | loadings: 0,
39 | data: {},
40 | },
41 | swapSubmission: {
42 | loadings: 0,
43 | },
44 | };
45 |
46 | export const loopringReducer = (state = initialState, action) => {
47 | const { type } = action;
48 | switch (type) {
49 | case LOGIN_SUCCESS: {
50 | return {
51 | ...state,
52 | account: action.account,
53 | wallet: action.wallet,
54 | exchange: action.exchange,
55 | };
56 | }
57 | case GET_SUPPORTED_TOKENS_START: {
58 | return {
59 | ...state,
60 | supportedTokens: {
61 | ...state.supportedTokens,
62 | loadings: state.supportedTokens.loadings + 1,
63 | },
64 | };
65 | }
66 | case GET_SUPPORTED_TOKENS_END: {
67 | return {
68 | ...state,
69 | supportedTokens: {
70 | ...state.supportedTokens,
71 | loadings: state.supportedTokens.loadings - 1,
72 | },
73 | };
74 | }
75 | case GET_SUPPORTED_TOKENS_SUCCESS: {
76 | return {
77 | ...state,
78 | supportedTokens: {
79 | ...state.supportedTokens,
80 | data: action.supportedTokens,
81 | },
82 | };
83 | }
84 | case GET_SUPPORTED_MARKETS_START: {
85 | return {
86 | ...state,
87 | supportedMarkets: {
88 | ...state.supportedMarkets,
89 | loadings: state.supportedMarkets.loadings + 1,
90 | },
91 | };
92 | }
93 | case GET_SUPPORTED_MARKETS_END: {
94 | return {
95 | ...state,
96 | supportedMarkets: {
97 | ...state.supportedMarkets,
98 | loadings: state.supportedMarkets.loadings - 1,
99 | },
100 | };
101 | }
102 | case GET_SUPPORTED_MARKETS_SUCCESS: {
103 | return {
104 | ...state,
105 | supportedMarkets: {
106 | ...state.supportedMarkets,
107 | data: action.supportedMarkets,
108 | },
109 | };
110 | }
111 | case GET_USER_BALANCES_START: {
112 | return {
113 | ...state,
114 | balances: {
115 | ...state.balances,
116 | loadings: state.balances.loadings + 1,
117 | },
118 | };
119 | }
120 | case GET_USER_BALANCES_END: {
121 | return {
122 | ...state,
123 | balances: {
124 | ...state.balances,
125 | loadings: state.balances.loadings - 1,
126 | },
127 | };
128 | }
129 | case GET_USER_BALANCES_SUCCESS: {
130 | return {
131 | ...state,
132 | balances: {
133 | ...state.balances,
134 | data: action.balances,
135 | },
136 | };
137 | }
138 | case GET_SWAP_DATA_START: {
139 | return {
140 | ...state,
141 | swap: {
142 | ...state.swap,
143 | loadings: state.swap.loadings + 1,
144 | },
145 | };
146 | }
147 | case GET_SWAP_DATA_END: {
148 | return {
149 | ...state,
150 | swap: {
151 | ...state.swap,
152 | loadings: state.swap.loadings - 1,
153 | },
154 | };
155 | }
156 | case GET_SWAP_DATA_SUCCESS: {
157 | return {
158 | ...state,
159 | swap: {
160 | ...state.swap,
161 | data: {
162 | averageFillPrice: action.averageFillPrice,
163 | slippagePercentage: action.slippagePercentage,
164 | maximumAmount: action.maximumAmount,
165 | feePercentage: action.feePercentage,
166 | },
167 | },
168 | };
169 | }
170 | case POST_SWAP_START: {
171 | return {
172 | ...state,
173 | swapSubmission: {
174 | ...state.swapSubmission,
175 | loadings: state.swapSubmission.loadings + 1,
176 | },
177 | };
178 | }
179 | case POST_SWAP_END: {
180 | return {
181 | ...state,
182 | swapSubmission: {
183 | ...state.swapSubmission,
184 | loadings: state.swapSubmission.loadings - 1,
185 | },
186 | };
187 | }
188 | case RESET_SWAP_DATA: {
189 | return {
190 | ...state,
191 | swap: {
192 | loadings: 0,
193 | data: {},
194 | },
195 | };
196 | }
197 | case LOGOUT: {
198 | return {
199 | ...initialState,
200 | supportedTokens: state.supportedTokens,
201 | supportedMarkets: state.supportedMarkets,
202 | };
203 | }
204 | default: {
205 | return state;
206 | }
207 | }
208 | };
209 |
--------------------------------------------------------------------------------
/src/reducers/web3/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | INITIALIZE_WEB3_SUCCESS,
3 | WEB3_ACCOUNT_CHANGED,
4 | CHAIN_ID_CHANGED,
5 | } from "../../actions/web3";
6 | import { LOGOUT } from "../../actions/loopring";
7 |
8 | const initialState = {
9 | instance: null,
10 | selectedAccount: "",
11 | chainId: null,
12 | };
13 |
14 | export const web3Reducer = (state = initialState, action) => {
15 | const { type } = action;
16 | switch (type) {
17 | case INITIALIZE_WEB3_SUCCESS: {
18 | return {
19 | ...state,
20 | instance: action.web3,
21 | selectedAccount: action.selectedAccount,
22 | };
23 | }
24 | case WEB3_ACCOUNT_CHANGED: {
25 | return { ...state, selectedAccount: action.selectedAccount };
26 | }
27 | case CHAIN_ID_CHANGED: {
28 | return { ...state, chainId: action.chainId };
29 | }
30 | case LOGOUT: {
31 | return { ...initialState };
32 | }
33 | default: {
34 | return state;
35 | }
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { getApiKey } from "../lightcone/api/LightconeAPI";
2 |
3 | export const getLoopringApiKey = async (wallet, account) => {
4 | const { signature } = wallet.getApiKey();
5 | return getApiKey(
6 | {
7 | accountId: account.accountId,
8 | publicKeyX: account.publicKeyX,
9 | publicKeyY: account.publicKeyY,
10 | },
11 | signature
12 | );
13 | };
14 |
15 | export const getShortenedEthereumAddress = (address) =>
16 | `${address.substring(0, 5)}...${address.substring(
17 | address.length - 4,
18 | address.length
19 | )}`;
20 |
21 | export const weiToEther = (weiBigNumber, decimals) => {
22 | return weiBigNumber.dividedBy("1e" + decimals);
23 | };
24 |
25 | export const etherToWei = (etherBigNumber, decimals) =>
26 | etherBigNumber.multipliedBy("1e" + decimals);
27 |
--------------------------------------------------------------------------------
/src/views/app/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Layout } from "../../components/layout";
3 | import { ThemeProvider } from "styled-components";
4 | import { GlobalStyle } from "./styled";
5 | import { useDispatch, useSelector } from "react-redux";
6 | import MewConnect from "@myetherwallet/mewconnect-web-client";
7 | import Web3Modal from "web3modal";
8 | import { INFURA_URL, CHAIN_ID } from "../../env";
9 | import { useCallback } from "react";
10 | import { Drawer } from "../../components/drawer";
11 | import { initializeWeb3 } from "../../actions/web3";
12 | import { ToastContainer, Slide } from "react-toastify";
13 | import "react-toastify/dist/ReactToastify.css";
14 | import {
15 | login,
16 | getSupportedTokens,
17 | getUserBalances,
18 | getSupportedMarkets,
19 | logout,
20 | } from "../../actions/loopring";
21 | import { Flex, Box } from "reflexbox";
22 | import { Swapper } from "../swapper";
23 | import { FullScreenOverlay } from "../../components/full-screen-overlay";
24 | import moment from "moment";
25 | import momentIt from "moment/locale/it";
26 | import { IntlProvider } from "react-intl";
27 | import en from "../../i18n/messages/en.json";
28 | import it from "../../i18n/messages/it.json";
29 | import { switchLanguage } from "../../actions/i18n";
30 | import { InvalidChainId } from "../../components/invalid-chain-id";
31 | import WalletConnectProvider from "@walletconnect/web3-provider";
32 | import Authereum from "authereum";
33 |
34 | // setting up moment locales
35 | moment.locale("it", momentIt);
36 | const localizedMessages = { en, it };
37 |
38 | const commonColors = {
39 | error: "#c62828",
40 | warning: "#FF6F00",
41 | primary: "#1c60ff",
42 | success: "#00c853",
43 | disabled: "rgb(202, 209, 213)",
44 | textDisabled: "rgba(5, 5, 5, 0.565)",
45 | textButton: "#d6e0ff",
46 | };
47 |
48 | const light = {
49 | ...commonColors,
50 | background: "#edf2f7",
51 | foreground: "#dfe6ef",
52 | border: "#b9ccdf",
53 | textLight: "#999999",
54 | text: "#0e062d",
55 | textInverted: "#F1F9D2",
56 | shadow: "rgba(0, 0, 0, 0.4)",
57 | placeholder: "#999999",
58 | };
59 |
60 | const dark = {
61 | ...commonColors,
62 | background: "#18191b",
63 | foreground: "#0d0d0d",
64 | border: "#23262a",
65 | textLight: "#737373",
66 | text: "#F1F9D2",
67 | textInverted: "#0e062d",
68 | shadow: "rgba(255, 255, 255, 0.1)",
69 | placeholder: "#737373",
70 | };
71 |
72 | const lightWeb3ModalTheme = {
73 | background: light.background,
74 | main: light.text,
75 | secondary: light.text,
76 | hover: light.foreground,
77 | };
78 |
79 | const darkWeb3ModalTheme = {
80 | background: dark.background,
81 | main: dark.text,
82 | secondary: dark.text,
83 | hover: dark.foreground,
84 | };
85 |
86 | const web3ModalOptions = {
87 | cacheProvider: true,
88 | providerOptions: {
89 | mewconnect: {
90 | package: MewConnect,
91 | options: {
92 | infuraId: INFURA_URL,
93 | },
94 | },
95 | walletconnect: {
96 | package: WalletConnectProvider,
97 | options: {
98 | infuraId: INFURA_URL,
99 | },
100 | },
101 | authereum: {
102 | package: Authereum,
103 | options: {
104 | infuraId: INFURA_URL,
105 | },
106 | },
107 | },
108 | };
109 |
110 | export const getWeb3Modal = () => new Web3Modal(web3ModalOptions);
111 |
112 | export let selectedTheme = light;
113 |
114 | export const App = () => {
115 | const dispatch = useDispatch();
116 |
117 | const {
118 | selectedLanguage,
119 | web3Instance,
120 | chainId,
121 | selectedAccount: selectedWeb3Account,
122 | loopringAccount,
123 | loopringWallet,
124 | supportedTokens,
125 | supportedMarkets,
126 | } = useSelector((state) => ({
127 | selectedLanguage: state.i18n.selectedLanguage,
128 | web3Instance: state.web3.instance,
129 | chainId: state.web3.chainId,
130 | selectedAccount: state.web3.selectedAccount,
131 | loopringAccount: state.loopring.account,
132 | loopringWallet: state.loopring.wallet,
133 | supportedTokens: state.loopring.supportedTokens.data,
134 | supportedMarkets: state.loopring.supportedMarkets.data,
135 | }));
136 |
137 | const [lightTheme, setLightTheme] = useState(true);
138 | const [drawerOpen, setDrawerOpen] = useState(false);
139 |
140 | useEffect(() => {
141 | dispatch(getSupportedMarkets());
142 | }, [dispatch]);
143 |
144 | useEffect(() => {
145 | if (supportedMarkets && supportedMarkets.length > 0) {
146 | dispatch(getSupportedTokens(supportedMarkets));
147 | }
148 | }, [dispatch, supportedMarkets]);
149 |
150 | useEffect(() => {
151 | if (
152 | loopringAccount &&
153 | loopringWallet &&
154 | supportedTokens &&
155 | supportedTokens.length > 0
156 | ) {
157 | dispatch(
158 | getUserBalances(
159 | loopringAccount,
160 | loopringWallet,
161 | supportedTokens
162 | )
163 | );
164 | }
165 | }, [dispatch, loopringAccount, loopringWallet, supportedTokens]);
166 |
167 | // setting up local storage-saved theme
168 | useEffect(() => {
169 | const cachedTheme =
170 | localStorage.getItem("loopring-swap-theme") || "light";
171 | const lightTheme = cachedTheme === "light";
172 | setLightTheme(lightTheme);
173 | web3ModalOptions.theme = lightTheme
174 | ? lightWeb3ModalTheme
175 | : darkWeb3ModalTheme;
176 | }, [dispatch]);
177 |
178 | // setting up local storage-saved language
179 | useEffect(() => {
180 | const cachedLanguage = localStorage.getItem("loopring-swap-language");
181 | if (cachedLanguage && cachedLanguage in localizedMessages) {
182 | dispatch(switchLanguage(cachedLanguage));
183 | }
184 | }, [dispatch]);
185 |
186 | const handleDrawerOpenClick = useCallback(() => {
187 | setDrawerOpen(true);
188 | }, []);
189 |
190 | const handleDrawerClose = useCallback(() => {
191 | setDrawerOpen(false);
192 | }, []);
193 |
194 | const handleConnectWallet = useCallback(() => {
195 | dispatch(initializeWeb3());
196 | }, [dispatch]);
197 |
198 | const handleLogin = useCallback(() => {
199 | dispatch(login(web3Instance, selectedWeb3Account));
200 | }, [dispatch, selectedWeb3Account, web3Instance]);
201 |
202 | const handleLogout = useCallback(() => {
203 | dispatch(logout());
204 | }, [dispatch]);
205 |
206 | const handleOverlayClick = useCallback(() => {
207 | setDrawerOpen(false);
208 | }, []);
209 |
210 | const handleThemeChange = useCallback(() => {
211 | const newLightTheme = !lightTheme;
212 | const textTheme = newLightTheme ? "light" : "dark";
213 | localStorage.setItem("loopring-swap-theme", textTheme);
214 | web3ModalOptions.theme = newLightTheme
215 | ? lightWeb3ModalTheme
216 | : darkWeb3ModalTheme;
217 | setLightTheme(newLightTheme);
218 | selectedTheme = newLightTheme ? light : dark;
219 | }, [lightTheme]);
220 |
221 | const handleSelectedLanguageChange = useCallback(
222 | (language) => {
223 | localStorage.setItem("loopring-swap-language", language);
224 | dispatch(switchLanguage(language));
225 | },
226 | [dispatch]
227 | );
228 |
229 | return (
230 |
234 |
235 |
236 |
237 |
243 |
244 | {!chainId || chainId === CHAIN_ID ? (
245 |
248 | ) : (
249 |
250 | )}
251 |
252 |
253 |
254 |
258 |
271 |
280 |
281 |
282 | );
283 | };
284 |
--------------------------------------------------------------------------------
/src/views/app/styled.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 |
3 | export const GlobalStyle = createGlobalStyle`
4 | html {
5 | height: 100%;
6 | font-family: Montserrat, sans-serif;
7 | font-size: 12px;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | height: 100%;
13 | }
14 |
15 | #root {
16 | height: 100%;
17 | }
18 |
19 | .custom-toast-root {
20 | width: auto !important;
21 | }
22 |
23 | @media (max-width: 600px) {
24 | .custom-toast-root {
25 | left: 16px !important;
26 | right: 16px !important;
27 | }
28 | }
29 |
30 | .custom-toast-container {
31 | margin-top: 16px !important;
32 | box-shadow: 0px 30px 62px 0px ${(props) =>
33 | props.theme.shadow} !important;
34 | border-radius: 12px !important;
35 | }
36 |
37 | .custom-toast-body {
38 | font-size: 16px;
39 | margin: 0;
40 | padding: 4px 12px;
41 | }
42 |
43 | .Toastify__toast {
44 | min-height: 0;
45 | font-family: Montserrat, sans-serif;
46 | }
47 |
48 | .Toastify__toast--warning {
49 | background: ${(props) => props.theme.warning} !important;
50 | }
51 |
52 | .Toastify__toast--error {
53 | background: ${(props) => props.theme.error} !important;
54 | }
55 |
56 | .web3modal-provider-container,
57 | .web3modal-modal-card {
58 | border-radius: 12px !important;
59 | }
60 | `;
61 |
--------------------------------------------------------------------------------
/src/views/swapper/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Flex, Box } from "reflexbox";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 |
5 | export const BackgroundFlex = styled(Flex)`
6 | border: solid 1px ${(props) => props.theme.border};
7 | border-radius: 12px;
8 | background: ${(props) => props.theme.foreground};
9 | padding: 8px;
10 | transition: background 0.3s ease, border 0.3s ease;
11 | `;
12 |
13 | export const ArrowIcon = styled(FontAwesomeIcon)`
14 | color: ${(props) => props.theme.primary};
15 | `;
16 |
17 | export const SlippageText = styled.span`
18 | color: ${(props) => props.theme.error};
19 | `;
20 |
21 | export const PointableBox = styled(Box)`
22 | cursor: pointer;
23 | `;
24 |
25 | export const ErrorTextBox = styled(Box)`
26 | color: ${(props) => props.theme.error};
27 | `;
28 |
29 | export const PriceFlipIcon = styled(FontAwesomeIcon)`
30 | color: ${(props) => props.theme.primary};
31 | `;
32 |
--------------------------------------------------------------------------------