├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitignore ├── .prettierrc.yaml ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── img │ └── solana-logo-horizontal.svg ├── index.css └── index.html ├── package-lock.json ├── package.json ├── server.js ├── src ├── account.js ├── components │ ├── Button.js │ ├── Loader.js │ └── NetworkSelect.js ├── icons │ ├── close.svg │ ├── eye.svg │ ├── file-copy.svg │ ├── gear.svg │ ├── info.svg │ ├── refresh.svg │ ├── send.svg │ └── warn.svg ├── index.js ├── settings.js ├── store.js ├── styles │ ├── _alert.scss │ ├── _balance.scss │ ├── _btn.scss │ ├── _common.scss │ ├── _dropdown.scss │ ├── _form.scss │ ├── _help-block.scss │ ├── _input.scss │ ├── _loader.scss │ ├── _modal.scss │ ├── _network-select.scss │ ├── _panel.scss │ ├── _request-modal.scss │ ├── _setup.scss │ ├── _tooltip.scss │ ├── _typography.scss │ ├── _vars.scss │ ├── _well.scss │ └── index.scss └── wallet.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | // The % refers to the global coverage of users from browserslist 6 | "browsers": [ ">0.25%", "not ie 11", "not op_mini all"] 7 | } 8 | }], 9 | "react", 10 | "stage-2" 11 | ], 12 | "plugins": [ 13 | "transform-class-properties", 14 | "transform-function-bind", 15 | "transform-runtime", 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // eslint-disable-line import/no-commonjs 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | plugins: ['react'], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:import/errors', 12 | 'plugin:import/warnings', 13 | 'plugin:react/recommended', 14 | ], 15 | parser: 'babel-eslint', 16 | parserOptions: { 17 | sourceType: 'module', 18 | ecmaVersion: 8, 19 | }, 20 | rules: { 21 | 'no-trailing-spaces': ['error'], 22 | 'import/first': ['error'], 23 | 'import/no-commonjs': ['error'], 24 | 'import/order': [ 25 | 'error', 26 | { 27 | groups: [ 28 | ['internal', 'external', 'builtin'], 29 | ['index', 'sibling', 'parent'], 30 | ], 31 | 'newlines-between': 'always', 32 | }, 33 | ], 34 | indent: ['error', 2, {MemberExpression: 0, SwitchCase: 1}], 35 | 'linebreak-style': ['error', 'unix'], 36 | 'no-console': [0], 37 | quotes: [ 38 | 'error', 39 | 'single', 40 | {avoidEscape: true, allowTemplateLiterals: true}, 41 | ], 42 | 'require-await': ['error'], 43 | semi: ['error', 'always'], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/* 3 | 4 | [include] 5 | 6 | [libs] 7 | node_modules/@solana/web3.js/module.flow.js 8 | 9 | emoji=true 10 | esproposal.class_instance_fields=enable 11 | esproposal.class_static_fields=enable 12 | esproposal.decorators=ignore 13 | esproposal.export_star_as=enable 14 | module.system.node.resolve_dirname=./src 15 | module.use_strict=true 16 | experimental.const_params=true 17 | include_warnings=true 18 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 19 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist/*bundle.js* 3 | /dist/*worker.js 4 | /dist/main.css 5 | *.sw[po] 6 | /store 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | arrowParens: "avoid" 2 | bracketSpacing: false 3 | jsxBracketSameLine: false 4 | semi: true 5 | singleQuote: true 6 | tabWidth: 2 7 | trailingComma: "all" 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: rust 4 | services: 5 | - docker 6 | cache: 7 | directories: 8 | - "~/.npm" 9 | notifications: 10 | email: false 11 | 12 | install: 13 | - cargo --version 14 | - docker --version 15 | - nvm install node 16 | - node --version 17 | - npm install 18 | 19 | script: 20 | - npm run test 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Solana Labs, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status][travis-image]][travis-url] 2 | 3 | [travis-image]: https://api.travis-ci.org/solana-labs/example-webwallet.svg?branch=master 4 | [travis-url]: https://travis-ci.org/solana-labs/example-webwallet 5 | 6 | # Example Web Wallet 7 | 8 | This project demonstrates how to use the [Solana Javascript API](https://github.com/solana-labs/solana-web3.js) 9 | to implement a simple web wallet. 10 | 11 | **IMPORTANT: This wallet does not sufficently protect the private keys it 12 | generates and should NOT be used in a non-test environment** 13 | 14 | ## Getting Started 15 | 16 | ``` 17 | $ npm install 18 | $ npm run start 19 | ``` 20 | 21 | Then open your browser to http://localhost:8080/ 22 | 23 | ## Development 24 | 25 | When making changes, using the webpack-dev-server can be quite convenient as it 26 | will rebuild and reload the app automatically 27 | 28 | ``` 29 | $ npm run dev 30 | ``` 31 | 32 | ## Funding dApps 33 | 34 | If this wallet is opened by a dApp, it will accept requests for funds. In order to 35 | request funds from your dApp, follow these steps: 36 | 37 | 1. Attach a message event listener to the dApp window 38 | ```js 39 | window.addEventListener('message', (e) => { /* ... */ }); 40 | ``` 41 | 2. Open the wallet url in a window from the dApp 42 | ```js 43 | const walletWindow = window.open(WALLET_URL, 'wallet', 'toolbar=no, location=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=500, height=600'); 44 | ``` 45 | 3. Wait for the wallet to load, it will post a `'ready'` message when it's ready to handle requests 46 | ```js 47 | window.addEventListener('message', (e) => { 48 | if (e.data) { 49 | switch (e.data.method) { 50 | case 'ready': { 51 | // ... 52 | break; 53 | } 54 | } 55 | } 56 | }); 57 | ``` 58 | 4. Send an `'addFunds'` request 59 | ```js 60 | walletWindow.postMessage({ 61 | method: 'addFunds', 62 | params: { 63 | pubkey: '7q4tpevKWZFSXszPfnvWDuuE19EhSnsAmt5x4MqCyyVb', 64 | amount: 150, 65 | network: 'https://devnet.solana.com', 66 | }, 67 | }, WALLET_URL); 68 | ``` 69 | 5. Listen for an `'addFundsResponse'` event which will include the amount transferred and the transaction signature 70 | ```js 71 | window.addEventListener('message', (e) => { 72 | // ... 73 | switch (e.data.method) { 74 | case 'ready': { 75 | // ... 76 | break; 77 | } 78 | case 'addFundsResponse': { 79 | const {amount, signature} = e.data.params; 80 | // ... 81 | break; 82 | } 83 | } 84 | }); 85 | ``` 86 | -------------------------------------------------------------------------------- /dist/img/solana-logo-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | body { 7 | height: 100%; 8 | width: 100%; 9 | background-color: #000; 10 | font-family: "Exo", sans-serif; 11 | color: #fff; 12 | font-size: 15px; 13 | } 14 | 15 | #app { 16 | height: 100%; 17 | width: 100%; 18 | } 19 | 20 | input:-webkit-autofill, 21 | input:-webkit-autofill:hover, 22 | input:-webkit-autofill:focus, 23 | input:-webkit-autofill:active { 24 | -webkit-box-shadow: 0 0 0 30px #000 inset !important; 25 | -webkit-text-fill-color: #fff !important; 26 | } 27 | 28 | .header { 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-between; 32 | padding: 10px 28px; 33 | height: 100px; 34 | margin-bottom: 40px; 35 | border-bottom: 1px solid rgba(151,151,151, .5); 36 | } 37 | 38 | 39 | .header-right { 40 | font-size: 12px; 41 | text-transform: uppercase; 42 | letter-spacing: 2.5px; 43 | margin-right: 40px; 44 | } 45 | @media (max-width: 768px) { 46 | .header { 47 | padding: 10px 26px; 48 | margin-bottom: 0; 49 | } 50 | .header-right { 51 | margin-right: 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Solana Example Wallet 5 | 6 | 12 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 27 | 28 |
29 | Wallet 30 |
31 |
32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode=production", 8 | "builddev": "webpack --mode=development", 9 | "dev": "webpack-dev-server --config ./webpack.config.js --mode development", 10 | "flow": "flow", 11 | "flow:watch": "watch 'flow' . --wait=1 --ignoreDirectoryPattern=/doc/", 12 | "lint": "npm run pretty && eslint .", 13 | "lint:fix": "npm run lint -- --fix", 14 | "lint:watch": "watch 'npm run lint:fix' . --wait=1", 15 | "postinstall": "npm run build", 16 | "pretty": "prettier --write '{,src/**/}*.js'", 17 | "start": "babel-node server.js", 18 | "test": "npm run lint && npm run flow" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@svgr/webpack": "^5.0.0", 25 | "babel-core": "^6.26.3", 26 | "babel-eslint": "^10.0.1", 27 | "babel-loader": "^7.1.5", 28 | "babel-plugin-transform-function-bind": "^6.22.0", 29 | "babel-plugin-transform-runtime": "^6.23.0", 30 | "babel-preset-env": "^1.7.0", 31 | "babel-preset-react": "^6.24.1", 32 | "babel-preset-stage-2": "^6.24.1", 33 | "eslint": "^6.0.1", 34 | "eslint-loader": "^4.0.0", 35 | "eslint-plugin-import": "^2.13.0", 36 | "eslint-plugin-jsx-a11y": "^6.1.1", 37 | "eslint-plugin-react": "^7.10.0", 38 | "mini-css-extract-plugin": "^0.11.0", 39 | "optimize-css-assets-webpack-plugin": "^5.0.3", 40 | "prettier": "^2.0.0", 41 | "react-hot-loader": "^4.3.4", 42 | "sass-loader": "^10.0.0", 43 | "terser-webpack-plugin": "^4.0.0", 44 | "webpack": "^4.16.5", 45 | "webpack-cli": "^3.1.0", 46 | "webpack-dev-server": "^3.1.5" 47 | }, 48 | "dependencies": { 49 | "@solana/web3.js": "^0.71.0", 50 | "babel-cli": "^6.26.0", 51 | "babel-plugin-transform-class-properties": "^6.24.1", 52 | "babel-runtime": "^6.26.0", 53 | "bip39": "^3.0.2", 54 | "body-parser": "^1.18.3", 55 | "bs58": "^4.0.1", 56 | "copy-to-clipboard": "^3.0.8", 57 | "css-loader": "^4.0.0", 58 | "event-emitter": "^0.3.5", 59 | "express": "^4.16.3", 60 | "flow-bin": "^0.132.0", 61 | "jayson": "^3.0.1", 62 | "joi": "^17.1.1", 63 | "localforage": "^1.7.2", 64 | "node-fetch": "^2.2.0", 65 | "promisify": "0.0.3", 66 | "prop-types": "^15.6.2", 67 | "react": "^16.4.2", 68 | "react-bootstrap": "~0.33.0", 69 | "react-dom": "^16.4.2", 70 | "sass": "^1.22.9", 71 | "semver": "^7.0.0", 72 | "sha256": "^0.2.0", 73 | "style-loader": "^1.0.0", 74 | "tweetnacl": "^1.0.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | 4 | const port = process.env.PORT || 8080; 5 | const app = express(); 6 | 7 | app.use(express.static(path.join(__dirname, 'dist'))); 8 | 9 | app.get('*', (req, res) => { 10 | res.sendFile(path.resolve(__dirname, 'dist/index.html')); 11 | }); 12 | console.log(`Listening on port ${port}`); 13 | app.listen(port); 14 | -------------------------------------------------------------------------------- /src/account.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button as BaseButton, 4 | ControlLabel, 5 | FormControl, 6 | FormGroup, 7 | HelpBlock, 8 | InputGroup, 9 | OverlayTrigger, 10 | ToggleButton, 11 | ToggleButtonGroup, 12 | Tooltip, 13 | Well, 14 | } from 'react-bootstrap'; 15 | import PropTypes from 'prop-types'; 16 | import copy from 'copy-to-clipboard'; 17 | import * as bip39 from 'bip39'; 18 | 19 | import FileCopyIcon from './icons/file-copy.svg'; 20 | import EyeIcon from './icons/eye.svg'; 21 | import Button from './components/Button'; 22 | const GENERATE_WALLET_MODE = 1; 23 | const RECOVER_WALLET_MODE = 2; 24 | 25 | export class Account extends React.Component { 26 | state = { 27 | walletMode: GENERATE_WALLET_MODE, 28 | revealSeedPhrase: false, 29 | generatedPhrase: bip39.generateMnemonic(), 30 | recoveredPhrase: '', 31 | }; 32 | 33 | regenerateSeedPhrase() { 34 | const generatedPhrase = bip39.generateMnemonic(); 35 | this.setState({generatedPhrase}); 36 | } 37 | 38 | copyGeneratedPhrase() { 39 | copy(this.state.generatedPhrase); 40 | } 41 | 42 | createAccount() { 43 | this.props.store.createAccountFromMnemonic(this.state.generatedPhrase); 44 | } 45 | 46 | recoverAccount() { 47 | this.props.store.createAccountFromMnemonic(this.state.recoveredPhrase); 48 | } 49 | 50 | onModeChange(walletMode) { 51 | this.setState({walletMode}); 52 | } 53 | 54 | onRecoverPhraseChange(e) { 55 | this.setState({recoveredPhrase: e.target.value}); 56 | } 57 | 58 | validateRecoverPhrase() { 59 | if (this.state.recoveredPhrase) { 60 | if (bip39.validateMnemonic(this.state.recoveredPhrase)) { 61 | return 'success'; 62 | } else { 63 | return 'error'; 64 | } 65 | } 66 | return null; 67 | } 68 | 69 | render() { 70 | return ( 71 | 72 |
73 |

Account Setup

74 |
75 |

76 | A locally cached wallet account was not found. Generate a new one or 77 | recover an existing wallet from its seed phrase. 78 |

79 |
80 | this.onModeChange(mode)} 84 | justified 85 | > 86 | 87 | Generate New Wallet 88 | 89 | 90 | Recover Existing Wallet 91 | 92 | 93 |
94 | 95 | {this.state.walletMode === GENERATE_WALLET_MODE && 96 | this.renderGenerateWalletMode()} 97 | {this.state.walletMode === RECOVER_WALLET_MODE && 98 | this.renderRecoverWalletMode()} 99 | 100 |
101 |
102 | ); 103 | } 104 | 105 | seedPhraseInputType() { 106 | if (this.state.revealSeedPhrase) { 107 | return 'text'; 108 | } else { 109 | return 'password'; 110 | } 111 | } 112 | 113 | toggleReveal() { 114 | this.setState({revealSeedPhrase: !this.state.revealSeedPhrase}); 115 | } 116 | 117 | renderRevealToggle() { 118 | let toggleText = 'Reveal'; 119 | if (this.state.revealSeedPhrase) { 120 | toggleText = 'Hide'; 121 | } 122 | 123 | const revealTooltip = {toggleText}; 124 | 125 | return ( 126 | 127 | 128 | this.toggleReveal()}> 129 | 130 | 131 | 132 | 133 | ); 134 | } 135 | 136 | renderRecoverWalletMode() { 137 | return ( 138 | 139 | 140 | 141 | Enter a valid seed phrase to recover a wallet 142 | 143 | 144 | {this.renderRevealToggle()} 145 | this.onRecoverPhraseChange(e)} 152 | /> 153 | 154 | 155 | 156 | Seed phrase should be 12 words in length. 157 | 158 | 159 |
160 | 166 |
167 |
168 | ); 169 | } 170 | 171 | renderGenerateWalletMode() { 172 | const copyTooltip = ( 173 | Copy seed phrase to clipboard 174 | ); 175 | 176 | return ( 177 | 178 | 179 | 180 | Generated Seed Phrase 181 | 182 | 183 | {this.renderRevealToggle()} 184 | { 191 | return false; 192 | }} 193 | /> 194 | 195 | 196 | this.copyGeneratedPhrase()}> 197 | 198 | 199 | 200 | 201 | 202 | 203 |

