├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── images │ ├── exit.png │ ├── enter.png │ ├── 3DK_LOGO.png │ └── 3DK_LOGO_ICON_1300.png ├── GlobalState │ ├── reducers.js │ ├── Store.js │ └── UserReducer.js ├── pages │ ├── LandingPage.jsx │ ├── Home.jsx │ └── Page2.jsx ├── setupTests.js ├── reportWebVitals.js ├── index.js ├── App.js ├── ProtectedRoute.jsx ├── App.scss ├── components │ └── Menu.jsx └── UserService.js ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/images/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/src/images/exit.png -------------------------------------------------------------------------------- /src/images/enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/src/images/enter.png -------------------------------------------------------------------------------- /src/images/3DK_LOGO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/src/images/3DK_LOGO.png -------------------------------------------------------------------------------- /src/images/3DK_LOGO_ICON_1300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azvast/wax-react-boilerplate/HEAD/src/images/3DK_LOGO_ICON_1300.png -------------------------------------------------------------------------------- /src/GlobalState/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import user from './UserReducer'; 3 | export const rootReducer = combineReducers({ 4 | user 5 | }); 6 | -------------------------------------------------------------------------------- /src/pages/LandingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const LandingPage = () => { 4 | return (<> 5 |

Welcome!

6 |

Landing Page

7 | ); 8 | } -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/GlobalState/Store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { rootReducer as reducer } from './reducers'; 3 | 4 | const Store = configureStore({ 5 | reducer, 6 | devTools: true 7 | }); 8 | 9 | export const storeAppDispatch = Store.dispatch; 10 | export default Store; 11 | -------------------------------------------------------------------------------- /src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Home = () => { 4 | return ( 5 |
6 |
7 |
8 |

Welcome to Home

9 |
10 |
11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/GlobalState/UserReducer.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | name: '', 5 | isLogged: false, 6 | balance: 0 7 | }; 8 | 9 | const user = createSlice({ 10 | name: 'user', 11 | initialState, 12 | reducers: { 13 | setPlayerData: (state, action) => (action.payload), 14 | setPlayerLogout: (state, action) => initialState, 15 | setPlayerBalance: (state, action) => ({...state, balance: action.payload}) 16 | } 17 | }); 18 | 19 | export const { setPlayerData, setPlayerLogout, setPlayerBalance } = user.actions; 20 | export default user.reducer; 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './App'; 5 | import Store from './GlobalState/Store'; 6 | import reportWebVitals from './reportWebVitals'; 7 | import { UserService } from './UserService'; 8 | 9 | UserService.init(); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.scss'; 2 | import { Menu } from './components/Menu'; 3 | import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; 4 | import { Home } from './pages/Home'; 5 | import { Page2 } from './pages/Page2'; 6 | import ProtectedRoute from './ProtectedRoute'; 7 | import { LandingPage } from './pages/LandingPage'; 8 | 9 | function App() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/ProtectedRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'react-router-dom'; 2 | import React from 'react'; 3 | import { UserService } from './UserService'; 4 | 5 | const ProtectedRoute = ({ component: Component, ...rest }) => { 6 | return ( 7 | { 9 | if (UserService.isLogged()) { 10 | return 11 | } else { 12 | return 20 | } 21 | } 22 | } /> 23 | ) 24 | } 25 | 26 | export default ProtectedRoute; -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap.scss"; 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | .App { 13 | background-color: #222f3e; 14 | width: 100%; 15 | height: 100vh; 16 | background-image: url("./images/3DK_LOGO.png"); 17 | background-position: center; 18 | background-repeat: no-repeat; 19 | } 20 | 21 | $heightMenu: 50px; 22 | #menu { 23 | height: $heightMenu; 24 | background-color: #11171f; 25 | .btn-item { 26 | background-color: #11171f; 27 | text-align: center; 28 | width: 100px; 29 | height: $heightMenu; 30 | line-height: $heightMenu; 31 | border-right: 1px solid #ff9f43; 32 | border-left: 1px solid #ff9f43; 33 | border-top: none; 34 | border-bottom: none; 35 | text-decoration: none; 36 | color: #ff9f43; 37 | margin-right: -1px; 38 | &:hover { 39 | background-color: #1e2835; 40 | color: #feca57; 41 | } 42 | } 43 | } 44 | 45 | .ual-button-gen { 46 | display: none; 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ual_template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@eosdacio/ual-wax": "^1.4.4", 7 | "@reduxjs/toolkit": "^1.6.1", 8 | "@testing-library/jest-dom": "^5.14.1", 9 | "@testing-library/react": "^11.2.7", 10 | "@testing-library/user-event": "^12.8.3", 11 | "anchor-link": "^3.3.3", 12 | "bootstrap": "^5.0.2", 13 | "glamor": "^2.20.40", 14 | "i": "^0.3.6", 15 | "lodash": "^4.17.21", 16 | "npm": "^7.20.3", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-redux": "^7.2.4", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "4.0.3", 22 | "ual-anchor": "^1.0.9", 23 | "ual-plainjs-renderer": "^0.3.0", 24 | "web-vitals": "^1.1.2" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "node-sass": "^6.0.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/Page2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { UserService } from '../UserService'; 3 | 4 | export const Page2 = () => { 5 | 6 | const onHandleSendWax = () => { 7 | UserService.session.signTransaction( 8 | { 9 | actions: [{ 10 | account: 'eosio.token', 11 | name: 'transfer', 12 | authorization: [{ 13 | actor: UserService.authName, 14 | permission: 'active' 15 | }], 16 | data: { 17 | from: UserService.authName, 18 | to: '3dkrenderwax', 19 | quantity: '1.00000000 WAX', 20 | memo: 'This works!' 21 | } 22 | }] 23 | }, 24 | { 25 | blocksBehind: 3, 26 | expireSeconds: 30 27 | } 28 | ).then((response) => { 29 | if(response.status === 'executed') { 30 | UserService.getBalance(); 31 | } 32 | }); 33 | 34 | // Refund balance 35 | } 36 | 37 | return ( 38 |
39 |
40 |
41 |

Welcome to Page2

42 |
43 |
44 | 45 |
46 |
47 |
48 | ); 49 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | import { useSelector } from 'react-redux'; 4 | import { Link, Redirect } from 'react-router-dom'; 5 | import LogoIcon from '../images/3DK_LOGO_ICON_1300.png'; 6 | import EnterIcon from '../images/enter.png'; 7 | import ExitIcon from '../images/exit.png'; 8 | import { useHistory } from 'react-router-dom'; 9 | import { UserService } from '../UserService'; 10 | import { useDispatch } from 'react-redux'; 11 | import { setPlayerLogout } from '../GlobalState/UserReducer'; 12 | 13 | export const Menu = (props) => { 14 | 15 | const dispatch = useDispatch(); 16 | const locationHistory = useHistory(); 17 | const UserState = useSelector((store) => store.user); 18 | 19 | const disabledStyle = css({ 20 | opacity: 0.5 21 | }) 22 | 23 | const handleLogin = () => { 24 | UserService.login(() => { 25 | if (UserService.isLogged()) { 26 | console.log(33) 27 | locationHistory.push('/home'); 28 | } else { 29 | console.log(44) 30 | dispatch(setPlayerLogout()); 31 | } 32 | }); 33 | } 34 | 35 | const onHandleLogout = () => { 36 | UserService.logout(); 37 | } 38 | 39 | return ( 40 | 69 | ); 70 | } -------------------------------------------------------------------------------- /src/UserService.js: -------------------------------------------------------------------------------- 1 | import { UALJs } from 'ual-plainjs-renderer'; 2 | import { Wax } from '@eosdacio/ual-wax'; 3 | import { isEmpty } from 'lodash'; 4 | import { Anchor } from 'ual-anchor'; 5 | 6 | import {storeAppDispatch} from './GlobalState/Store'; 7 | import { setPlayerBalance, setPlayerData, setPlayerLogout } from './GlobalState/UserReducer'; 8 | 9 | /** 10 | * Class to manage user data; it will be saved on Login and deleted on Logout 11 | */ 12 | export class User { 13 | 14 | appName = 'ual_template'; 15 | 16 | /** 17 | * WAX Mainnet configuration 18 | */ 19 | myChain = { 20 | chainId: '1064487b3cd1a897ce03ae5b6a865651747e2e152090f99c1d19d44e01aea5a4', 21 | rpcEndpoints: [{ 22 | protocol: 'https', 23 | host: 'apiwax.3dkrender.com', 24 | port: '' 25 | }] 26 | }; 27 | 28 | /** 29 | * WAX Testnet configuration 30 | */ 31 | // myChain = { 32 | // chainId: 'f16b1833c747c43682f4386fca9cbb327929334a762755ebec17f6f23c9b8a12', 33 | // rpcEndpoints: [{ 34 | // protocol: 'https', 35 | // host: 'testnet-wax.3dkrender.com', 36 | // port: '' 37 | // }] 38 | // }; 39 | 40 | ual; 41 | 42 | // User session data 43 | authName = undefined; 44 | serviceLoginName = undefined; 45 | // Shows petition signing and current balance obtaining methods 46 | session = undefined; 47 | 48 | // Current balance 49 | userBalance = 0; 50 | 51 | // Callback to refer to successful login 52 | callbackServerUserData = undefined; 53 | 54 | getName() { 55 | return this.authName; 56 | } 57 | 58 | login(callback) { 59 | const ualButton = document.querySelector(".ual-button-gen"); 60 | ualButton.click(); 61 | 62 | this.callbackServerUserData = callback; 63 | } 64 | 65 | isLogged() { 66 | const auth = !isEmpty(this.authName) && !isEmpty(this.session); 67 | return auth; 68 | } 69 | 70 | logout() { 71 | console.log("Logout"); 72 | this.authName = undefined; 73 | this.session = undefined; 74 | 75 | this.ual.logoutUser(); 76 | 77 | storeAppDispatch(setPlayerLogout()); 78 | 79 | if(this.callbackServerUserData !== undefined) { 80 | this.callbackServerUserData(); 81 | } 82 | } 83 | 84 | // UAL API call response 85 | async ualCallback(userObject) { 86 | 87 | this.session = userObject[0]; 88 | this.serviceLoginName = this.session.constructor.name; 89 | this.authName = this.session.accountName; 90 | 91 | storeAppDispatch(setPlayerData({ 92 | name: this.authName, 93 | isLogged: this.isLogged(), 94 | balance: (this.balance !== undefined) ? this.balance : 0 95 | })); 96 | 97 | this.getBalance(); 98 | 99 | if(this.callbackServerUserData !== undefined) { 100 | this.callbackServerUserData(); 101 | } 102 | } 103 | 104 | getBalance() { 105 | const balance = this.session.rpc.get_account(this.authName); 106 | balance.then((balance) => { 107 | this.balance = balance.core_liquid_balance; 108 | storeAppDispatch(setPlayerBalance((this.balance !== undefined) ? this.balance : 0)); 109 | }); 110 | return balance; 111 | } 112 | 113 | // UserService init called to prepare UAL Login. 114 | init() { 115 | // Binding: 116 | this.ualCallback = this.ualCallback.bind(this); 117 | 118 | const wax = new Wax([this.myChain], { appName: this.appName }); 119 | 120 | const anchor = new Anchor([this.myChain], { appName: this.appName }); 121 | 122 | const divUal = document.createElement('div') 123 | divUal.setAttribute('id', 'ual-login'); 124 | document.body.appendChild(divUal); 125 | 126 | const divLoginRoot = document.getElementById('ual-login'); 127 | this.ual = new UALJs(this.ualCallback, [this.myChain], this.appName, [wax, anchor], { containerElement: divLoginRoot }); 128 | this.ual.init() 129 | } 130 | 131 | static new() { 132 | if (!User.instance) { 133 | User.instance = new User(); 134 | } 135 | 136 | return User.instance; 137 | } 138 | } 139 | 140 | export const UserService = User.new(); 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Template for WAX (UAL) 2 | 3 | This template was created to make the use of WAX with React easier. 4 | 5 | Please remember to install all needed dependencies beforehand: 6 | ``` 7 | > npm i 8 | ``` 9 | You can start the project by: 10 | ``` 11 | > npm run start 12 | ``` 13 | 14 | ## Dependencies used 15 | - Dependencies for WAX 16 | - @eosdacio/ual-wax 17 | - ual-anchor 18 | - ual-plainjs-renderer 19 | - anchor-link 20 | - Dependencies for React routes 21 | - react-router-dom 22 | 23 | - Global state dependencies 24 | - redux 25 | - react-redux 26 | - @reduxjs/toolkit 27 | 28 | - Style and design dependencies 29 | - bootstrap 30 | - glamor 31 | 32 | - Help dependencies 33 | - lodash 34 | 35 | ## Key files 36 | 37 | - **UserService.js**: A file needed to manage the user's login, UAL configuration and different login methods. It also has a certain mode that lets us know the user's status (**isLogged()**) with the answers **true** or **false** if the user has previously logged in. 38 | 39 | **Remember** to comment and uncomment as needed, depending on wether you will be working with mainnet or testnet. 40 | 41 | When the user logs in, this data will be saved in the app's global state thanks to Redux and @reduxjs/toolkit (we will also save an **isLogged** in order to maintain a live actualization in React). 42 | 43 | - **GlobalState/**: In this folder we will keep our configuration and **store** from **redux**, so we can save and manage the user's data in a global state. 44 | 45 | - **App.js**: With this file we can manage the web's route system. 46 | 47 | - **index.js**: This file initiates **UserService** (UserService.init()) and also contains the for **store** from **redux**. 48 | 49 | - **pages**: In this folder we will save our web's pages. **Remember to configure new pages' routes in your App.js**. 50 | 51 | - **components**: In this folder we will keep our app's components. For this example we only have **Menu.jsx**, a component for our menu, which will help us redirect the user when they log in. 52 | 53 | ## File Menu.jsx 54 | The file **components/Menu.jsx** is a component from our app or web's menu, which has four tabs: Main, Home, Page2, Login/Logout. 55 | 56 | As we can see, we have two tabs which are disabled and we are not allowed to access them. These are: **Home** and **Page2**. To achieve this, we will simply check out if the user has logged in or not, thanks to **UserState.isLogged** (in the redux state), and we will show or lock them with any **CSS** style. 57 | 58 | As for the Login/Logout tab, we will show one or the other depending on the user's state. This is easily ckeched by opening the file, but you will probably see something like this: 59 | ```jsx 60 | { 61 | !UserState.isLogged && 62 | 63 | } 64 | { 65 | UserState.isLogged && 66 | Logout 67 | } 68 | ``` 69 | ## Login system 70 | 71 | The system to start login is located in **components/Menu.jsx**. Once we click on the login button in the menu, this will trigger **handleLogin** and at the same time will activate the **UserService.login()** fuction. In this function it is possible to add an anonymous function as a callback, and the answer will be delivered once we log in. This callback will allow us to check if the user has logged in, if they have. 72 | If that is the case, we will be redirected to a page (**/home, in this case**). Otherwise, it will log out in order to clear data. 73 | 74 | ```jsx 75 | const handleLogin = () => { 76 | UserService.login(() => { 77 | if (UserService.isLogged()) { 78 | locationHistory.push('/home'); 79 | } else { 80 | dispatch(setPlayerLogout()); 81 | } 82 | }); 83 | } 84 | ``` 85 | 86 | ## React routes' protection 87 | 88 | Routes must be protected. For that, we will need to create a component called **ProtectedRouter.jsx**. We have created one for this example, and its function will be to ckeck the route we're in and to show us the user's state so we can know if they have logged in or not (**UserService.isLogged()**). If the user has not logged in, it will take the user from this route and redirect them to another one we have configured previously, which in this case leads to **/login**. 89 | 90 | In order to use this component, we will simply open our **App.js**, where we will substitute for our . You will see this if you open **App.js**: 91 | 92 | ```jsx 93 | function App() { 94 | return ( 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 | ); 107 | } 108 | ``` 109 | 110 | ## Send WAX 111 | 112 | You can see an example of how to send items from WAX in **pages/Page2.jsx**. The **UserService** saves the session and we simply have to access this session and sing any transaction. The code you will see in this example looks like this: 113 | 114 | ```js 115 | UserService.session.signTransaction( 116 | { 117 | actions: [{ 118 | account: 'eosio.token', 119 | name: 'transfer', 120 | authorization: [{ 121 | actor: UserService.authName, 122 | permission: 'active' 123 | }], 124 | data: { 125 | from: UserService.authName, 126 | to: '3dkrenderwax', 127 | quantity: '1.00000000 WAX', 128 | memo: 'This works!' 129 | } 130 | }] 131 | }, 132 | { 133 | blocksBehind: 3, 134 | expireSeconds: 30 135 | } 136 | 137 | ).then((response) => { 138 | if(response.status === 'executed') { 139 | UserService.getBalance(); 140 | } 141 | }); 142 | ``` 143 | --------------------------------------------------------------------------------