204 | WARNING: The seed phrase will not be 205 | shown again, copy it down or save in your password manager to recover 206 | this wallet in the future. 207 |

208 |
209 | 215 |
216 |
217 | ); 218 | } 219 | } 220 | 221 | Account.propTypes = { 222 | store: PropTypes.object, 223 | }; 224 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Button = props => { 5 | return ( 6 |
7 |
9 | ); 10 | }; 11 | 12 | Button.propTypes = { 13 | disabled: PropTypes.bool, 14 | }; 15 | 16 | export default Button; 17 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader = () => ( 4 |
5 |
6 |
7 |
8 |
9 |
10 | ); 11 | 12 | export default Loader; 13 | -------------------------------------------------------------------------------- /src/components/NetworkSelect.js: -------------------------------------------------------------------------------- 1 | import * as web3 from '@solana/web3.js'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import {DropdownButton, FormGroup, InputGroup, MenuItem} from 'react-bootstrap'; 5 | 6 | const NetworkSelect = ({value, onChange}) => { 7 | return ( 8 | 9 | 10 | 16 | {[ 17 | web3.clusterApiUrl('mainnet-beta'), 18 | web3.clusterApiUrl('testnet'), 19 | web3.clusterApiUrl('devnet'), 20 | 'http://localhost:8899', 21 | ].map((url, index) => ( 22 | 23 | {url} 24 | 25 | ))} 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | NetworkSelect.propTypes = { 33 | value: PropTypes.string, 34 | onChange: PropTypes.func, 35 | }; 36 | 37 | export default NetworkSelect; 38 | -------------------------------------------------------------------------------- /src/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/file-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/icons/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/warn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import {Wallet} from './wallet'; 5 | import {Store} from './store'; 6 | 7 | import './styles/index.scss'; 8 | 9 | class App extends React.Component { 10 | state = { 11 | store: new Store(), 12 | initialized: false, 13 | }; 14 | 15 | async componentDidMount() { 16 | await this.state.store.init(); 17 | this.setState({initialized: true}); 18 | } 19 | 20 | render() { 21 | if (!this.state.initialized) { 22 | return
; // TODO: Loading screen? 23 | } 24 | return ; 25 | } 26 | } 27 | 28 | ReactDOM.render(, document.getElementById('app')); 29 | module.hot.accept(); 30 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | DropdownButton, 4 | HelpBlock, 5 | MenuItem, 6 | FormControl, 7 | FormGroup, 8 | InputGroup, 9 | ControlLabel, 10 | } from 'react-bootstrap'; 11 | import PropTypes from 'prop-types'; 12 | import * as web3 from '@solana/web3.js'; 13 | 14 | import Button from './components/Button'; 15 | 16 | export class Settings extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | validationState: null, 22 | validationHelpBlock: null, 23 | checkNetworkCount: 0, 24 | networkEntryPoint: '', 25 | }; 26 | 27 | this.onStoreChange = this.onStoreChange.bind(this); 28 | } 29 | 30 | componentDidMount() { 31 | this.props.store.onChange(this.onStoreChange); 32 | this.onStoreChange(); 33 | } 34 | 35 | componentWillUnmount() { 36 | this.props.store.removeChangeListener(this.onStoreChange); 37 | } 38 | 39 | onStoreChange() { 40 | this.setState( 41 | { 42 | networkEntryPoint: this.props.store.networkEntryPoint, 43 | }, 44 | this.checkNetwork, 45 | ); 46 | } 47 | 48 | setNetworkEntryPoint(url) { 49 | this.setState({networkEntryPoint: url}, this.checkNetwork); 50 | } 51 | 52 | async checkNetwork() { 53 | if (!this.state.networkEntryPoint) return; 54 | console.log('Checking network:', this.state.networkEntryPoint); 55 | const connection = new web3.Connection(this.state.networkEntryPoint); 56 | const checkNetworkCount = this.state.checkNetworkCount + 1; 57 | this.setState({ 58 | validationState: 'warning', 59 | validationHelpBlock: 'Connecting to network...', 60 | checkNetworkCount, 61 | }); 62 | 63 | try { 64 | const {feeCalculator} = await connection.getRecentBlockhash(); 65 | const minBalanceForRentException = await connection.getMinimumBalanceForRentExemption( 66 | 0, 67 | ); 68 | if (this.state.checkNetworkCount <= checkNetworkCount) { 69 | this.props.store.setFeeCalculator(feeCalculator); 70 | this.props.store.setMinBalanceForRentExemption( 71 | minBalanceForRentException, 72 | ); 73 | this.props.store.setNetworkEntryPoint(this.state.networkEntryPoint); 74 | this.setState({ 75 | validationState: 'success', 76 | validationHelpBlock: 'Connected', 77 | }); 78 | } 79 | } catch (err) { 80 | console.log('checkNetwork error:', err); 81 | if (this.state.checkNetworkCount <= checkNetworkCount) { 82 | this.setState({ 83 | validationState: 'error', 84 | validationHelpBlock: 'Connection failed', 85 | }); 86 | } 87 | } 88 | } 89 | 90 | async resetAccount() { 91 | await this.props.store.resetAccount(); 92 | this.props.onHide(); 93 | } 94 | 95 | render() { 96 | return ( 97 |
98 | 99 | Network Settings 100 | 101 | 107 | {[ 108 | web3.clusterApiUrl('mainnet-beta'), 109 | web3.clusterApiUrl('testnet'), 110 | web3.clusterApiUrl('devnet'), 111 | 'http://localhost:8899', 112 | ].map((url, index) => ( 113 | 118 | {url} 119 | 120 | ))} 121 | 122 | this.setNetworkEntryPoint(e.target.value)} 127 | /> 128 | 129 | 130 | {this.state.validationHelpBlock} 131 | 132 |
133 |
Account Settings
134 |

135 | WARNING: 136 |  Any tokens associated with the current account will be lost 137 |

138 |
139 | 140 |
141 |
142 |
143 | ); 144 | } 145 | } 146 | Settings.propTypes = { 147 | store: PropTypes.object, 148 | onHide: PropTypes.func, 149 | }; 150 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage'; 2 | import EventEmitter from 'event-emitter'; 3 | import * as web3 from '@solana/web3.js'; 4 | import nacl from 'tweetnacl'; 5 | import * as bip39 from 'bip39'; 6 | import gte from 'semver/functions/gte'; 7 | 8 | export class Store { 9 | constructor() { 10 | this._ee = new EventEmitter(); 11 | this._lf = localforage.createInstance({ 12 | name: 'configuration', 13 | }); 14 | } 15 | 16 | async init() { 17 | for (let key of [ 18 | 'networkEntryPoint', 19 | 'accountSecretKey', // TODO: THIS KEY IS NOT STORED SECURELY!! 20 | 'feeCalculator', 21 | 'minBalanceForRentException', 22 | ]) { 23 | this[key] = await this._lf.getItem(key); 24 | } 25 | 26 | if (typeof this.networkEntryPoint !== 'string') { 27 | await this.setNetworkEntryPoint(web3.clusterApiUrl(process.env.CLUSTER)); 28 | } else { 29 | await this.resetConnection(); 30 | } 31 | } 32 | 33 | async resetAccount() { 34 | this.accountSecretKey = null; 35 | this._ee.emit('change'); 36 | await this._lf.setItem('accountSecretKey', this.accountSecretKey); 37 | } 38 | 39 | async createAccountFromMnemonic(mnemonic) { 40 | const seed = await bip39.mnemonicToSeed(mnemonic); 41 | const keyPair = nacl.sign.keyPair.fromSeed(seed.slice(0, 32)); 42 | this.accountSecretKey = keyPair.secretKey; 43 | this._ee.emit('change'); 44 | await this._lf.setItem('accountSecretKey', keyPair.secretKey); 45 | } 46 | 47 | async resetConnection() { 48 | const url = this.networkEntryPoint; 49 | let connection = new web3.Connection(url); 50 | let feeCalculator; 51 | let minBalanceForRentException; 52 | try { 53 | feeCalculator = (await connection.getRecentBlockhash()).feeCalculator; 54 | minBalanceForRentException = await connection.getMinimumBalanceForRentExemption( 55 | 0, 56 | ); 57 | // commitment params are only supported >= 0.21.0 58 | const version = await connection.getVersion(); 59 | const solanaCoreVersion = version['solana-core'].split(' ')[0]; 60 | if (gte(solanaCoreVersion, '0.21.0')) { 61 | connection = new web3.Connection(url, 'recent'); 62 | } 63 | } catch (err) { 64 | console.error('Failed to reset connection', err); 65 | connection = null; 66 | } 67 | 68 | if (url === this.networkEntryPoint) { 69 | this.setFeeCalculator(feeCalculator); 70 | this.setMinBalanceForRentExemption(minBalanceForRentException); 71 | this.connection = connection; 72 | this._ee.emit('change'); 73 | } 74 | } 75 | 76 | async setNetworkEntryPoint(value) { 77 | if (value !== this.networkEntryPoint) { 78 | this.networkEntryPoint = value; 79 | await this.resetConnection(); 80 | await this._lf.setItem('networkEntryPoint', value); 81 | } 82 | } 83 | 84 | async setMinBalanceForRentExemption(minBalanceForRentException) { 85 | this.minBalanceForRentException = minBalanceForRentException; 86 | await this._lf.setItem( 87 | 'minBalanceForRentException', 88 | minBalanceForRentException, 89 | ); 90 | } 91 | 92 | async setFeeCalculator(feeCalculator) { 93 | this.feeCalculator = feeCalculator; 94 | await this._lf.setItem('feeCalculator', feeCalculator); 95 | } 96 | 97 | onChange(fn) { 98 | this._ee.on('change', fn); 99 | } 100 | 101 | removeChangeListener(fn) { 102 | this._ee.off('change', fn); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/styles/_alert.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | display: flex; 3 | align-items: center; 4 | letter-spacing: 2.5px; 5 | font-weight: bold; 6 | overflow-wrap: break-word; 7 | & > svg { 8 | margin-right: 16px; 9 | margin-top: -3px; 10 | flex-shrink: 0; 11 | } 12 | & > span { 13 | min-width: 1px; 14 | padding-right: 15px; 15 | } 16 | a { 17 | margin-left: auto; 18 | } 19 | &-info, 20 | &-warning, 21 | &-danger { 22 | background: $grey; 23 | border-color: $grey; 24 | } 25 | &-danger { 26 | color: $error; 27 | } 28 | &-warning { 29 | color: $warning; 30 | } 31 | &-info { 32 | color: $primary; 33 | } 34 | } 35 | @media (max-width: 768px) { 36 | .alert { 37 | margin-top: 20px; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/_balance.scss: -------------------------------------------------------------------------------- 1 | .balance { 2 | display: flex; 3 | align-items: center; 4 | svg { 5 | flex-shrink: 0; 6 | } 7 | &-header { 8 | display: flex; 9 | margin-bottom: 16px; 10 | & > h4 { 11 | margin-right: auto; 12 | font-size: 15px; 13 | } 14 | button:last-child { 15 | margin-left: 15px; 16 | } 17 | } 18 | &-val { 19 | font-size: 58px; 20 | font-weight: bold; 21 | letter-spacing: 3.4px; 22 | color: $primary; 23 | line-height: 55px; 24 | max-width: 100%; 25 | margin-right: auto; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | min-width: 1px; 29 | } 30 | &-ttl { 31 | font-size: 16px; 32 | font-weight: bold; 33 | text-transform: uppercase; 34 | margin-right: 7px; 35 | margin-left: 7px; 36 | } 37 | &-title { 38 | text-transform: uppercase; 39 | letter-spacing: 2px; 40 | color: $light-grey; 41 | font-weight: bold; 42 | margin-right: auto; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/_btn.scss: -------------------------------------------------------------------------------- 1 | .app-btn { 2 | border: 1px solid $primary; 3 | padding: 7px; 4 | border-radius: 0; 5 | display: inline-block; 6 | min-width: 180px; 7 | &.disabled { 8 | opacity: .2; 9 | } 10 | button { 11 | height: 38px; 12 | padding: 0 15px; 13 | background-color: $primary; 14 | color: $black; 15 | text-transform: uppercase; 16 | font-size: 12px; 17 | font-weight: bold; 18 | letter-spacing: 2px; 19 | border: none; 20 | width: 100%; 21 | } 22 | } 23 | 24 | .sl-toggle { 25 | text-transform: uppercase; 26 | color: $primary; 27 | letter-spacing: 2.5px; 28 | font-size: 15px; 29 | font-weight: bold; 30 | height: 55px; 31 | border: 1px solid $primary; 32 | border-radius: 0; 33 | background-color: $black; 34 | &:active, 35 | &:active:hover, 36 | &:hover { 37 | background-color: $black; 38 | border-color: $primary; 39 | color: $primary; 40 | } 41 | &.active { 42 | background-color: $primary; 43 | color: $black; 44 | &:active, 45 | &:active:hover, 46 | &:hover { 47 | color: $black; 48 | border-color: $primary; 49 | background-color: $primary; 50 | } 51 | } 52 | } 53 | 54 | .icon-btn { 55 | background: none; 56 | border: none; 57 | } 58 | @media (max-width: 768px) { 59 | .sl-toggle { 60 | white-space: normal; 61 | font-size: 12px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/styles/_common.scss: -------------------------------------------------------------------------------- 1 | *:focus, 2 | *:active, 3 | *:hover { 4 | outline: none !important; 5 | box-shadow: none !important; 6 | } 7 | * { 8 | box-sizing: border-box; 9 | font: inherit; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | .green { 14 | color: $primary; 15 | } 16 | 17 | hr { 18 | border-color: rgba($light-grey2, 0.5); 19 | } 20 | 21 | .mt40 { 22 | margin-top: 40px; 23 | } 24 | 25 | .ml20 { 26 | margin-left: 20px; 27 | } 28 | 29 | .section-header { 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | padding-bottom: 17px; 34 | border-bottom: 1px solid $border-color; 35 | margin-bottom: 30px; 36 | button { 37 | background-color: transparent; 38 | border-color: $primary; 39 | color: $primary; 40 | text-transform: uppercase; 41 | font-size: 12px; 42 | letter-spacing: 2.5px; 43 | padding: 0 13px; 44 | height: 40px; 45 | span { 46 | display: flex; 47 | align-items: center; 48 | svg { 49 | margin-right: 10px; 50 | width: 19px; 51 | height: 21px; 52 | } 53 | } 54 | } 55 | } 56 | 57 | .decor { 58 | position: relative; 59 | &::before { 60 | content: ''; 61 | position: absolute; 62 | left: -138px; 63 | top: 50%; 64 | transform: translateY(-50%); 65 | height: 45px; 66 | width: 99px; 67 | display: block; 68 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGMAAAAuCAYAAAA8/acnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAIoSURBVHgB7ZvNbRsxEIW52l1BN6uEDSQBusUlbAdxCZsKbHeQVJASonQgV2Cmg+QsCdpUEPkm6NePxp6MGdKgYIAw3gcIPDw98olz8IADZ9PptDoej7UROJ/P7XK5tMbDZDJpNG2/38/btt1IWlVVw7IsbzTvYrGYGQ+j0eim1+sNlXMtzm01b2zmznuN5VrSQvcVyly4QmCTn4rfdh8Vj9dkWfaIRfxhg8FgeDgcVC+YeTS39y3OriUtz/MGS6t5YzN33i9YvinazHjuK5S5Z0gysBgJwWIkBIuRECxGQhSn0+k//spbRf9jwlhNQOfwZCK9b0DNht/zz0SeG8rs9sZ3rKShbQ3d1yWZCSGEEEIIIYQQQj4aWTcK/CGJeLeyq9Xqq28DjCHXmoZJXr1er8U3l27c+6h5MXb9ZDyMx+M53nM+Sxpy3yH3g+aNzdx5b7HcSRrerOYYu96byMyFm8lik0r6AoyVCaB538IlXnDl8Q/NO50Lr9u78mg+vJn5hJ4QLEZCsBgJwWIkBIuREEWe59a1c5KIbio0NnXUmuBrEbfb7aYoitpEgmz36EyuJA0t89+AvdYEX2YHMv/CfVlJK8vS670wMyGEEEIIIYQQQshHg2PXV3DsGgfHruT9YDESgsVICBYjIViMhHgZu6KtaiQRrecmtAG6sUbT0CaqY1s3du33+42J5zvOnknCbrf77TPGZnagw3zAp5U03GPoP1a9mZ8BSwFXa2Vh5IIAAAAASUVORK5CYII=') 69 | } 70 | } 71 | 72 | @media (max-width: 768px) { 73 | .text-center-xs { 74 | text-align: center; 75 | } 76 | .section-header { 77 | margin-bottom: 0; 78 | button { 79 | border: none; 80 | padding: 0 7px; 81 | span { 82 | svg { 83 | margin-right: 0; 84 | } 85 | span { 86 | display: none; 87 | } 88 | } 89 | } 90 | } 91 | .mb25-xs { 92 | margin-bottom: 25px; 93 | } 94 | } 95 | 96 | .btns { 97 | & > *:not(:last-child) { 98 | margin-right: 15px; 99 | } 100 | } 101 | @media (max-width: 992px) { 102 | .section-header { 103 | flex-wrap: wrap; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/styles/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown-toggle { 2 | background-color: $black; 3 | border-radius: 0; 4 | border-color: $primary; 5 | margin-right: 0 !important; 6 | color: $primary; 7 | font-size: 12px; 8 | text-transform: uppercase; 9 | letter-spacing: 2.5px; 10 | &:hover { 11 | background-color: $black; 12 | color: $primary; 13 | border-color: $primary; 14 | } 15 | } 16 | .dropdown { 17 | &.active > .dropdown-toggle, 18 | &.open > .dropdown-toggle, 19 | &.open > .dropdown-toggle.focus, 20 | &.open > .dropdown-toggle:focus, 21 | &.open > .dropdown-toggle:hover, 22 | .dropdown-toggle:focus, 23 | .dropdown-toggle:active, 24 | .dropdown-toggle:active.focus, 25 | .dropdown-toggle:active:focus { 26 | background-color: $black; 27 | color: $primary; 28 | border-color: $primary; 29 | } 30 | } 31 | 32 | .dropdown-menu { 33 | background-color: $black; 34 | border-radius: 0; 35 | border-color: $primary; 36 | padding: 0; 37 | & > li > a { 38 | color: $primary; 39 | font-size: 12px; 40 | letter-spacing: 1px; 41 | line-height: 2.5; 42 | &:hover { 43 | color: $black; 44 | background-color: $primary; 45 | } 46 | } 47 | } 48 | .dropdown-menu > .active > a, 49 | .dropdown-menu > .active > a:focus, 50 | .dropdown-menu > .active > a:hover { 51 | background-color: $primary; 52 | color: $black; 53 | } 54 | -------------------------------------------------------------------------------- /src/styles/_form.scss: -------------------------------------------------------------------------------- 1 | .form-group { 2 | position: relative; 3 | &.has-success { 4 | .sl-input::before { 5 | border-color: $success-border; 6 | } 7 | } 8 | &.has-error { 9 | .sl-input::before { 10 | border-color: $error-border; 11 | } 12 | } 13 | .control-label { 14 | text-transform: uppercase; 15 | letter-spacing: 2px; 16 | color: $light-grey; 17 | margin-bottom: 25px; 18 | } 19 | .form-control { 20 | background-color: $black; 21 | border: none; 22 | border-radius: 0; 23 | height: 38px; 24 | margin-left: 6px; 25 | width: calc(100% - 12px); 26 | color: $white; 27 | &:focus { 28 | box-shadow: none; 29 | } 30 | } 31 | .form-control-feedback { 32 | right: 5px; 33 | } 34 | &.has-feedback label~.form-control-feedback { 35 | top: 48px; 36 | } 37 | .input-group { 38 | background-color: $black; 39 | margin: 0 6px; 40 | input { 41 | width: 100%; 42 | margin-left: 0; 43 | } 44 | } 45 | } 46 | .help-block { 47 | margin-top: 12px; 48 | } 49 | .has-success .form-control-feedback { 50 | color: $primary; 51 | } 52 | 53 | .has-error .form-control-feedback { 54 | color: $error; 55 | } 56 | .form-control-feedback { 57 | top: 3px; 58 | } 59 | -------------------------------------------------------------------------------- /src/styles/_help-block.scss: -------------------------------------------------------------------------------- 1 | .has-success .help-block { 2 | color: $primary; 3 | } 4 | .has-error .help-block { 5 | color: $error; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/_input.scss: -------------------------------------------------------------------------------- 1 | .sl-input { 2 | position: relative; 3 | width: calc(100% - 12px); 4 | &::before { 5 | content: ''; 6 | display: block; 7 | width: calc(100% + 12px); 8 | height: 50px; 9 | border: 1px solid $black; 10 | position: absolute; 11 | top: -6px; 12 | left: -6px; 13 | pointer-events: none; 14 | } 15 | input { 16 | background-color: $black; 17 | height: 38px; 18 | border: none; 19 | color: $white; 20 | &:focus { 21 | border-color: transparent; 22 | box-shadow: none; 23 | } 24 | } 25 | button { 26 | background-color: $black; 27 | border: none; 28 | border-radius: 0; 29 | padding: 6px 12px; 30 | color: $primary; 31 | display: flex; 32 | align-items: center; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/_loader.scss: -------------------------------------------------------------------------------- 1 | .bars { 2 | height: 30px; 3 | display: flex; 4 | align-items: flex-end; 5 | justify-content: center; 6 | } 7 | .bars div { 8 | display: inline-block; 9 | position: relative; 10 | background: $primary; 11 | margin: 0 2px; 12 | } 13 | .bar1 { 14 | height: 10px; 15 | width: 5px; 16 | animation: 1s loader infinite; 17 | } 18 | .bar2 { 19 | height: 10px; 20 | width: 5px; 21 | animation: 1s loader infinite; 22 | animation-delay: 0.3s; 23 | } 24 | .bar3 { 25 | height: 25px; 26 | width: 5px; 27 | animation: 1s loader infinite; 28 | animation-delay: 0.5s; 29 | } 30 | .bar4 { 31 | height: 10px; 32 | width: 5px; 33 | animation: 1s loader infinite; 34 | animation-delay: 0.7s; 35 | } 36 | @keyframes loader { 37 | 0% { 38 | height: 10px; 39 | } 40 | 50% { 41 | height: 25px; 42 | } 43 | 100% { 44 | height: 10px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/styles/_modal.scss: -------------------------------------------------------------------------------- 1 | .sl-modal { 2 | .modal-content { 3 | background-color: $grey; 4 | padding: 10px 30px 40px; 5 | border-radius: 0; 6 | } 7 | .modal-header { 8 | border-color: $border-color; 9 | } 10 | .close { 11 | color: $white; 12 | text-shadow: none; 13 | opacity: 1; 14 | } 15 | } 16 | 17 | .modal-sl-title { 18 | font-size: 23px; 19 | color: $white; 20 | } 21 | 22 | .sl-modal-light { 23 | .modal-sm { 24 | width: 316px; 25 | } 26 | .modal-content { 27 | background-color: $grey2; 28 | text-align: center; 29 | padding: 10px 25px 25px; 30 | } 31 | .modal-header { 32 | border: none; 33 | padding-bottom: 10px; 34 | } 35 | } 36 | .sl-modal-title-light { 37 | color: $white; 38 | text-transform: none; 39 | font-size: 18px; 40 | text-align: center; 41 | letter-spacing: 0.4px; 42 | border-bottom: none; 43 | } 44 | 45 | @media (max-width: 768px) { 46 | .sl-modal { 47 | .modal-dialog { 48 | margin-top: 10px; 49 | margin-left: auto; 50 | margin-right: auto; 51 | } 52 | .modal-content { 53 | padding-left: 15px; 54 | padding-right: 15px; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/_network-select.scss: -------------------------------------------------------------------------------- 1 | .network-select { 2 | display: flex; 3 | align-items: center; 4 | margin-left: auto; 5 | margin-right: 30px; 6 | @media (max-width: 992px) { 7 | order: 3; 8 | margin-right: 0; 9 | margin-left: 0; 10 | width: 100%; 11 | } 12 | &__title { 13 | font-size: 12px; 14 | text-transform: uppercase; 15 | letter-spacing: 2.5px; 16 | margin-right: 10px; 17 | @media (max-width: 992px) { 18 | display: none; 19 | } 20 | } 21 | .form-group { 22 | margin-bottom: 0; 23 | .input-group { 24 | margin: 0; 25 | } 26 | @media (max-width: 992px) { 27 | width: 100%; 28 | } 29 | } 30 | .dropdown { 31 | border: 1px solid $primary; 32 | padding-left: 1px; 33 | } 34 | .dropdown-toggle { 35 | @media (max-width: 992px) { 36 | width: 100%; 37 | font-size: 11px; 38 | letter-spacing: 1.5px; 39 | .caret { 40 | margin-left: auto; 41 | } 42 | } 43 | } 44 | @media (max-width: 992px) { 45 | .dropdown, 46 | .sl-input { 47 | width: 100%; 48 | } 49 | } 50 | .sl-input { 51 | &::before { 52 | display: none; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/_panel.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | background-color: transparent; 3 | &-default { 4 | border: none; 5 | } 6 | .panel-heading { 7 | padding: 20px 0; 8 | text-transform: uppercase; 9 | font-size: 23px; 10 | font-weight: bold; 11 | letter-spacing: 2.5px; 12 | margin-bottom: 26px; 13 | background-color: transparent; 14 | border-bottom: 1px solid $border-color; 15 | color: $light-grey; 16 | } 17 | .panel-body { 18 | background-color: $grey; 19 | padding: 30px 30px 40px; 20 | } 21 | } 22 | @media (max-width: 768px) { 23 | .panel { 24 | .panel-body { 25 | margin-left: -15px; 26 | margin-right: -15px; 27 | padding: 30px 15px; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/_request-modal.scss: -------------------------------------------------------------------------------- 1 | .request-modal { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: $grey; 8 | z-index: 1000; 9 | padding: 30px 0; 10 | overflow-y: auto; 11 | @media (max-width: 768px) { 12 | padding: 10px; 13 | } 14 | 15 | &__header { 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | margin-bottom: 31px; 20 | @media (max-width: 768px) { 21 | margin-bottom: 0; 22 | } 23 | } 24 | 25 | &__row { 26 | margin-bottom: 75px; 27 | @media (max-width: 768px) { 28 | margin-bottom: 0; 29 | } 30 | } 31 | 32 | &__close { 33 | background: none; 34 | border: none; 35 | } 36 | 37 | &__btns { 38 | margin-top: 50px; 39 | 40 | & > div:not(:last-child) { 41 | margin-right: 15px; 42 | @media (max-width: 768px) { 43 | margin-right: 0; 44 | margin-bottom: 15px; 45 | } 46 | } 47 | 48 | @media (max-width: 768px) { 49 | margin-top: 0; 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | } 54 | } 55 | &__alert { 56 | background-color: $black; 57 | margin: 0 0 -15px; 58 | transform: translateY(-30px); 59 | } 60 | @media (max-width: 768px) { 61 | .col-xs-12 { 62 | margin-bottom: 29px; 63 | } 64 | } 65 | .alert { 66 | margin: 0 auto; 67 | background-color: $black; 68 | border-color: $black; 69 | padding-left: 13px; 70 | padding-right: 13px; 71 | @media (min-width: 768px) { 72 | width: 750px; 73 | 74 | @media (min-width: 992px) { 75 | width: 970px; 76 | } 77 | @media (min-width: 1200px) { 78 | width: 1170px; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/styles/_setup.scss: -------------------------------------------------------------------------------- 1 | .setup-warn { 2 | max-width: 550px; 3 | margin-bottom: 32px; 4 | } 5 | 6 | .setup-desc { 7 | margin-bottom: 34px; 8 | margin-top: 31px; 9 | } 10 | 11 | .setup-label { 12 | margin-bottom: 23px; 13 | } 14 | .setup-switch { 15 | margin-bottom: 15px; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | font-family: inherit; 3 | } 4 | .tooltip-inner { 5 | background-color: $white; 6 | color: $grey; 7 | font-size: 12px; 8 | line-height: 16px; 9 | border-radius: 0; 10 | padding: 11px 14px; 11 | } 12 | .tooltip.top-left .tooltip-arrow, 13 | .tooltip.top-right .tooltip-arrow, 14 | .tooltip.top .tooltip-arrow { 15 | border-top-color: $white; 16 | } 17 | .tooltip.bottom-left .tooltip-arrow, 18 | .tooltip.bottom-right .tooltip-arrow, 19 | .tooltip.bottom .tooltip-arrow { 20 | border-bottom-color: $white; 21 | } 22 | .tooltip.right .tooltip-arrow { 23 | border-right-color: $white; 24 | } 25 | .tooltip.left .tooltip-arrow { 26 | border-left-color: $white; 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | .control-label-sl { 2 | text-transform: uppercase; 3 | font-size: 15px; 4 | font-weight: bold; 5 | letter-spacing: 2px; 6 | color: $light-grey; 7 | } 8 | 9 | .text { 10 | font-size: 15px; 11 | line-height: 26px; 12 | letter-spacing: .5px; 13 | &.lg { 14 | font-size: 18px; 15 | font-weight: normal; 16 | line-height: 29px; 17 | } 18 | } 19 | 20 | h2 { 21 | font-size: 23px; 22 | font-weight: bold; 23 | letter-spacing: 2.5px; 24 | text-transform: uppercase; 25 | line-height: 31px; 26 | } 27 | 28 | h4 { 29 | text-transform: uppercase; 30 | letter-spacing: 2px; 31 | color: $white; 32 | font-weight: bold; 33 | margin: 0; 34 | @media (max-width: 768px) { 35 | font-size: 15px; 36 | } 37 | } 38 | 39 | h5 { 40 | text-transform: uppercase; 41 | font-weight: bold; 42 | letter-spacing: 2px; 43 | color: #c4c4c4; 44 | margin-bottom: 25px; 45 | } 46 | @media (max-width: 768px) { 47 | .text.lg { 48 | font-size: 15px; 49 | line-height: 26px; 50 | } 51 | h2 { 52 | letter-spacing: .5px; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | $primary: #00FFAD; 2 | $black: #000; 3 | $white: #fff; 4 | $grey: #202020; 5 | $grey2: #505050; 6 | $light-grey: #c4c4c4; 7 | $light-grey2: #979797; 8 | $error: #F71EF4; 9 | $border-color: rgba($light-grey2, .5); 10 | $success-border: #0E4835; 11 | $error-border: rgba($error, .35); 12 | $warning: #FFC617; 13 | -------------------------------------------------------------------------------- /src/styles/_well.scss: -------------------------------------------------------------------------------- 1 | .well { 2 | background-color: $grey; 3 | border: none; 4 | border-radius: 0; 5 | box-shadow: none; 6 | padding: 25px 29px; 7 | } 8 | 9 | @media (max-width: 768px) { 10 | .well { 11 | margin-left: -15px; 12 | margin-right: -15px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | @import "common"; 3 | @import "btn"; 4 | @import "well"; 5 | @import "typography"; 6 | @import "input"; 7 | @import "panel"; 8 | @import "form"; 9 | @import "setup"; 10 | @import "balance"; 11 | @import "modal"; 12 | @import "dropdown"; 13 | @import "help-block"; 14 | @import "tooltip"; 15 | @import "loader"; 16 | @import "alert"; 17 | @import "request-modal"; 18 | @import "network-select"; 19 | -------------------------------------------------------------------------------- /src/wallet.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Alert, 4 | ControlLabel, 5 | FormControl, 6 | FormGroup, 7 | HelpBlock, 8 | InputGroup, 9 | Modal, 10 | OverlayTrigger, 11 | Panel, 12 | Tooltip, 13 | Well, 14 | Grid, 15 | Row, 16 | Col, 17 | } from 'react-bootstrap'; 18 | import PropTypes from 'prop-types'; 19 | import copy from 'copy-to-clipboard'; 20 | import * as web3 from '@solana/web3.js'; 21 | import bs58 from 'bs58'; 22 | 23 | import Loader from './components/Loader'; 24 | import NetworkSelect from './components/NetworkSelect'; 25 | import RefreshIcon from './icons/refresh.svg'; 26 | import SendIcon from './icons/send.svg'; 27 | import FileCopyIcon from './icons/file-copy.svg'; 28 | import GearIcon from './icons/gear.svg'; 29 | import CloseIcon from './icons/close.svg'; 30 | import WarnIcon from './icons/warn.svg'; 31 | import Button from './components/Button'; 32 | import {Account} from './account'; 33 | import {Settings} from './settings'; 34 | 35 | const alertIcon = { 36 | danger: , 37 | warning: , 38 | }; 39 | 40 | const copyTooltip = ( 41 | Copy public key to clipboard 42 | ); 43 | const refreshBalanceTooltip = ( 44 | Refresh account balance 45 | ); 46 | 47 | const airdropTooltip = Request an airdrop; 48 | 49 | class PublicKeyInput extends React.Component { 50 | state = { 51 | value: '', 52 | validationState: null, 53 | help: '', 54 | }; 55 | 56 | componentDidMount() { 57 | this.handleChange(this.props.defaultValue || ''); 58 | } 59 | 60 | getValidationState(value) { 61 | const length = value.length; 62 | if (length === 0) { 63 | return [null, '']; 64 | } 65 | 66 | try { 67 | new web3.PublicKey(value); 68 | return ['success', this.identityText(value)]; 69 | } catch (err) { 70 | return ['error', 'Invalid Public Key']; 71 | } 72 | } 73 | 74 | handleChange(value) { 75 | const [validationState, help] = this.getValidationState(value); 76 | this.setState({value, validationState, help}); 77 | this.props.onPublicKey(validationState === 'success' ? value : null); 78 | } 79 | 80 | identityText(value) { 81 | if (this.props.identity && value === this.props.defaultValue) { 82 | const {name, keybaseUsername} = this.props.identity; 83 | if (keybaseUsername) { 84 | const verifyUrl = `https://keybase.pub/${keybaseUsername}/solana/validator-${value}`; 85 | return ( 86 | 87 | {`Identified "${name}" who can be verified on `} 88 | Keybase 89 | 90 | ); 91 | } else { 92 | return {`Identified "${name}"`}; 93 | } 94 | } 95 | } 96 | 97 | render() { 98 | const {help, validationState} = this.state; 99 | return ( 100 |
101 | 102 | Recipient's Public Key 103 | 104 | this.handleChange(e.target.value)} 109 | /> 110 | 111 | 112 | {help} 113 | 114 |
115 | ); 116 | } 117 | } 118 | PublicKeyInput.propTypes = { 119 | onPublicKey: PropTypes.func, 120 | defaultValue: PropTypes.string, 121 | identity: PropTypes.object, 122 | }; 123 | 124 | class TokenInput extends React.Component { 125 | state = { 126 | value: '', 127 | validationState: null, 128 | help: '', 129 | }; 130 | 131 | componentDidMount() { 132 | this.handleChange(this.props.defaultValue || ''); 133 | } 134 | 135 | componentDidUpdate(prevProps) { 136 | if (this.props.maxValue !== prevProps.maxValue) { 137 | this.handleChange(this.state.value); 138 | } 139 | } 140 | 141 | getValidationState(value) { 142 | if (value.length === 0) { 143 | return [null, '']; 144 | } 145 | if (parseInt(value) > this.props.maxValue) { 146 | return ['error', 'Insufficient funds, did you account for fees?']; 147 | } 148 | if (value.match(/^\d+$/)) { 149 | return ['success', '']; 150 | } 151 | return ['error', 'Not a valid number']; 152 | } 153 | 154 | handleChange(value) { 155 | const [validationState, help] = this.getValidationState(value); 156 | this.setState({value, validationState, help}); 157 | this.props.onAmount(validationState === 'success' ? value : null); 158 | } 159 | 160 | render() { 161 | return ( 162 |
163 | 164 | Amount 165 | 166 | this.handleChange(e.target.value)} 171 | /> 172 | 173 | 174 | {this.state.help} 175 | 176 |
177 | ); 178 | } 179 | } 180 | TokenInput.propTypes = { 181 | onAmount: PropTypes.func, 182 | defaultValue: PropTypes.string, 183 | maxValue: PropTypes.number, 184 | }; 185 | 186 | class SignatureInput extends React.Component { 187 | state = { 188 | value: '', 189 | validationState: null, 190 | }; 191 | 192 | getValidationState(value) { 193 | if (value.length === 0) return null; 194 | 195 | try { 196 | if (bs58.decode(value).length === 64) { 197 | return 'success'; 198 | } else { 199 | return 'error'; 200 | } 201 | } catch (err) { 202 | return 'error'; 203 | } 204 | } 205 | 206 | handleChange(e) { 207 | const {value} = e.target; 208 | const validationState = this.getValidationState(value); 209 | this.setState({value, validationState}); 210 | this.props.onSignature(validationState === 'success' ? value : null); 211 | } 212 | 213 | render() { 214 | return ( 215 |
216 | 217 | Signature 218 | 219 | this.handleChange(e)} 224 | /> 225 | 226 | 227 | 228 |
229 | ); 230 | } 231 | } 232 | SignatureInput.propTypes = { 233 | onSignature: PropTypes.func, 234 | }; 235 | 236 | class DismissibleMessages extends React.Component { 237 | render() { 238 | const messages = this.props.messages.map(([msg, style], index) => { 239 | return ( 240 | 241 | {alertIcon[style]} 242 | {msg} 243 | this.props.onDismiss(index)}> 244 | 245 | {' '} 246 | 247 | ); 248 | }); 249 | return
{messages}
; 250 | } 251 | } 252 | DismissibleMessages.propTypes = { 253 | messages: PropTypes.array, 254 | onDismiss: PropTypes.func, 255 | }; 256 | 257 | class BusyModal extends React.Component { 258 | render() { 259 | return ( 260 | 266 | 267 | 271 | {this.props.title} 272 | 273 | 274 | 275 | {this.props.text} 276 |
277 |
278 | 279 |
280 |
281 | ); 282 | } 283 | } 284 | BusyModal.propTypes = { 285 | title: PropTypes.string, 286 | text: PropTypes.string, 287 | }; 288 | 289 | class SettingsModal extends React.Component { 290 | render() { 291 | return ( 292 | 298 | 299 | 300 | Settings 301 | 302 | 303 | 304 | 305 | 306 | 307 | ); 308 | } 309 | } 310 | SettingsModal.propTypes = { 311 | onHide: PropTypes.func, 312 | store: PropTypes.object, 313 | }; 314 | 315 | export class Wallet extends React.Component { 316 | state = { 317 | messages: [], 318 | busyModal: null, 319 | settingsModal: false, 320 | balance: 0, 321 | account: null, 322 | url: '', 323 | requestMode: false, 324 | requesterOrigin: '*', 325 | requestPending: false, 326 | requestedPublicKey: '', 327 | requestedAmount: '', 328 | recipientPublicKey: '', 329 | recipientAmount: '', 330 | recipientIdentity: null, 331 | confirmationSignature: null, 332 | transactionConfirmed: null, 333 | }; 334 | 335 | setConfirmationSignature(confirmationSignature) { 336 | this.setState({ 337 | transactionConfirmed: null, 338 | confirmationSignature, 339 | }); 340 | } 341 | 342 | async setRecipientPublicKey(recipientPublicKey) { 343 | this.setState({recipientPublicKey}); 344 | if (recipientPublicKey) { 345 | const recipientIdentity = await this.fetchIdentity( 346 | new web3.PublicKey(recipientPublicKey), 347 | ); 348 | this.setState({recipientIdentity}); 349 | } 350 | } 351 | 352 | async fetchIdentity(publicKey) { 353 | const configKey = new web3.PublicKey( 354 | 'Config1111111111111111111111111111111111111', 355 | ); 356 | const keyAndAccountList = await this.web3sol.getProgramAccounts(configKey); 357 | for (const {account} of keyAndAccountList) { 358 | const validatorInfo = web3.ValidatorInfo.fromConfigData(account.data); 359 | if (validatorInfo && validatorInfo.key.equals(publicKey)) { 360 | return validatorInfo.info; 361 | } 362 | } 363 | } 364 | 365 | setRecipientAmount(recipientAmount) { 366 | this.setState({recipientAmount}); 367 | } 368 | 369 | dismissMessage(index) { 370 | const {messages} = this.state; 371 | messages.splice(index, 1); 372 | this.setState({messages}); 373 | } 374 | 375 | addError(message) { 376 | this.addMessage(message, 'danger'); 377 | } 378 | 379 | addWarning(message) { 380 | this.addMessage(message, 'warning'); 381 | } 382 | 383 | addInfo(message) { 384 | this.addMessage(message, 'info'); 385 | } 386 | 387 | addMessage(message, type) { 388 | const {messages} = this.state; 389 | messages.push([message, type]); 390 | this.setState({messages}); 391 | } 392 | 393 | async runModal(title, text, f) { 394 | this.setState({ 395 | busyModal: {title, text}, 396 | }); 397 | 398 | try { 399 | await f(); 400 | } catch (err) { 401 | console.error(err); 402 | this.addError(err.message); 403 | } 404 | 405 | this.setState({busyModal: null}); 406 | } 407 | 408 | onStoreChange = () => { 409 | const { 410 | networkEntryPoint: url, 411 | feeCalculator, 412 | connection, 413 | accountSecretKey, 414 | minBalanceForRentException, 415 | } = this.props.store; 416 | 417 | this.web3sol = connection; 418 | this.feeCalculator = feeCalculator; 419 | this.minBalanceForRentException = minBalanceForRentException; 420 | 421 | if (url !== this.state.url) { 422 | this.addWarning(`Changed wallet network to "${url}"`); 423 | } 424 | 425 | let account = null; 426 | if (accountSecretKey) { 427 | account = new web3.Account(accountSecretKey); 428 | } 429 | 430 | this.setState({account, url}, this.refreshBalance); 431 | }; 432 | 433 | onAddFunds(params, origin) { 434 | if (!params || this.state.requestPending) return; 435 | if (!params.pubkey || !params.network) { 436 | if (!params.pubkey) this.addError(`Request did not specify a public key`); 437 | if (!params.network) this.addError(`Request did not specify a network`); 438 | return; 439 | } 440 | 441 | let requestedNetwork; 442 | try { 443 | requestedNetwork = new URL(params.network).origin; 444 | } catch (err) { 445 | this.addError(`Request network is invalid: "${params.network}"`); 446 | return; 447 | } 448 | 449 | const walletNetwork = new URL(this.props.store.networkEntryPoint).origin; 450 | if (requestedNetwork !== walletNetwork) { 451 | this.setNetworkEntryPoint(requestedNetwork); 452 | } 453 | 454 | this.setState({ 455 | requesterOrigin: origin, 456 | requestPending: true, 457 | requestedAmount: `${params.amount || ''}`, 458 | requestedPublicKey: params.pubkey, 459 | }); 460 | } 461 | 462 | postWindowMessage(method, params) { 463 | if (window.opener) { 464 | window.opener.postMessage({method, params}, this.state.requesterOrigin); 465 | } 466 | } 467 | 468 | onWindowOpen() { 469 | this.setState({requestMode: true}); 470 | window.addEventListener('message', e => { 471 | if (e.data) { 472 | switch (e.data.method) { 473 | case 'addFunds': 474 | this.onAddFunds(e.data.params, e.origin); 475 | return true; 476 | } 477 | } 478 | }); 479 | 480 | this.postWindowMessage('ready'); 481 | } 482 | 483 | closeRequestModal = () => { 484 | window.close(); 485 | }; 486 | 487 | componentDidMount() { 488 | this.setState({url: this.props.store.networkEntryPoint}, () => { 489 | this.props.store.onChange(this.onStoreChange); 490 | this.onStoreChange(); 491 | if (window.opener) { 492 | this.onWindowOpen(); 493 | } 494 | }); 495 | } 496 | 497 | componentWillUnmount() { 498 | this.props.store.removeChangeListener(this.onStoreChange); 499 | } 500 | 501 | copyPublicKey() { 502 | copy(this.state.account.publicKey); 503 | } 504 | 505 | refreshBalance() { 506 | if (this.state.account) { 507 | this.runModal('Updating Account Balance', 'Please wait...', async () => { 508 | if (this.web3sol) { 509 | const url = this.state.url; 510 | const balance = await this.web3sol.getBalance( 511 | this.state.account.publicKey, 512 | ); 513 | if (url === this.state.url) { 514 | this.setState({balance}); 515 | } 516 | } else { 517 | this.addWarning(`Encountered unexpected error, please report!`); 518 | } 519 | }); 520 | } else { 521 | this.setState({balance: 0}); 522 | } 523 | } 524 | 525 | airdropAmount() { 526 | if (this.feeCalculator && this.feeCalculator.lamportsPerSignature) { 527 | // Drop enough to create 100 rent exempt accounts, that should be plenty 528 | return ( 529 | 100 * 530 | (this.feeCalculator.lamportsPerSignature + 531 | this.minBalanceForRentException) 532 | ); 533 | } 534 | // Otherwise some large number 535 | return 100000000; 536 | } 537 | 538 | requestAirdrop() { 539 | this.runModal('Requesting Airdrop', 'Please wait...', async () => { 540 | const airdrop = this.airdropAmount(); 541 | await this.web3sol.requestAirdrop(this.state.account.publicKey, airdrop); 542 | this.setState({ 543 | balance: await this.web3sol.getBalance(this.state.account.publicKey), 544 | }); 545 | }); 546 | } 547 | 548 | sendTransaction(closeOnSuccess) { 549 | this.runModal('Sending Transaction', 'Please wait...', async () => { 550 | const amount = this.state.recipientAmount; 551 | this.setState({requestedAmount: '', requestPending: false}); 552 | const transaction = web3.SystemProgram.transfer({ 553 | fromPubkey: this.state.account.publicKey, 554 | toPubkey: new web3.PublicKey(this.state.recipientPublicKey), 555 | lamports: amount, 556 | }); 557 | 558 | let signature = ''; 559 | try { 560 | signature = await web3.sendAndConfirmTransaction( 561 | this.web3sol, 562 | transaction, 563 | [this.state.account], 564 | {confirmations: 1}, 565 | ); 566 | } catch (err) { 567 | // Transaction failed but fees were still taken 568 | this.setState({ 569 | balance: await this.web3sol.getBalance(this.state.account.publicKey), 570 | }); 571 | this.postWindowMessage('addFundsResponse', {err: true}); 572 | throw err; 573 | } 574 | 575 | this.addInfo(`Transaction ${signature} has been confirmed`); 576 | this.postWindowMessage('addFundsResponse', {signature, amount}); 577 | if (closeOnSuccess) { 578 | window.close(); 579 | } else { 580 | this.setState({ 581 | balance: await this.web3sol.getBalance(this.state.account.publicKey), 582 | }); 583 | } 584 | }); 585 | } 586 | 587 | confirmTransaction() { 588 | this.runModal('Confirming Transaction', 'Please wait...', async () => { 589 | const result = ( 590 | await this.web3sol.confirmTransaction( 591 | this.state.confirmationSignature, 592 | 1, 593 | ) 594 | ).value; 595 | console.log({result}); 596 | const transactionConfirmed = 597 | result !== null && 598 | (result.confirmations === null || result.confirmations > 0); 599 | this.setState({ 600 | transactionConfirmed, 601 | }); 602 | }); 603 | } 604 | 605 | sendDisabled() { 606 | return ( 607 | this.state.recipientPublicKey === null || 608 | this.state.recipientAmount === null 609 | ); 610 | } 611 | 612 | render() { 613 | if (!this.state.account) { 614 | return ; 615 | } 616 | 617 | const busyModal = this.state.busyModal ? ( 618 | 623 | ) : null; 624 | 625 | const settingsModal = this.state.settingsModal ? ( 626 | this.setState({settingsModal: false})} 630 | /> 631 | ) : null; 632 | 633 | return ( 634 |
635 | {busyModal} 636 | {settingsModal} 637 | {this.state.requestMode 638 | ? this.renderTokenRequestPanel() 639 | : this.renderMainPanel()} 640 |
641 | ); 642 | } 643 | 644 | setNetworkEntryPoint(val) { 645 | if (this.props.store.networkEntryPoint !== val) { 646 | this.setState( 647 | { 648 | busyModal: { 649 | title: 'Changing network', 650 | text: 'Please wait...', 651 | }, 652 | }, 653 | () => { 654 | this.props.store.setNetworkEntryPoint(val); 655 | }, 656 | ); 657 | } 658 | } 659 | 660 | renderMainPanel() { 661 | const {store} = this.props; 662 | const {networkEntryPoint, feeCalculator} = store; 663 | let fee; 664 | if (feeCalculator && feeCalculator.lamportsPerSignature) { 665 | fee = feeCalculator.lamportsPerSignature; 666 | } else { 667 | fee = 5000; 668 | } 669 | let minBalanceForRentException; 670 | if (store.minBalanceForRentException) { 671 | minBalanceForRentException = store.minBalanceForRentException; 672 | } else { 673 | minBalanceForRentException = 42; 674 | } 675 | return ( 676 | 677 |
678 | this.dismissMessage(index)} 681 | /> 682 |
683 | 684 | 685 | 686 |
687 |

network information

688 |
689 |
Network:
690 | 694 |
695 | 700 |
701 | 702 |
703 | 704 | 705 | 706 |

Fee per Signature: {fee} lamports

707 |

708 | Minimum rent exempt balance for empty account:{' '} 709 | {minBalanceForRentException} lamports 710 |

711 |
712 | 713 |
714 | 715 | 716 |
717 |

account information

718 |
719 | 720 |
721 | 722 | 723 | {this.renderAccountBalance()} 724 | 725 | 726 | 727 | 728 | Account Public Key 729 | 730 | 736 | 737 | 738 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 |
752 |
{this.renderPanels()}
753 |
754 | ); 755 | } 756 | 757 | renderPanels() { 758 | return ( 759 | 760 | {this.renderSendTokensPanel()} 761 | {this.renderConfirmTxPanel()} 762 | 763 | ); 764 | } 765 | 766 | renderAccountBalance = () => { 767 | const {balance} = this.state; 768 | return ( 769 | 770 |
771 |
Account Balance
772 | 773 | 776 | 777 | 778 | 781 | 782 |
783 |
784 |
{balance}
785 |
lamports
786 |
787 |
788 | ); 789 | }; 790 | 791 | renderTokenRequestPanel() { 792 | const {store} = this.props; 793 | const {networkEntryPoint} = store; 794 | 795 | return ( 796 |
797 | 798 | 799 | 800 |
801 |

Token Request

802 | 809 |
810 | 811 |
812 |
813 |
814 | this.dismissMessage(index)} 817 | /> 818 |
819 | 820 | 821 | 822 |
823 |

account information

824 |
825 |
Network:
826 | 830 |
831 | 836 |
837 | 838 |
839 | 840 | 841 | {this.renderAccountBalance()} 842 | 843 | 844 | 845 | Account Public Key 846 | 847 | 853 | 854 | 855 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 |
870 |

Send Tokens

871 |
872 | 873 |
874 | 875 | 876 | this.setRecipientAmount(amount)} 881 | /> 882 | 883 | 884 | this.setRecipientPublicKey(publicKey)} 888 | identity={this.state.recipientIdentity} 889 | /> 890 | 891 | 892 | 893 | 894 |
895 | 901 |
902 | 903 |
904 |
905 |
906 | ); 907 | } 908 | 909 | renderSendTokensPanel() { 910 | return ( 911 | 912 | Send Tokens 913 | 914 | 915 | 916 | 917 | this.setRecipientAmount(amount)} 920 | /> 921 | 922 | 923 | 925 | this.setRecipientPublicKey(publicKey) 926 | } 927 | identity={this.state.recipientIdentity} 928 | /> 929 | 930 | 931 | 932 | 933 |
934 | 940 |
941 | 942 |
943 |
944 |
945 |
946 | ); 947 | } 948 | 949 | renderConfirmTxPanel() { 950 | const confirmDisabled = this.state.confirmationSignature === null; 951 | return ( 952 | 953 | Confirm Transaction 954 | 955 | 956 | 957 | 958 | 960 | this.setConfirmationSignature(signature) 961 | } 962 | /> 963 | 964 | 965 | 966 | 967 |
968 | 974 | {typeof this.state.transactionConfirmed === 'boolean' ? ( 975 | 976 | {this.state.transactionConfirmed 977 | ? 'CONFIRMED' 978 | : 'NOT CONFIRMED'} 979 | 980 | ) : ( 981 | '' 982 | )} 983 |
984 | 985 |
986 |
987 |
988 |
989 | ); 990 | } 991 | } 992 | Wallet.propTypes = { 993 | store: PropTypes.object, 994 | }; 995 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-commonjs:0 */ 2 | const webpack = require('webpack'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | 7 | module.exports = { 8 | optimization: { 9 | minimizer: [new TerserPlugin(), new OptimizeCSSAssetsPlugin()], 10 | }, 11 | entry: './src/index.js', 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | exclude: /node_modules/, 17 | use: ['babel-loader'], 18 | }, 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | use: ['babel-loader', 'eslint-loader'], 23 | }, 24 | { 25 | test: /\.(sa|sc|c)ss$/, 26 | use: [ 27 | { 28 | loader: MiniCssExtractPlugin.loader, 29 | options: { 30 | hmr: process.env.NODE_ENV === 'development', 31 | }, 32 | }, 33 | 'css-loader', 34 | 'sass-loader', 35 | ], 36 | }, 37 | { 38 | test: /\.svg$/, 39 | use: ['@svgr/webpack'], 40 | }, 41 | ], 42 | }, 43 | resolve: { 44 | extensions: ['*', '.js', '.jsx'], 45 | }, 46 | node: { 47 | fs: 'empty', 48 | }, 49 | output: { 50 | path: __dirname + '/dist', 51 | publicPath: '/', 52 | filename: 'bundle.js', 53 | }, 54 | plugins: [ 55 | new webpack.HotModuleReplacementPlugin(), 56 | new webpack.EnvironmentPlugin({ 57 | CLUSTER: 'devnet', 58 | }), 59 | new MiniCssExtractPlugin({ 60 | // Options similar to the same options in webpackOptions.output 61 | // all options are optional 62 | filename: '[name].css', 63 | chunkFilename: '[id].css', 64 | ignoreOrder: false, // Enable to remove warnings about conflicting order 65 | }), 66 | ], 67 | devServer: { 68 | contentBase: './dist', 69 | hot: true, 70 | host: '0.0.0.0', 71 | historyApiFallback: { 72 | index: 'index.html', 73 | }, 74 | }, 75 | }; 76 | --------------------------------------------------------------------------------