Forked from gatsby-theme-bch-wallet

This commit is contained in:
Chris Troutner
2022-04-11 16:33:03 -07:00
commit 3ed3531cb0
69 changed files with 59089 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "standard",
"env": {
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 10
},
"rules": {
"no-unused-vars": "off"
}
}
+73
View File
@@ -0,0 +1,73 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity
# Android
android/
+4
View File
@@ -0,0 +1,4 @@
.cache
package.json
package-lock.json
public
+4
View File
@@ -0,0 +1,4 @@
{
"arrowParens": "avoid",
"semi": false
}
+33
View File
@@ -0,0 +1,33 @@
# This is a node.js v8+ JavaScript project
language: node_js
node_js:
- "10"
# Build on Ubuntu Trusty (14.04)
# https://docs.travis-ci.com/user/reference/trusty/#javascript-and-nodejs-images
dist: xenial
sudo: required
# Use Docker
services:
- docker
before_install:
#- npm install -g mocha
# https://github.com/greenkeeperio/greenkeeper-lockfile/issues/156
#install: case $TRAVIS_BRANCH in greenkeeper*) npm i;; *) npm ci;; esac;
install:
- npm install
script: "npm run test"
# Send coverage data to Coveralls
after_success:
- npm run coverage
deploy:
provider: script
skip_cleanup: true
script:
- npx semantic-release
+25
View File
@@ -0,0 +1,25 @@
The MIT License (MIT)
Copyright (c) 2022 Permissionless Software Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
# gatsby-theme-bch-wallet
This repository is a Gatsby Theme. It can be accessed via the [gatsby-theme-bch-wallet npm package](https://www.npmjs.com/package/gatsby-theme-bch-wallet). It is used by the [bch-wallet-starter Gatsby Starter](https://github.com/Permissionless-Software-Foundation/bch-wallet-starter)
If you want to create your own BCH web wallet app, you should [start with the starter](https://github.com/Permissionless-Software-Foundation/bch-wallet-starter).
## Live Demos:
- [Demo of wallet boilerplate](https://demo-wallet.fullstack.cash)
- [Official FullStack.cash wallet](https://wallet.fullstack.cash)
## Background
This is a mobile-first Gatsby Theme that is [IPFS](https://ipfs.io)-ready. It integrates the [AdminLTE Dashboard React components](https://www.npmjs.com/package/adminlte-2-react) to create a dashboard. This app is built as a light-weight Bitcoin Cash (BCH) wallet. It can be forked and the BCH functionality can be leveraged for many different use cases, and to solve many different business problems.
## Installing a Dev Environment
Standard workflow for setting up a development environment for working on this repo:
- `git clone https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet`
- `cd gatsby-ipfs-web-wallet`
- `npm install`
- `npm start`
## Compiling Android App
Hooks are provided for compiling this code base into an Android App using [Capacitor](https://capacitorjs.com/). It's possible to use this code to also compile an iOS app, but it is untested. Here is the flow for building the Android app:
- Follow the instructions above to install the Dev Environment
- Download and install [Android Studio](https://developer.android.com/studio)
- Update the capacitor.config file with the route where Android studio is installed.
- `npm run build` (generates the public folder)
- `npm run android:setup` (generates the android folder)
- `npm run android:open` (opens the android studio with the project)
- Compile the APK with Android Studio.
Note: The `gcradle` and `sdk` versions must be compatible.
## License
[MIT](./LICENSE.md)
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
{
"appId": "com.example.app",
"appName": "wallet.fullstack.cash",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "public",
"plugins": {
"SplashScreen": {
"launchShowDuration": 0
}
},
"cordova": {},
"linuxAndroidStudioPath": "/home/danielhumgon/Downloads/android-studio/bin/studio.sh"
}
+54
View File
@@ -0,0 +1,54 @@
# Wallet Visual Customization
[![](https://avatars3.githubusercontent.com/u/46114007?s=200&v=4)](https://psfoundation.cash/)
# Setup
> We can establish global CSS variables that would allow us to use
> those variables in the different components, to make them
> customizable and dynamic
### Example:
```
const myColor = 'black'
const footerH = '500px'
const styles = document.documentElement.style
styles.setProperty('--main-color', myColor)
styles.setProperty('--footer-height', footerH)
```
This way we can create dynamic variables to change different CSS
properties, to see the use of the global variables we can check
the the [/components/app-colors.css](https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet/blob/master/src/components/app-colors.css)
### Example:
```
const body = document.getElementsByTagName('body')
body[0].style.fontFamily = 'Courier New'
body[0].style.fontSize = '25px'
```
This way we could change some styles of everything covered by the `<body>`
tag, it is basically the whole app.
### Example:
The following example is to customize the color of the warning message:
```
# app-color.css
:root {
--warning-alert-bg-color:'red';
--warning-alert-txt-color:'white';
}
# layout.css
.version-status div{
color: var(--warning-alert-txt-color);
background-color: var(--warning-alert-bg-color);
}
# JS
// We change the values of the CSS variables
const styles = document.documentElement.style
styles.setProperty('--warning-alert-bg-color', 'white')
styles.setProperty('--warning-alert-txt-color', 'black')
```
+2
View File
@@ -0,0 +1,2 @@
import wrapWithProvider from './wrap-with-provider'
export const wrapRootElement = wrapWithProvider
+100
View File
@@ -0,0 +1,100 @@
const ipfsPrefix = process.argv.find(val => val === '--prefix-paths')
// Normal build for sites served with nginx or Apache.
const normalConfig = [
'gatsby-plugin-react-helmet',
{
resolve: 'gatsby-source-filesystem',
options: {
// start_url: '/',
name: 'images',
path: `${__dirname.toString()}/src/images`
}
},
'gatsby-transformer-sharp',
'gatsby-plugin-sharp',
'gatsby-plugin-image',
{
resolve: 'gatsby-plugin-manifest',
options: {
name: 'gatsby-starter-default',
short_name: 'starter',
start_url: '/',
background_color: '#663399',
theme_color: '#663399',
display: 'minimal-ui',
// icon: 'src/images/gatsby-icon.png' // This path is relative to the root of the site.
icons: [
{
src: 'src/images/gatsby-icon.png',
sizes: '192x192',
type: 'image/png'
}
] // Add or remove icon sizes as desired
}
}
// this (optional) plugin enables Progressive Web App + Offline functionality
// To learn more, visit: https://gatsby.dev/offline
// `gatsby-plugin-offline`,
]
// Building for deployment over IPFS or with UncensorablePublishing.com tools.
const ipfsConfig = [
'@chris.troutner/gatsby-plugin-ipfs',
'gatsby-plugin-react-helmet',
{
resolve: 'gatsby-source-filesystem',
options: {
start_url: '__GATSBY_IPFS_PATH_PREFIX__',
name: 'images',
path: `${__dirname.toString()}/src/images`
}
},
'gatsby-transformer-sharp',
'gatsby-plugin-sharp',
'gatsby-plugin-image',
{
resolve: 'gatsby-plugin-manifest',
options: {
name: 'gatsby-starter-default',
short_name: 'starter',
start_url: '__GATSBY_IPFS_PATH_PREFIX__',
background_color: '#663399',
theme_color: '#663399',
display: 'minimal-ui',
// icon: 'src/images/gatsby-icon.png' // This path is relative to the root of the site.
icons: [
{
src: 'src/images/gatsby-icon.png',
sizes: '192x192',
type: 'image/png'
}
] // Add or remove icon sizes as desired
}
}
// this (optional) plugin enables Progressive Web App + Offline functionality
// To learn more, visit: https://gatsby.dev/offline
// `gatsby-plugin-offline`,
]
// Common settings to export.
const exportObj = {
siteMetadata: {
title: 'FullStack.cash Web Wallet',
description: 'A BCH web wallet that uses FullStack.cash for its back end.',
author: '@christroutner'
}
}
// Build for IPFS
if (ipfsPrefix) {
exportObj.pathPrefix = '__GATSBY_IPFS_PATH_PREFIX__'
exportObj.plugins = ipfsConfig
} else {
// Build for normal
exportObj.plugins = normalConfig
}
module.exports = exportObj
+67
View File
@@ -0,0 +1,67 @@
const path = require('path')
const webpack = require('webpack')
exports.onCreateWebpackConfig = ({ stage, actions, getConfig, plugins }) => {
// console.log('stage', stage)
// console.log('actions', actions)
// console.log('getConfig', getConfig)
// https://webpack.js.org/configuration/resolve/
actions.setWebpackConfig({
resolve: {
fallback: {
fs: false,
path: require.resolve('path-browserify'),
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer'),
http: require.resolve('stream-http'),
zlib: require.resolve('browserify-zlib'),
https: require.resolve('https-browserify'),
process: require.resolve('process/browser'),
assert: require.resolve('assert')
},
alias: {
react: path.resolve('./node_modules/react'),
'react-dom': path.resolve('./node_modules/react-dom'),
process: 'process/browser',
mocha: false
}
}
})
// Ignore css order
if (stage === 'build-javascript' || stage === 'develop') {
const config = getConfig()
const miniCssExtractPlugin = config.plugins.find(
plugin => plugin.constructor.name === 'MiniCssExtractPlugin'
)
if (miniCssExtractPlugin) {
miniCssExtractPlugin.options.ignoreOrder = true
}
// Create new webpack plugins for resolve 'process/browser'
// and buffer/Buffer
const processPlugin = new webpack.ProvidePlugin({
process: 'process/browser'
})
const bufferPlugin = new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer']
})
// Add plugins to webpack plugins array
config.plugins.push(processPlugin)
config.plugins.push(bufferPlugin)
// Save the new config
actions.replaceWebpackConfig(config)
}
}
exports.createPages = async ({ actions }, themeOptions) => {
const basePath = '/'
actions.createPage({
path: basePath,
component: require.resolve('./src/pages/index.js')
})
}
+2
View File
@@ -0,0 +1,2 @@
import wrapWithProvider from './wrap-with-provider'
export const wrapRootElement = wrapWithProvider
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
// noop
+52426
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
{
"name": "gatsby-theme-bch-wallet",
"description": "A GatsbyJS Theme with AdminLTE integration and basic Bitcoin Cash wallet functionality.",
"version": "1.3.0",
"main": "index.js",
"author": "Chris Troutner <chris.troutner@gmail.com>",
"contributors": [
"Daniel Gonzalez <danielhumgon@gmail.com>",
"Andre Cabrera <andrecabrera@protonmail.ch>"
],
"license": "MIT",
"scripts": {
"build": "gatsby build",
"build:ipfs": "gatsby build --prefix-paths",
"android:setup": "npx cap add android; npx cap update android; npx cap update android; npx cap copy android; capacitor-resources -p android -I",
"android:open": "npx cap open android",
"develop": "gatsby develop",
"lint": "standard --env mocha --fix",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean",
"test": "npm run lint",
"unit": "mocha test/unit/"
},
"dependencies": {
"@capacitor/android": "3.4.1",
"@capacitor/cli": "3.4.1",
"@capacitor/core": "3.4.1",
"adminlte-2-react": "0.1.31",
"assert": "2.0.0",
"browserify-zlib": "0.2.0",
"capacitor-resources": "2.0.5",
"copy-to-clipboard": "3.3.1",
"crypto-browserify": "3.12.0",
"deepmerge": "4.2.2",
"fs-extra": "10.0.0",
"gatsby": "4.7.2",
"gatsby-plugin-image": "2.7.0",
"gatsby-plugin-manifest": "4.7.0",
"gatsby-plugin-react-helmet": "5.7.0",
"gatsby-plugin-sharp": "4.7.0",
"gatsby-source-filesystem": "4.7.0",
"gatsby-transformer-sharp": "4.7.0",
"https-browserify": "1.0.0",
"jsonrpc-lite": "2.2.0",
"p-queue": "7.2.0",
"p-retry": "5.0.0",
"path-browserify": "1.0.1",
"process": "0.11.10",
"prop-types": "15.8.1",
"qrcode.react": "1.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-jdenticon": "0.0.9",
"react-qr-reader": "2.2.1",
"react-redux": "7.2.6",
"semver": "7.3.5",
"sharp": "0.30.1",
"slp-mutable-data": "1.8.1",
"stream-browserify": "3.0.0",
"stream-http": "3.2.0"
},
"devDependencies": {
"colors": "1.4.0",
"eslint": "8.9.0",
"eslint-config-prettier": "8.4.0",
"eslint-config-standard": "16.0.3",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-standard": "5.0.0",
"husky": "7.0.4",
"mocha": "9.2.1",
"prettier": "2.5.1",
"semantic-release": "19.0.2",
"standard": "16.0.4"
},
"keywords": [
"gatsby",
"gatsby-plugin",
"gatsby-theme"
],
"repository": {
"type": "git",
"url": "https://github.com/Permissionless-Software-Foundation/gatsby-theme-bch-wallet"
},
"bugs": {
"url": "https://github.com/Permissionless-Software-Foundation/gatsby-theme-bch-wallet/issues"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+57
View File
@@ -0,0 +1,57 @@
.sidebar-balance{
height: 100px;
color: white;
display: flex;
justify-content: center;
align-items: center;
}
.sidebar-balance div{
width: 100%;
height: auto;
text-align: center;
}
.sidebar-balance .siderbar-balance-content{
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-end;
}
.sidebar-balance span{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.sidebar-balance .siderbar-balance-content svg{
margin-bottom: 1em!important;
}
.scanner-left-menu {
padding: 5px 5px 5px 18px;
cursor: pointer;
}
/* .scanner-left-menu:hover li a {
color: white;
} */
.scanner-left-menu li a {
display: block;
width: 100%;
}
#import-mnemonic{
text-transform: lowercase;
}
.components-container{
min-height: calc(100vh - var(--footer-height) - 50px);
}
.sidebar-toggle:hover{
background-color: white!important;
color: var(--main-color)!important;
}
+45
View File
@@ -0,0 +1,45 @@
import React from 'react'
import { Content, Row, Col, Box } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
class Audit extends React.Component {
// state = {}
render () {
return (
<Content>
<Row>
<Col sm={4} />
<Col sm={4}>
<Box className='hover-shadow border-none mt-2'>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='eye'
/>
<span>Audit</span>
</h1>
</Col>
<Col sm={12} className='text-center mt-2 mb-2'>
<h3>Never trust, always verify.</h3>
<p>
Check the open source code here. Check and/or change the
REST API in Configure. Install, build and run Bitcoin.com
Mint locally. Join our public telegram group.
</p>
</Col>
</Row>
</Box>
</Col>
<Col sm={4} />
</Row>
</Content>
)
}
}
export default Audit
@@ -0,0 +1,31 @@
import React from 'react'
import { Tabs, Tab } from 'react-bootstrap'
import MenuComponents from './menu-components'
function TabsMenu (props) {
const menuComponents = MenuComponents(props)
return (
<>
{menuComponents.length > 0 && (
<Tabs
defaultActiveKey='Configure'
id='configure-tabs'
onSelect={props.onSelect}
>
<Tab title='General' eventKey='Configure' />
{menuComponents.map((menuItem, i) => (
<Tab
key={`${menuItem.id}-${i}`}
eventKey={menuItem.key}
title={menuItem.key}
{...props}
/>
))}
</Tabs>
)}
</>
)
}
export default TabsMenu
@@ -0,0 +1,71 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Content } from 'adminlte-2-react'
import MenuComponents from './menu-components'
import Servers from './servers'
import JsonWebTokens from './jwt'
import ConfigureInfo from './info'
import TabsMenu from './TabsMenu'
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
let _this
class Configure extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
menuItem: 'Configure'
}
_this.BchWallet = BchWallet
_this.tabsComponents = MenuComponents(props)
this.handleSelect = key => {
_this.setState({ menuItem: key })
}
}
render () {
return (
<Content>
<TabsMenu onSelect={this.handleSelect} />
{/* // Default View */}
{_this.state.menuItem === 'Configure' && (
<>
<Servers
setWalletInfo={_this.props.setWalletInfo}
walletInfo={_this.props.walletInfo}
setBchWallet={_this.props.setBchWallet}
updateBalance={_this.props.updateBalance}
/>
<JsonWebTokens
setWalletInfo={_this.props.setWalletInfo}
walletInfo={_this.props.walletInfo}
setBchWallet={_this.props.setBchWallet}
/>
<ConfigureInfo />
</>
)}
{/* // Load Plugin Views */}
{_this.state.menuItem !== 'Configure' &&
_this.tabsComponents.filter(
menuItem => menuItem.key === _this.state.menuItem
)[0].component}
</Content>
)
}
}
Configure.propTypes = {
setWalletInfo: PropTypes.func.isRequired,
walletInfo: PropTypes.object.isRequired,
setBchWallet: PropTypes.func.isRequired,
updateBalance: PropTypes.func.isRequired
}
export default Configure
@@ -0,0 +1,78 @@
import React from 'react'
import { Row, Col, Box, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { setWalletInfo } from '../../localWallet'
// import BchWallet from 'minimal-slp-wallet'
const BchWallet =
typeof window !== 'undefined'
? window.SlpWallet
: null
let _this
class ConfigureInfo extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
}
_this.BchWallet = BchWallet
}
render () {
return (
<Row>
<Col sm={12}>
<Box className='hover-shadow border-none mt-2'>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='cog'
/>
<span>Configure</span>
</h1>
<Box className='border-none'>
<h3>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='exclamation-triangle'
/>
Be Careful
</h3>
<p>
Backup your wallet first, by writing down your 12-word mnemonic.
Updating the configuration
will restart the app.
</p>
<Button
text='Clear LocalStorage'
type='primary'
className='btn-lg mt-1'
onClick={_this.handleClearLocalStorage}
/>
</Box>
</Col>
</Row>
</Box>
</Col>
</Row>
)
}
// Clear localstorage info
handleClearLocalStorage () {
setWalletInfo({})
window.location.reload()
}
}
export default ConfigureInfo
+145
View File
@@ -0,0 +1,145 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box, Inputs, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// import BchWallet from 'minimal-slp-wallet'
const BchWallet =
typeof window !== 'undefined'
? window.SlpWallet
: null
const { Text } = Inputs
let _this
class JsonWebTokens extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
JWT: '',
errMsg: ''
}
_this.BchWallet = BchWallet
}
render () {
return (
<Row>
<Col sm={12}>
<Box className='hover-shadow border-none mt-2'>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='coins'
/>
<span>JWT</span>
</h1>
<Box className='border-none'>
<Text
id='jwt'
name='JWT'
placeholder='Enter FullStack.cash JWT'
label='Enter FullStack.cash JWT to increase rate limits.'
labelPosition='above'
onChange={_this.handleUpdate}
/>
<Button
text='Update'
type='primary'
className='btn-lg'
onClick={_this.handleUpdateJWT}
/>
</Box>
</Col>
<Col sm={12} className='text-center'>
{_this.state.errMsg && (
<p className='error-color'>{_this.state.errMsg}</p>
)}
</Col>
</Row>
</Box>
</Col>
</Row>
)
}
setJwt () {
const { JWT } = _this.props.walletInfo
if (JWT) {
const jwtElem = document.getElementById('jwt')
jwtElem.value = JWT
}
}
handleUpdate (event) {
const value = event.target.value
_this.setState({
[event.target.name]: value
})
}
async handleUpdateJWT () {
try {
const { mnemonic, selectedServer } = _this.props.walletInfo
const _interface = _this.props.walletInfo.interface
const apiToken = _this.state.JWT
// Update instance with JWT
if (mnemonic) {
const bchjsOptions = { apiToken: apiToken }
if (selectedServer) {
bchjsOptions.restURL = selectedServer
}
bchjsOptions.interface = _interface
// console.log('bchjs options : ', bchjsOptions)
const bchWalletLib = new _this.BchWallet(mnemonic, bchjsOptions)
// Update bchjs instances of minimal-slp-wallet libraries
bchWalletLib.tokens.sendBch.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
bchWalletLib.tokens.utxos.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
_this.props.setBchWallet(bchWalletLib)
}
const walletInfo = _this.props.walletInfo
walletInfo.JWT = apiToken
_this.props.setWalletInfo(walletInfo)
} catch (error) {
_this.setState({
errMsg: error.message
})
}
}
// Reset form and component state
resetValues () {
_this.setState({
JWT: '',
errMsg: ''
})
const jwtElem = document.getElementById('jwt')
jwtElem.value = ''
}
componentDidMount () {
_this.setJwt()
}
}
JsonWebTokens.propTypes = {
setWalletInfo: PropTypes.func.isRequired,
walletInfo: PropTypes.object.isRequired,
setBchWallet: PropTypes.func.isRequired
}
export default JsonWebTokens
@@ -0,0 +1,5 @@
// import React from 'react'
const MenuComponents = props => {
return []
}
export default MenuComponents
@@ -0,0 +1,365 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box, Inputs, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// import BchWallet from 'minimal-slp-wallet'
const Gist = require('../../../services/fetch-servers')
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
const { Text, Select } = Inputs
let _this
class Servers extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
selectOptions: [],
errMsg: '',
selectedServer: '',
showAddField: false,
newServer: '',
inFetch: false,
currentServer: ''
}
_this.BchWallet = BchWallet
}
render () {
return (
<Row>
<Col sm={12}>
<Box
className='hover-shadow border-none mt-2'
loaded={!_this.state.inFetch}
>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='server'
/>
<span>Back End Server</span>
</h1>
<Box className='border-none'>
<Row>
<Col xs={12}>
{_this.state.showAddField
? (
<Text
id='newServer'
name='newServer'
placeholder='Add New Server url'
label='Add New Server Url'
labelPosition='above'
onChange={_this.handleUpdate}
value={_this.state.newServer}
buttonRight={
<Button
text={_this.state.newServer ? ' ADD ' : 'CLOSE'}
type='primary'
onClick={_this.handleNewServerUrl}
/>
}
/>
)
: (
<Select
name='selectedServer'
label='Select Server Url'
labelPosition='above'
options={_this.state.selectOptions}
value={_this.state.selectedServer}
onChange={_this.handleUpdate}
buttonRight={
<Button
icon='fa-plus'
onClick={_this.handleTextField}
/>
}
/>
)}
</Col>
<Col sm={12} />
</Row>
<Button
text='Update'
type='primary'
className='btn-lg'
disabled={_this.state.selectedServer === _this.state.currentServer}
onClick={_this.handleUpdateServer}
/>
</Box>
</Col>
<Col sm={12} className='text-center'>
{_this.state.errMsg && (
<p className='error-color'>{_this.state.errMsg}</p>
)}
</Col>
</Row>
</Box>
</Col>
</Row>
)
}
// turn on / off text field
handleTextField () {
_this.setState({
showAddField: !_this.state.showAddField
})
}
// Add the new server value to the select field
handleNewServerUrl () {
_this.setState({
errMsg: ''
})
const { newServer, selectOptions } = _this.state
try {
if (newServer) {
if (newServer.match(' ')) {
throw new Error('backend url must have no spaces')
}
const alreadyExist = selectOptions.find(val => {
return val.value === newServer
})
// Prevent duplicate options
if (alreadyExist) {
_this.setState({
showAddField: false,
selectedServer: newServer
})
_this.resetForm()
return
}
const option = {
value: newServer,
text: newServer
}
// add the new select option
selectOptions.push(option)
}
_this.setState({
selectOptions,
showAddField: false,
selectedServer: newServer || _this.state.selectedServer
})
_this.resetForm()
} catch (error) {
_this.setState({
errMsg: error.message
})
}
}
// populate select field with select options from localstorage
populateSelect (externalsServers = []) {
try {
const walletInfo = _this.props.walletInfo
const appServers = walletInfo.servers
const allServers = appServers.concat(externalsServers)
// Remove duplicates
const servers = [...new Set(allServers)]
const selectOptions = []
// populate select field with data from localstorage
for (let i = 0; i < servers.length; i++) {
const option = {
value: servers[i],
text: servers[i]
}
selectOptions.push(option)
}
_this.setState({
selectOptions,
currentServer: walletInfo.selectedServer,
selectedServer: walletInfo.selectedServer
})
} catch (error) {
console.warn(error)
}
}
async componentDidMount () {
const externalsServers = await _this.getGistServers()
_this.populateSelect(externalsServers)
}
handleUpdate (event) {
const value = event.target.value
_this.setState({
[event.target.name]: value
})
}
handleUpdateServer () {
// Show loader spinner
_this.setState({
inFetch: true
})
_this.handleNewServerUrl()
// await for state update delay
setTimeout(() => {
_this.updateWalletInstance()
}, 500)
_this.setState({
currentServer: _this.state.selectedServer
})
}
// Update the wallet instance state
updateWalletInstance () {
try {
const { mnemonic, JWT } = _this.props.walletInfo
const apiToken = JWT
const restURL = _this.state.selectedServer
const _interface = _this.props.walletInfo.interface
// Update instance with the selected url
if (mnemonic) {
const bchjsOptions = {}
if (apiToken) {
bchjsOptions.apiToken = apiToken
}
bchjsOptions.restURL = restURL
bchjsOptions.interface = _interface
// console.log('bchjs options : ', bchjsOptions)
const bchWalletLib = new _this.BchWallet(mnemonic, bchjsOptions)
// Update bchjs instances of minimal-slp-wallet libraries
bchWalletLib.tokens.sendBch.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
bchWalletLib.tokens.utxos.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
_this.props.setBchWallet(bchWalletLib)
// update server current price
_this.handleUpdateBalance(bchWalletLib)
} else {
// Hide loader spinner
_this.setState({
inFetch: false
})
}
_this.saveServer()
} catch (error) {
console.warn(error)
_this.setState({
errMsg: error.message,
inFetch: false
})
}
}
// store servers in the localstorage
saveServer () {
try {
// store the new server in the localstorage if it does not exist
const walletInfo = _this.props.walletInfo
const { servers } = walletInfo
const selectedValue = _this.state.selectedServer
if (!selectedValue) {
return
}
const alreadyExist = servers.find(val => {
return val === selectedValue
})
// Prevent duplicate values
if (!alreadyExist) {
servers.push(selectedValue)
}
walletInfo.servers = servers
walletInfo.selectedServer = selectedValue
_this.props.setWalletInfo(walletInfo)
} catch (error) {
console.warn(error)
}
}
// Get wallet balance
async handleUpdateBalance (bchWallet) {
try {
const { mnemonic } = _this.props.walletInfo
if (mnemonic && bchWallet) {
const bchWalletLib = bchWallet
await bchWalletLib.walletInfoPromise
const myBalance = await bchWalletLib.getBalance()
const currentRate = (await bchWalletLib.getUsd()) * 100
_this.setState({
currentRate: currentRate
})
_this.props.updateBalance({ myBalance, currentRate })
}
// Hide loader spinner
_this.setState({
inFetch: false
})
} catch (error) {
console.error(error)
// Hide loader spinner
_this.setState({
inFetch: false
})
}
}
async getGistServers () {
try {
_this.setState({
inFetch: true
})
const gistLib = new Gist()
const gistServers = await gistLib.getServerList()
const serversArr = []
for (let i = 0; i < gistServers.length; i++) {
serversArr.push(gistServers[i].url)
}
_this.setState({
inFetch: false
})
return serversArr
} catch (error) {
console.warn(error)
_this.setState({
inFetch: false
})
return []
}
}
// clean the text form field
resetForm () {
_this.setState({
newServer: ''
})
}
}
Servers.propTypes = {
setWalletInfo: PropTypes.func.isRequired,
walletInfo: PropTypes.object.isRequired,
setBchWallet: PropTypes.func.isRequired,
updateBalance: PropTypes.func.isRequired
}
export default Servers
+469
View File
@@ -0,0 +1,469 @@
import React from 'react'
import PropTypes from 'prop-types'
import siteConfig from '../site-config'
// import Audit from "./audit"
import AdminLTE, { Sidebar, Navbar, Box } from 'adminlte-2-react'
// import ScannerModal from '../qr-scanner/modal'
import Layout from '../layout'
import './admin-lte.css'
// import BchWallet from 'minimal-slp-wallet'
import VersionStatus from '../version-status'
// import { BrowserRouter as Router } from 'react-router-dom'
import menuComponents from '../menu-components.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const { Item } = Sidebar
// Screen width to hide the side menu on click
const MENU_HIDE_WIDTH = 770
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
let _this
class AdminLTEPage extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
bchBalance: 0,
showScannerModal: false,
section: '',
menuIsHide: false,
walletInfo: {},
inFetch: false,
usdBalance: 0,
currentRate: 0
}
_this.BchWallet = BchWallet
_this.sidebar = []
// This variables don't get added to
// the state to avoid 'setState()' errors inside render()
_this.activedItem = ''
_this.menuLoaded = false
/* If no wallet exists the default section will be 'Wallet' */
const { mnemonic } = _this.props.walletInfo
_this.defaultSection = mnemonic ? 'Tokens' : 'Wallet'
}
render () {
return (
<>
<AdminLTE
title={[siteConfig.title]}
titleShort={[siteConfig.titleShort]}
theme='blue'
>
<Sidebar.Core>
<Item key='Balance' text='Balance' icon={siteConfig.balanceIcon}>
<Box
className='hover-shadow border-none background-none'
loaded={!_this.state.inFetch}
>
<div className='sidebar-balance'>
<div>
{!_this.state.inFetch && (
<div className='siderbar-balance-content'>
<span>
<h3>{siteConfig.balanceText}</h3>
<span style={{ fontSize: '18px' }}>
{_this.state.bchBalance}
</span>
<small>USD: ${_this.state.usdBalance}</small>
</span>
<FontAwesomeIcon
className='ml-1 icon'
size='lg'
icon='redo'
onClick={_this.handleGetBalance}
/>
</div>
)}
</div>
</div>
</Box>
</Item>
{_this.sidebar}
{_this.renderNewMenuItems(_this.props)}
</Sidebar.Core>
<Navbar.Core>
<VersionStatus />
</Navbar.Core>
<Layout path='/' {..._this.props}>
<div className='components-container'>
{_this.renderNewViewItems(_this.props)}
</div>
</Layout>
</AdminLTE>
{/*
<Router>
<ScannerModal
show={_this.state.showScannerModal}
handleOnHide={_this.onHandleToggleScannerModal}
path='/'
/>
</Router> */}
</>
)
}
// Get wallet balance
async handleGetBalance () {
try {
_this.setState({
inFetch: true
})
const { mnemonic } = _this.props.walletInfo
if (mnemonic && _this.props.bchWallet) {
const bchWalletLib = _this.props.bchWallet
await bchWalletLib.walletInfoPromise
const myBalance = await bchWalletLib.getBalance()
// const bchjs = bchWalletLib.bchjs
const currentRate = (await bchWalletLib.getUsd()) * 100
console.log('currentRate: ', currentRate)
// if (bchjs.restURL.includes('abc.fullstack')) {
// currentRate = (await bchjs.Price.getBchaUsd()) * 100
// } else if (siteConfig.interface === 'consumer-api') {
// bchjs.restURL = `${siteConfig.restURL}/`
// currentRate = (await bchjs.Price.getUsd()) * 100
// } else {
// // BCHN price.
// currentRate = (await bchjs.Price.getUsd()) * 100
// }
_this.setState({
currentRate: currentRate
})
_this.props.updateBalance({ myBalance, currentRate })
}
_this.setState({
inFetch: false
})
} catch (error) {
console.error(error)
_this.setState({
inFetch: false
})
}
}
hideSplashLoader () {
try {
const loader = document.getElementById('___loader')
loader.className = 'display-none'
} catch (error) {
console.error(error)
}
}
async componentDidMount () {
_this.customMenuItems()
// _this.addOnClickEventToScanner()
_this.setDefaultServers()
await _this.updateState()
setTimeout(() => {
_this.dropDownBalance()
_this.handleGetBalance()
_this.hideSplashLoader()
}, 250)
// Add "changeSection" function to redux store
_this.props.setMenuNavigation({ changeTo: _this.changeSection })
}
componentDidUpdate () {
// Update state with the active item-menu selected in component-menu.js
if (_this.menuLoaded && !_this.state.section) {
if (_this.activedItem) {
_this.changeSection(_this.activedItem)
_this.toggleMenuInMobile()
} else {
_this.changeSection(_this.defaultSection)
_this.toggleMenuInMobile()
}
}
_this.updateState()
}
// Update component state when props change
updateState () {
if (_this.props.walletInfo.mnemonic !== _this.state.walletInfo.mnemonic) {
_this.setState({
walletInfo: _this.props.walletInfo
})
}
if (_this.props.bchBalance.bchBalance !== _this.state.bchBalance) {
_this.setState({
bchBalance: _this.props.bchBalance.bchBalance
})
}
if (_this.props.bchBalance.usdBalance !== _this.state.usdBalance) {
_this.setState({
usdBalance: _this.props.bchBalance.usdBalance
})
}
}
// Due to that it is not possible to add the "onClick" method
// directly to the <Item> component we do it using JS
customMenuItems () {
try {
// Ignore menu items without link to components
const ignoreItems = ['Balance', 'Qr Scanner', 'Link']
const menu = document.getElementsByClassName('sidebar-menu')
const ulElement = menu[0]
const childrens = ulElement.children
if (childrens && childrens.length) {
for (let i = 0; i < childrens.length; i++) {
// const href = childrens[i].children[0].href
const textValue = childrens[i].children[0].children[1].textContent
childrens[i].id = textValue
const ignore = ignoreItems.find(val => textValue === val)
// Ignore menu items without link to components
if (!ignore && childrens[i]) {
childrens[i].onclick = () => this.changeSection(textValue)
}
}
}
} catch (error) {
console.error(error)
}
}
// Displays the BCH balance by default
dropDownBalance () {
try {
const balanceEle = document.getElementById('Balance')
balanceEle.children[0].click()
} catch (error) {
console.error(error)
}
}
// Section change, renders the corresponding component
// to the selected section. each menu item corresponds
// to a section.
changeSection (section, data) {
if (data) {
_this.props.setMenuNavigation({ data })
}
if (!section) return
if (_this.state.section === section) return
_this.activeItemById(section)
_this.setState({
section: section
})
_this.hideMenu()
}
// Adds a visual mark to the selected item on the menu
activeItemById (id) {
try {
const elementActived = document.getElementsByClassName('active')
if (elementActived[0]) {
elementActived[0].className = ''
}
const element = document.getElementById(id)
if (element) element.className = `${element.className} active`
} catch (error) {
console.error(error)
}
}
// Hides the side menu when clicking on mobile devices
hideMenu () {
try {
const windowWidth = window.innerWidth
// console.log("Window Width : ",windowWidth)
if (windowWidth > MENU_HIDE_WIDTH) return
// Veryfies if the sideba is open
const sidebarEle = document.getElementsByClassName('main-sidebar')
const style = window.getComputedStyle(sidebarEle[0])
const transform = style.transform // get transform property
// If the transform property has any
// negative property, means that the menu
// is not visible on the screen
if (transform.match('-')) {
// Returns if the menu is already hidden
return
}
const toggleEle = document.getElementsByClassName('sidebar-toggle')
toggleEle[0].click()
} catch (error) {
// console.error(error)
}
}
// Adds the "onClick" event to the QR scanner item
addOnClickEventToScanner () {
try {
const qrScannerEle = document.getElementById('Qr Scanner')
qrScannerEle.onclick = () => _this.onHandleToggleScannerModal()
} catch (error) {
console.error(error)
}
}
// Controller to show the QR scanner
onHandleToggleScannerModal () {
if (!_this.state.showScannerModal) {
_this.hideMenu()
}
_this.setState({
showScannerModal: !_this.state.showScannerModal
})
setTimeout(() => {
console.log(_this.state.showScannerModal)
}, 500)
}
// Render non-default menu items. The catch ensures that the render function
// won't be interrupted if there is an issue porting new menu items.
renderNewMenuItems (props) {
try {
const _menuComponents = menuComponents(props)
return (
_menuComponents &&
_menuComponents.map((m, i) => {
if (m.active && !_this.activedItem && !_this.menuLoaded) {
_this.activedItem = m.key // Prevents this action from being repeated
}
if (!_this.menuLoaded && i === _menuComponents.length - 1) {
_this.menuLoaded = true
}
return m.menuItem
})
)
} catch (err) {
// TODO: Figure out how to return an invisible Item.
return _this.getInvisibleMenuItem() // <Item style={{ display: 'none' }} />
}
}
// Displays the View corresponding to the dynamically loaded menu item.
renderNewViewItems (props) {
try {
const _menuComponents = menuComponents(props)
return (
_menuComponents &&
_menuComponents.map(m => {
if (_this.state.section === m.key) {
return m.component
}
return ''
})
)
} catch (err) {}
}
getInvisibleMenuItem () {
return (
<li style={{ display: 'none' }}>
{/* Adding this childrens prevents console errors */}
<a href='#'>
<span />
<span />
</a>
</li>
)
}
// Define backends servers configuration by default
setDefaultServers () {
try {
const walletInfo = _this.props.walletInfo
const accessLocation = window.location.hostname
console.log('accessLocation', accessLocation)
// return if servers configurations exist
if (walletInfo.selectedServer) return null
const server1 = 'https://free-bch.fullstack.cash/'
const server2 = 'http://localhost:5005/'
const servers = [server1, server2]
// Default server is BCHN.
const selectedServer = server1
/* if (accessLocation === 'wallet.fullstack.cash') {
selectedServer = server1
}
// BCHN wallet.
if (accessLocation === 'bchn-wallet.fullstack.cash') {
selectedServer = server1
}
if (accessLocation === 'abc-wallet.fullstack.cash') {
selectedServer = server2
}
// Split uses BCHN by default
if (accessLocation === 'splitbch.com') {
selectedServer = server1
} */
walletInfo.selectedServer = selectedServer
walletInfo.servers = servers
_this.props.setWalletInfo(walletInfo)
} catch (error) {
console.warn(error)
}
}
// Displays or hides the sidebar
// in the responsive design
toggleMenuInMobile () {
try {
const windowWidth = window.innerWidth
if (windowWidth > MENU_HIDE_WIDTH) return
const toggleEle = document.getElementsByClassName('sidebar-toggle')
toggleEle[0].click()
} catch (error) {}
}
}
// Props prvided by redux
AdminLTEPage.propTypes = {
walletInfo: PropTypes.object.isRequired, // wallet info
bchBalance: PropTypes.object.isRequired, // bch balance
setWalletInfo: PropTypes.func.isRequired, // set wallet info
updateBalance: PropTypes.func.isRequired, // update bch balance
setBchWallet: PropTypes.func.isRequired, // set minimal-slp-wallet instance
bchWallet: PropTypes.object, // get minimal-slp-wallet instance
setTokensInfo: PropTypes.func.isRequired, // set tokens info
tokensInfo: PropTypes.array, // tokens info,
setMenuNavigation: PropTypes.func.isRequired,
menuNavigation: PropTypes.object
}
export default AdminLTEPage
@@ -0,0 +1,55 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Content, Row, Col, Box } from 'adminlte-2-react'
import Receive from './receive'
import Send from './send'
import './send-receive.css'
let _this
class SendReceive extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {}
}
render () {
return (
<>
{_this.props.walletInfo.mnemonic
? (
<Content>
<Receive walletInfo={_this.props.walletInfo} />
<Send
updateBalance={_this.props.updateBalance}
bchWallet={_this.props.bchWallet}
currentRate={_this.props.currentRate}
/>
</Content>
)
: (
<Content>
<Box padding='true' className='container-nofound'>
<Row>
<Col xs={12}>
<em>You need to create or import a wallet first</em>
</Col>
</Row>
</Box>
</Content>
)}
</>
)
}
}
SendReceive.propTypes = {
setWalletInfo: PropTypes.func.isRequired,
walletInfo: PropTypes.object.isRequired,
updateBalance: PropTypes.func.isRequired,
setBchWallet: PropTypes.func.isRequired,
bchWallet: PropTypes.object,
currentRate: PropTypes.number
}
export default SendReceive
@@ -0,0 +1,104 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Content, Row, Col, Box } from 'adminlte-2-react'
import copy from 'copy-to-clipboard'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const QRCode = require('qrcode.react')
let _this
class Receive extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
addr: _this.props.walletInfo.cashAddress,
copySuccess: false
}
}
render () {
return (
<>
<Content>
<>
<Row>
<Col sm={2} />
<Col sm={8}>
<Box className=' border-none mt-2'>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='wallet'
/>
<span>Receive</span>
</h1>
</Col>
<Col sm={12} className='text-center mt-2 mb-2'>
{_this.state.copySuccess &&
<div className='copied-message'>
Copied!
</div>}
<QRCode
className='qr-code'
value={_this.state.addr}
size={256}
includeMargin
fgColor='#333'
onClick={_this.handleCopyAddres}
/>
<p>{_this.state.addr}</p>
<label className='switch-address' htmlFor='address-checkbox'>
<input
id='address-checkbox'
type='checkbox'
onChange={_this.handleChangeAddr}
/>
<span className='slider round' />
</label>
</Col>
</Row>
</Box>
</Col>
<Col sm={2} />
</Row>
</>
</Content>
</>
)
}
handleChangeAddr () {
const checkbox = document.getElementById('address-checkbox')
const { cashAddress, slpAddress } = _this.props.walletInfo
let addr
if (checkbox.checked) {
addr = slpAddress
} else {
addr = cashAddress
}
_this.setState({
addr
})
}
handleCopyAddres () {
const address = _this.state.addr
copy(address)
_this.setState({ copySuccess: true })
setTimeout(function () {
_this.setState({ copySuccess: false })
}, 1500)
}
}
Receive.propTypes = {
walletInfo: PropTypes.object.isRequired
}
export default Receive
@@ -0,0 +1,67 @@
.switch-address {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch-address input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--main-color);
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: var(--main-color);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--main-color);
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.qr-code {
cursor: pointer;
}
.copied-message {
color: #00AA57;
}
@@ -0,0 +1,497 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Content, Row, Col, Box, Inputs, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// import BchWallet from 'minimal-slp-wallet'
import ScannerModal from '../../qr-scanner/modal'
const { Text } = Inputs
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
let _this
class Send extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
address: '',
amountSat: '',
errMsg: '',
txId: '',
showScan: false,
inFetch: false,
sendCurrency: 'USD',
sendMax: false,
explorerURL: ''
}
_this.BchWallet = BchWallet
}
render () {
return (
<>
<Content>
<Row>
<Col sm={2} />
<Col sm={8}>
<Box
loaded={!_this.state.inFetch}
className='hover-shadow border-none mt-2'
>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='paper-plane'
/>
<span>Send</span>
</h1>
<Box className='border-none'>
<Text
id='addressToSend'
name='address'
placeholder='Enter bch address to send'
label='BCH Address'
labelPosition='above'
onChange={_this.handleUpdate}
className='title-icon'
buttonRight={
<Button
icon='fa-qrcode'
onClick={_this.handleModal}
/>
}
/>
<Text
id='amountToSend'
name='amountSat'
value={_this.state.amountSat}
placeholder={`Enter amount to send in ${_this.state.sendCurrency}`}
label='Amount'
labelPosition='above'
onChange={_this.handleUpdate}
addonRight={_this.state.sendCurrency}
disabled={_this.state.sendMax}
buttonRight={
<Button
icon='fa-random'
onClick={_this.handleChangeCurrency}
/>
}
buttonLeft={
<Button
text={_this.state.sendMax ? 'UNDO' : 'MAX'}
onClick={_this.handleSendType}
/>
}
/>
<div className='text-left pb-4'>
<p>
{_this.state.sendCurrency === 'BCH'
? `USD: ${(
_this.state.amountSat *
(_this.props.currentRate / 100)
).toFixed(2)}`
: `BCH: ${(
_this.state.amountSat /
(_this.props.currentRate / 100)
).toFixed(8)}`}
</p>
</div>
<Button
text='Send'
type='primary'
className='btn-lg'
onClick={
_this.state.sendMax
? _this.handleSendAll
: _this.handleSend
}
/>
</Box>
</Col>
<Col sm={12} className='text-center'>
{_this.state.errMsg && (
<p className='error-color'>{_this.state.errMsg}</p>
)}
{_this.state.txId && (
<a
target='_blank'
rel='noopener noreferrer'
href={`${_this.state.explorerURL}/${_this.state.txId}`}
>
Transaction ID: {_this.state.txId}
</a>
)}
</Col>
</Row>
</Box>
</Col>
<Col sm={2} />
</Row>
<ScannerModal
show={_this.state.showScan}
handleOnHide={_this.onHandleToggleScanner}
handleOnScan={_this.onHandleScan}
/>
</Content>
</>
)
}
componentDidMount () {
// const { bchWallet } = _this.props
// console.log('bchWallet', bchWallet)
_this.defineExplorer()
}
// Define the explorer to use
// depending on the selected chain
defineExplorer () {
const bchWalletLib = _this.props.bchWallet
const bchjs = bchWalletLib.bchjs
let explorerURL
if (bchjs.restURL.includes('abc.fullstack')) {
explorerURL = 'https://explorer.bitcoinabc.org/tx'
} else {
explorerURL = 'https://explorer.bitcoin.com/bch/tx'
}
_this.setState({
explorerURL
})
}
handleChangeCurrency () {
if (_this.state.sendCurrency === 'USD') {
_this.setState({
sendCurrency: 'BCH'
})
if (_this.state.amountSat > 0) {
_this.setState({
amountSat: (
_this.state.amountSat /
(_this.props.currentRate / 100)
).toFixed(8)
})
}
} else {
_this.setState({
sendCurrency: 'USD'
})
if (_this.state.amountSat > 0) {
_this.setState({
amountSat: (
_this.state.amountSat *
(_this.props.currentRate / 100)
).toFixed(2)
})
}
}
}
handleSendType () {
const sendMax = !_this.state.sendMax
_this.setState({
sendMax
})
if (sendMax) {
_this.getMaxAmount()
} else {
_this.setState({
amountSat: ''
})
}
}
async getMaxAmount () {
try {
const bchWalletLib = _this.props.bchWallet
// Ensure the wallet UTXOs are up-to-date.
const walletAddr = bchWalletLib.walletInfo.address
await bchWalletLib.utxos.initUtxoStore(walletAddr)
const utxos = bchWalletLib.utxos.utxoStore.bchUtxos
if (!utxos.length) {
throw new Error('No BCH Utxos to spend!')
}
// Get total of satoshis fron the bch utxos
let totalAmount = 0
utxos.map(val => (totalAmount += val.value))
// Convert satoshis to bch
let amountSat = totalAmount / 100000000
// Change the amount to send to USD if is the selected currency
if (_this.state.sendCurrency === 'USD') {
const _usdAmount = amountSat * (_this.props.currentRate / 100)
const usdAmount = Number(_usdAmount.toFixed(2)) // usd Amount
amountSat = usdAmount
}
_this.setState({
amountSat: amountSat
})
} catch (error) {
console.error(error)
_this.setState({
errMsg: error.message,
sendMax: false
})
}
}
async handleSendAll () {
try {
_this.validateInputs()
const bchWalletLib = _this.props.bchWallet
let { address, amountSat } = _this.state
if (_this.state.sendCurrency === 'USD') {
amountSat = (amountSat / (_this.props.currentRate / 100)).toFixed(8)
}
const amountToSend = Math.floor(Number(amountSat) * 100000000)
console.log(`Sending ${amountToSend} satoshis to ${address}`)
if (!bchWalletLib) {
throw new Error('Wallet not found')
}
_this.setState({
inFetch: true
})
// Ensure the wallet UTXOs are up-to-date.
const walletAddr = bchWalletLib.walletInfo.address
await bchWalletLib.utxos.initUtxoStore(walletAddr)
// Send the BCH.
const result = await bchWalletLib.sendAll(address)
// console.log('result',result)
_this.setState({
txId: result
})
// update balance
setTimeout(async () => {
const myBalance = await bchWalletLib.getBalance()
const currentRate = (await bchWalletLib.getUsd()) * 100
_this.props.updateBalance({ myBalance, currentRate })
}, 1000)
_this.resetValues()
} catch (error) {
_this.handleError(error)
}
}
handleUpdate (event) {
const value = event.target.value
_this.setState({
[event.target.name]: value
})
}
async handleSend () {
try {
_this.validateInputs()
const bchWalletLib = _this.props.bchWallet
let { address, amountSat } = _this.state
if (_this.state.sendCurrency === 'USD') {
amountSat = (amountSat / (_this.props.currentRate / 100)).toFixed(8)
}
const amountToSend = Math.floor(Number(amountSat) * 100000000)
console.log(`Sending ${amountToSend} satoshis to ${address}`)
const receivers = [
{
address,
// amount in satoshis, 1 satoshi = 0.00000001 Bitcoin
amountSat: amountToSend
}
]
// console.log("receivers", receivers)
if (!bchWalletLib) {
throw new Error('Wallet not found')
}
_this.setState({
inFetch: true
})
// Ensure the wallet UTXOs are up-to-date.
const walletAddr = bchWalletLib.walletInfo.address
await bchWalletLib.utxos.initUtxoStore(walletAddr)
// Used for debugging.
// console.log(
// `bchWalletLib.utxos.bchUtxos: ${JSON.stringify(
// bchWalletLib.utxos.bchUtxos,
// null,
// 2
// )}`
// )
// Send the BCH.
const result = await bchWalletLib.send(receivers)
console.log('result', result)
_this.setState({
txId: result.txid || result
})
// update balance
setTimeout(async () => {
const myBalance = await bchWalletLib.getBalance()
const currentRate = (await bchWalletLib.getUsd()) * 100
_this.props.updateBalance({ myBalance, currentRate })
}, 1000)
_this.resetValues()
} catch (error) {
_this.handleError(error)
}
}
// Reset form and component state
resetValues () {
_this.setState({
address: '',
amountSat: '',
errMsg: '',
inFetch: false,
sendMax: ''
})
const amountEle = document.getElementById('amountToSend')
amountEle.value = ''
const addressEle = document.getElementById('addressToSend')
addressEle.value = ''
}
validateInputs () {
const { address, amountSat } = _this.state
const amountNumber = Number(amountSat)
if (!address) {
throw new Error('Address is required')
}
if (!amountSat) {
throw new Error('Amount is required')
}
if (!amountNumber) {
throw new Error('Amount must be a number')
}
if (amountNumber < 0) {
throw new Error('Amount must be greater than zero')
}
}
onHandleToggleScanner () {
_this.setState({
showScan: !_this.state.showScan
})
}
handleModal () {
_this.setState({
showScan: !_this.state.showScan
})
}
resetAddressValue () {
_this.setState({
address: '',
errMsg: ''
})
const addressEle = document.getElementById('addressToSend')
addressEle.value = ''
}
onHandleScan (data) {
try {
_this.resetAddressValue()
if (!data) {
throw new Error('No Result!')
}
if (typeof data !== 'string') {
throw new Error('It should scan a bch address or slp address')
}
_this.setState({
address: data,
errMsg: ''
})
const addressEle = document.getElementById('addressToSend')
addressEle.value = data
_this.onHandleToggleScanner()
} catch (error) {
_this.onHandleToggleScanner()
_this.setState({
errMsg: error.message
})
}
}
handleError (error) {
// console.error(error)
let errMsg = ''
if (error.message) {
errMsg = error.message
}
if (error.error) {
if (error.error.match('rate limits')) {
errMsg = (
<span>
Rate limits exceeded, increase rate limits with a JWT token from
<a
style={{ marginLeft: '5px' }}
target='_blank'
href='https://fullstack.cash'
rel='noopener noreferrer'
>
FullStack.cash
</a>
</span>
)
} else {
errMsg = error.error
}
}
_this.setState(prevState => {
return {
...prevState,
errMsg,
txId: '',
inFetch: false
}
})
}
}
Send.propTypes = {
updateBalance: PropTypes.func.isRequired,
bchWallet: PropTypes.object,
currentRate: PropTypes.number
}
export default Send
+308
View File
@@ -0,0 +1,308 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Content, Row, Col, Box, Button } from 'adminlte-2-react'
import TokenCard from './token-card'
import TokenModal from './token-modal'
import Spinner from '../../../images/loader.gif'
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import SendTokens from './send-tokens'
// import { SlpMutableData } from 'slp-mutable-data'
let _this
class Tokens extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
tokens: [],
selectedTokenToView: '',
showModal: false,
inFetch: true,
errMsg: '',
selectedTokenToSend: '',
showForm: false,
txId: null,
explorerURL: ''
}
}
render () {
// const { JWT } = _this.props.walletInfo
return (
<>
<Button
text='Refresh'
icon='fa-redo'
type='primary'
className='btn-md ml-1 mt-1 mb-1'
onClick={() => _this.handleGetTokens(true)}
/>
{_this.state.txId && (
<div className='txIdContainer'>
<button onClick={() => _this.setState({ txId: null })}>
&times;
</button>
<Col xs={12} className='text-center mt-1'>
<Box title='Transaction ID' type='primary' className='p-0'>
<a
target='_blank'
rel='noopener noreferrer'
href={`${_this.state.explorerURL}/${_this.state.txId}`}
>
{_this.state.txId}
</a>
</Box>
</Col>
</div>
)}
{_this.state.showForm && (
<SendTokens
bchWallet={_this.props.bchWallet}
walletInfo={_this.props.walletInfo}
handleBack={_this.onHandleForm}
selectedToken={
_this.state.selectedTokenToSend
? _this.state.selectedTokenToSend
: {}
}
handleSend={() => _this.onHandleGetTokens(true)}
setTxId={_this.setTxId}
/>
)}
{_this.state.inFetch
? (
<div className='spinner'>
<img alt='Loading...' src={Spinner} width={100} />
</div>
)
: (
<Content>
{_this.state.errMsg && (
<Box padding='true' className='container-nofound'>
<Row>
<Col xs={12}>
<em>{_this.state.errMsg}</em>
</Col>
</Row>
</Box>
)}
{_this.state.tokens.length > 0 && (
<>
<Row>
{_this.state.tokens.map((val, i) => {
if (val.qty > 0) {
return (
<Col sm={4} key={`token-${i}`}>
<TokenCard
key={`token-${i}`}
id={`token-${i}`}
token={val}
showToken={_this.showToken}
selectToken={_this.selectToken}
/>
</Col>
)
} else {
return <span key={`token-${i}`} />
}
})}
</Row>
</>
)}
</Content>
)}
<TokenModal
bchWallet={_this.props.bchWallet}
token={
_this.state.selectedTokenToView
? _this.state.selectedTokenToView
: {}
}
handleOnHide={_this.onHandleToggleModal}
show={_this.state.showModal}
explorerURL={_this.state.explorerURL}
/>
</>
)
}
setTxId (txId = null) {
_this.setState({
txId: txId
})
}
onHandleForm () {
_this.setState({
showForm: !_this.state.showForm
})
}
async handleGetTokens (refresh = null) {
_this.setState({
inFetch: true
})
const { mnemonic } = _this.props.walletInfo
const bchWallet = _this.props.bchWallet
let tokens = []
try {
if (!mnemonic || !bchWallet) {
throw new Error(
'You need to create or import a wallet first, to view tokens'
)
}
await bchWallet.walletInfoPromise
if (_this.props.tokensInfo.length > 0 && refresh === null) {
tokens = _this.props.tokensInfo
} else {
tokens = await bchWallet.listTokens()
}
const mutableTokens = await _this.handleMutableData(tokens)
// console.log(`mutableTokens: ${JSON.stringify(mutableTokens, null, 2)}`)
_this.setState({
tokens: mutableTokens,
inFetch: false
})
if (!tokens.length) {
throw new Error('No tokens found on this wallet.')
}
_this.props.setTokensInfo(mutableTokens)
} catch (error) {
_this.handleError(error)
}
}
// Wrapper for handleGetTokens()
async onHandleGetTokens (refresh = null) {
return _this.handleGetTokens(refresh)
}
async componentDidMount () {
_this.defineExplorer()
await _this.handleGetTokens()
}
showToken (selectedTokenToView) {
_this.setState({
selectedTokenToView
})
_this.onHandleToggleModal()
}
selectToken (selectedTokenToSend) {
_this.setState({
selectedTokenToSend
})
!_this.state.showForm && _this.onHandleForm()
const ele = document.getElementById('___gatsby')
ele.scrollIntoView({ behavior: 'smooth' })
}
onHandleToggleModal (refresh = null) {
_this.setState({
showModal: !_this.state.showModal
})
if (refresh) {
_this.handleGetTokens(true)
}
}
handleError (error) {
let errMsg = ''
if (error.message) {
errMsg = error.message
}
if (error.error) {
if (error.error.match('rate limits')) {
errMsg = (
<span>
Rate limits exceeded, increase rate limits with a JWT token from
<a
style={{ marginLeft: '5px' }}
target='_blank'
href='https://fullstack.cash'
rel='noopener noreferrer'
>
FullStack.cash
</a>
</span>
)
} else {
errMsg = error.error
}
}
_this.setState(prevState => {
return {
...prevState,
errMsg: errMsg,
txId: null,
inFetch: false
}
})
}
// Define the explorer to use
// depending on the selected chain
defineExplorer () {
const bchWalletLib = _this.props.bchWallet
const bchjs = bchWalletLib.bchjs
let explorerURL
if (bchjs.restURL.includes('abc.fullstack')) {
explorerURL = 'https://explorer.be.cash/tx'
} else {
explorerURL = 'https://blockchair.com/bitcoin-cash/transaction'
}
_this.setState({
explorerURL
})
}
// try to get mutable data from token id
async handleMutableData (tokensArr) {
try {
const tokens = []
// const slpMutableLib = new SlpMutableData()
for (let i = 0; i < tokensArr.length; i++) {
const token = tokensArr[i]
// try {
// const data = await slpMutableLib.get.mutableData(token.tokenId)
// token.mutableData = data
// // console.log(`data: ${JSON.stringify(data, null, 2)}`)
// } catch (error) {
// // console.warn(error)
// // Skip error
// console.log(
// `Could not access mutable data for ${token.ticker} (${token.tokenId})`
// )
// }
tokens.push(token)
}
return tokens
} catch (error) {
console.log('Error in handleMutableData()')
console.warn(error)
}
}
}
Tokens.propTypes = {
walletInfo: PropTypes.object.isRequired, // wallet info
bchWallet: PropTypes.object, // get minimal-slp-wallet instance
setTokensInfo: PropTypes.func.isRequired, // set tokens info
tokensInfo: PropTypes.array // tokens info
}
export default Tokens
@@ -0,0 +1,399 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box, Inputs, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// import BchWallet from 'minimal-slp-wallet'
import ScannerModal from '../../qr-scanner/modal'
const { Text } = Inputs
const siteConfig = require('../../site-config')
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
let _this
class SendTokens extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
address: '',
amountSat: '',
errMsg: '',
txId: '',
showScan: false,
inFetch: false,
tokenId: ''
}
_this.BchWallet = BchWallet
}
render () {
const { name } = _this.props.selectedToken
return (
<>
<Row>
<Col sm={12}>
<Box className=' border-none mt-2' loaded={!this.state.inFetch}>
<Row>
<Col sm={12} className='text-center'>
<h1 id='SendTokens'>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='paper-plane'
/>
<span>Send</span>
</h1>
<Box className='border-none'>
<Text
id='addressToSend'
name='address'
placeholder='Enter simpleledger address to send'
label='SLP Address'
labelPosition='above'
onChange={_this.handleUpdate}
className='title-icon'
buttonRight={
<Button icon='fa-qrcode' onClick={_this.handleModal} />
}
/>
<Text
id='amountToSend'
name='amountSat'
placeholder='Enter amount to send'
label='Amount'
value={_this.state.amountSat}
labelPosition='above'
onChange={_this.handleUpdate}
/>
<Row className='token-botton-container'>
<Col xs={12} sm={4}>
<Button
text='Send'
type='primary'
className='btn-lg btn-send-token mr-1 ml-1 mt-1'
onClick={_this.handleSend}
/>
</Col>
<Col xs={12} sm={4}>
<Button
text='Send Max'
type='primary'
className='btn-lg btn-send-token mr-1 ml-1 mt-1'
onClick={_this.handleSendMax}
/>
</Col>
<Col xs={12} sm={4}>
<Button
text='Close'
type='primary'
className='btn-lg btn-send-token mr-1 ml-1 mt-1'
onClick={_this.props.handleBack}
/>
</Col>
</Row>
</Box>
</Col>
<Col sm={12} className='text-center'>
{_this.state.errMsg && (
<p className='error-color'>{_this.state.errMsg}</p>
)}
{/* {_this.state.txId && (
<p className=''>
Transaction ID:
<a
target='_blank'
rel='noopener noreferrer'
href={`https://explorer.bitcoin.com/bch/tx/${_this.state.txId}`}
>
{_this.state.txId}
</a>
</p>
)} */}
{name && (
<span>
Selected Token : <b>{name}</b>
</span>
)}
</Col>
</Row>
</Box>
</Col>
</Row>
<ScannerModal
show={_this.state.showScan}
handleOnHide={_this.onHandleToggleScanner}
handleOnScan={_this.onHandleOnScan}
/>
</>
)
}
handleUpdate (event) {
const value = event.target.value
_this.setState({
[event.target.name]: value
})
// console.log(_this.state)
}
// Sets the qty property with
// the corresponding token decimals
floorQty (qty, decimals) {
try {
const a = qty * Math.pow(10, decimals)
const b = Math.floor(a)
const result = b / Math.pow(10, decimals)
return result
} catch (error) {
console.warn(error)
}
}
async handleSendMax () {
const { qty, decimals } = _this.props.selectedToken
_this.setState({
amountSat: _this.floorQty(qty, decimals)
})
}
async handleSend () {
try {
_this.setState({
txId: '',
inFetch: true
})
_this.validateInputs()
const bchWalletLib = _this.props.bchWallet
const { address, amountSat } = _this.state
const { tokenId, qty } = _this.props.selectedToken
// console.log(`qty: ${qty}`)
if (!tokenId) {
throw new Error('There is no token selected')
}
const receiver = {
address,
tokenId,
qty: amountSat
}
if (qty < receiver.qty) {
throw new Error('Insufficient balance')
}
// console.log('receiver', receiver)
if (!bchWalletLib) {
throw new Error('Wallet not found')
}
// Ensure the wallet UTXOs are up-to-date.
const walletAddr = bchWalletLib.walletInfo.address
await bchWalletLib.utxos.initUtxoStore(walletAddr)
// Used for debugging.
// console.log(`receiver: ${JSON.stringify(receiver, null, 2)}`)
// console.log(
// `bchWalletLib.utxos.bchUtxos: ${JSON.stringify(
// bchWalletLib.utxos.bchUtxos,
// null,
// 2
// )}`
// )
// console.log(
// `bchWalletLib.utxos.tokenUtxos: ${JSON.stringify(
// bchWalletLib.utxos.tokenUtxos,
// null,
// 2
// )}`
// )
// Send token.
const result = await bchWalletLib.sendTokens(receiver, 5.0)
// console.log('result: ', result)
if (siteConfig.interface === 'consumer-api') {
_this.setState({
txId: result.txid,
inFetch: false
})
// Set the transaction ID in Tokens state
_this.props.setTxId(result.txid)
} else {
_this.setState({
txId: result,
inFetch: false
})
// Set the transaction ID in Tokens state
_this.props.setTxId(result)
}
_this.resetValues()
setTimeout(() => {
_this.props.handleSend()
_this.props.handleBack()
}, 3000)
} catch (error) {
_this.handleError(error)
}
}
// Reset form and component state
resetValues () {
_this.setState({
address: '',
amountSat: '',
errMsg: ''
})
const amountEle = document.getElementById('amountToSend')
amountEle.value = ''
const addressEle = document.getElementById('addressToSend')
addressEle.value = ''
}
validateInputs () {
const { address, amountSat } = _this.state
const amountNumber = Number(amountSat)
if (!address) {
throw new Error('Address is required')
}
if (!amountSat) {
throw new Error('Amount is required')
}
if (!amountNumber) {
throw new Error('Amount must be a number')
}
if (amountNumber < 0) {
throw new Error('Amount must be greater than zero')
}
}
onHandleToggleScanner () {
_this.setState({
showScan: !_this.state.showScan
})
}
handleModal () {
_this.setState({
showScan: !_this.state.showScan
})
}
resetAddressValue () {
_this.setState({
address: '',
errMsg: ''
})
const addressEle = document.getElementById('addressToSend')
addressEle.value = ''
}
onHandleOnScan (data) {
const validateAdrrs = ['simpleledger', 'bitcoincash']
try {
_this.resetAddressValue()
if (!data) {
throw new Error('No Result!')
}
if (typeof data !== 'string') {
throw new Error('It should scan a bch address or slp address')
}
// Validates that the words "bitcoincash" or "simpleledger" are contained
let isValid = false
for (let i = 0; i < validateAdrrs.length; i++) {
isValid = isValid ? true : data.match(validateAdrrs[i])
if (isValid) {
_this.setState({
address: data,
errMsg: ''
})
const addressEle = document.getElementById('addressToSend')
addressEle.value = data
}
}
if (!isValid) {
throw new Error('It should scan a bch address or slp address')
}
_this.onHandleToggleScanner()
} catch (error) {
_this.onHandleToggleScanner()
_this.setState({
errMsg: error.message
})
}
}
handleError (error) {
// console.error(error)
let errMsg = ''
if (error.message) {
errMsg = error.message
}
if (error.error) {
if (error.error.match('rate limits')) {
errMsg = (
<span>
Rate limits exceeded, increase rate limits with a JWT token from
<a
style={{ marginLeft: '5px' }}
target='_blank'
href='https://fullstack.cash'
rel='noopener noreferrer'
>
FullStack.cash
</a>
</span>
)
} else {
errMsg = error.error
}
}
_this.setState(prevState => {
return {
...prevState,
errMsg,
txId: '',
inFetch: false
}
})
}
componentDidMount () {
_this.setState({
tokenId: _this.props.selectedToken.tokenId
})
}
componentDidUpdate () {
if (_this.props.selectedToken.tokenId !== _this.state.tokenId) {
_this.setState({
tokenId: _this.props.selectedToken.tokenId,
amountSat: ''
})
}
}
}
SendTokens.propTypes = {
walletInfo: PropTypes.object.isRequired, // wallet info
bchWallet: PropTypes.object, // get minimal-slp-wallet instance
selectedToken: PropTypes.object,
handleBack: PropTypes.func.isRequired,
handleSend: PropTypes.func.isRequired,
setTxId: PropTypes.func.isRequired
}
export default SendTokens
@@ -0,0 +1,84 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box, Button } from 'adminlte-2-react'
import Jdenticon from 'react-jdenticon'
import './token.css'
let _this
class TokenCard extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {}
}
render () {
const token = _this.props.token
const mutableData = token.mutableData
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
return (
<>
<Box className='hover-shadow border-none mt-2' id='token-card'>
<Row className='text-center'>
<Col sm={12} className='text-center mt-2 '>
{mutableData && mutableData.tokenIcon ? <img className='mutable-img' src={mutableData.tokenIcon} width={100} /> : <Jdenticon size='100' value={token.tokenId} />}
<hr />
</Col>
<Col sm={12} className='flex justify-content-center '>
<div className='info-container'>
<p className='info-content'>
<b>Ticker: </b>
<span> {token.ticker}</span>
</p>
<p className='info-content'>
<b>Name: </b>
<span>{token.name}</span>
</p>
<p className='info-content'>
<b>Balance</b>
<span>{_this.round(token.qty, token.decimals)}</span>
</p>
</div>
</Col>
<Col xs={6} className='text-center mb-1'>
<Button
text='Info'
type='primary'
className='btn-lg max-width'
onClick={() => {
_this.props.showToken(token)
}}
/>
</Col>
<Col xs={6} className='text-center'>
<Button
text='Send'
type='primary'
className='btn-lg max-width'
onClick={() => {
_this.props.selectToken(token)
}}
/>
</Col>
</Row>
</Box>
</>
)
}
shouldComponentUpdate () {
return false
}
round (value, decimals) {
return Number(Math.round(value + `e${decimals}`) + `e-${decimals}`)
}
}
TokenCard.propTypes = {
token: PropTypes.object.isRequired,
showToken: PropTypes.func.isRequired,
selectToken: PropTypes.func.isRequired
}
export default TokenCard
@@ -0,0 +1,316 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Content, Row, Col, Box, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './token.css'
let _this
class TokenModal extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
copySuccess: '',
isBurnView: false,
txId: '',
errMsg: '',
inFetch: false
}
this.modalFooter = (
<>
<Button text='Close' pullLeft onClick={this.handleModal} />
<Button
type='primary'
text='Burn All'
pullRight
onClick={this.handleConfirm}
/>
</>
)
this.burnFooter = (
<>
<Button text='No' pullLeft onClick={() => this.handleBurnAll(false)} />
<Button
type='primary'
text='Yes'
pullRight
onClick={() => this.handleBurnAll(true)}
/>
</>
)
this.onDoneFooter = (
<>
<Button
text='Close'
pullLeft
onClick={() => this.handleBurnAll(false)}
/>
</>
)
}
render () {
const token = _this.props.token
return (
<>
<Content
title={_this.state.isBurnView ? `Burn All ${token.name}` : token.name}
modal
modalFooter={
!_this.state.isBurnView
? this.modalFooter
: _this.state.txId || _this.state.errMsg
? this.onDoneFooter
: this.burnFooter
}
show={_this.props.show}
modalCloseButton
onHide={_this.handleModal}
>
<Row>
<Col sm={12}>
{_this.state.isBurnView && !_this.state.txId && (
<Box loaded={!_this.state.inFetch} className='border-none'>
<p>
Are you sure you want to burn {`${token.qty} `}
tokens?
</p>
</Box>
)}
{_this.state.isBurnView && _this.state.txId && (
<div className='text-center '>
<p>Transaction ID:</p>
<a
target='_blank'
rel='noopener noreferrer'
href={`${_this.props.explorerURL}/${_this.state.txId}`}
>
{_this.state.txId}
</a>
</div>
)}
{_this.state.isBurnView && _this.state.errMsg && (
<div className='text-center'>
<p className='error-color'> {_this.state.errMsg}</p>
</div>
)}
{!_this.state.isBurnView && (
<Box className=' border-none '>
<Row>
<Col
sm={12}
className='text-center tokenModal-info-container'
>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>TokenId:</b>
</Col>
<Col xs={9} sm={7}>
{token.tokenId}
</Col>
<Col
xs={3}
sm={2}
className={
_this.state.copySuccess ? 'nopadding' : ''
}
>
{_this.state.copySuccess === 'tokenId'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<FontAwesomeIcon
className='icon btn-animation'
style={{ cssFloat: 'right' }}
size='lg'
onClick={() =>
_this.copyToClipBoard('tokenId')}
icon='copy'
/>
)}
</Col>
</Row>
</Col>
</Row>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>URL:</b>
</Col>
<Col xs={12} sm={9}>
<a
href={`http://${token.url}`}
target='_blank'
rel='noopener noreferrer'
>
{token.url}
</a>
</Col>
</Row>
</Col>
</Row>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>Ticker:</b>
</Col>
<Col xs={12} sm={9}>
{token.ticker}
</Col>
</Row>
</Col>
</Row>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>Name:</b>
</Col>
<Col xs={12} sm={9}>
{token.name}
</Col>
</Row>
</Col>
</Row>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>Balance:</b>
</Col>
<Col xs={12} sm={9}>
{token.qty}
</Col>
</Row>
</Col>
</Row>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>Decimals:</b>
</Col>
<Col xs={12} sm={9}>
{token.decimals}
</Col>
</Row>
</Col>
</Row>
<Row className='tokenModal-info-content mt-1 text-left'>
<Col xs={12}>
<Row>
<Col xs={12} sm={3}>
<b>TokenType:</b>
</Col>
<Col xs={12} sm={9}>
{token.tokenType}
</Col>
</Row>
</Col>
</Row>
</Col>
</Row>
</Box>
)}
</Col>
</Row>
</Content>
</>
)
}
// copy info to clipboard
copyToClipBoard (key) {
const val = _this.props.token[key]
const textArea = document.createElement('textarea')
textArea.value = val // copyText.textContent;
document.body.appendChild(textArea)
textArea.select()
document.execCommand('Copy')
textArea.remove()
_this.handleCopySuccess(key)
}
handleCopySuccess (key) {
_this.setState({
copySuccess: key
})
setTimeout(() => {
_this.setState({
copySuccess: ''
})
}, 1000)
}
handleConfirm () {
_this.setState({
isBurnView: true
})
}
handleModal () {
// if txId exist refresh tokens on close
_this.props.handleOnHide(_this.state.txId)
setTimeout(() => {
_this.setState({
isBurnView: false,
txId: '',
errMsg: '',
inFetch: false
})
}, 200)
}
async handleBurnAll (isConfirmed) {
try {
// Dismiss
if (!isConfirmed) {
_this.handleModal()
return
}
/**
* BURN ALL
*
*/
_this.setState({
inFetch: true
})
const { bchWallet, token } = _this.props
const result = await bchWallet.burnAll(token.tokenId)
console.log('burn txid: ', result)
_this.setState({
txId: result,
inFetch: false
})
} catch (error) {
console.warn(error)
_this.setState({
errMsg: error.message,
inFetch: false
})
}
}
}
TokenModal.propTypes = {
token: PropTypes.object.isRequired,
show: PropTypes.bool.isRequired,
handleOnHide: PropTypes.func.isRequired,
bchWallet: PropTypes.object, // get minimal-slp-wallet instance
explorerURL: PropTypes.string
}
export default TokenModal
+110
View File
@@ -0,0 +1,110 @@
#token-card .info-container{
width: fit-content;
}
#token-card .info-container .info-content{
display: flex;
justify-content: space-between;
font-size: medium;
}
#token-card .info-container .info-content b{
margin-right: 15px;
}
#token-card .info-container .info-content span{
text-align: end;
}
#token-card .divider{
width: 100%;
border-bottom: 1px solid black;
}
.tokenModal-info-content{
/* border:0.7px solid var(--main-color); */
border-radius: 5px;
max-width: 100%;
padding-top: 4px;
padding-bottom: 4px;
margin-right: 0px;
margin-left: 0px;
border-top: none;
border-right: none;
border-left: none;
font-size: medium;
}
.tokenModal-info-content div{
overflow:hidden;
white-space:nowrap;
text-overflow: ellipsis;
}
.tokenModal-info-content div b{
margin-right: 5px;
}
.container-nofound {
text-align: center;
max-width: 80%;
margin: 0 auto;
color: rgba(0, 0, 0, 0.7);
font-size: large;
}
.spinner{
display: flex;
height: calc(100vh - var(--footer-height) - 50px);
width: 100%!important;
justify-content: center;
align-items: center;
}
.btn-send-token{
width: 120px;
}
.token-botton-container{
max-width: 70%;
margin: 0 auto!important;
}
.btn-send-token{
min-width: 100%;
}
@media (max-width: 740px) {
.token-botton-container{
max-width: 100%;
}
.btn-send-token{
width: 100%;
margin-right: 0;
margin-left: 0;
}
}
.txIdContainer {
position: relative;
margin-bottom: 100px;
}
@media only screen and (min-width: 768px) {
.txIdContainer {
margin-bottom: 0px;
}
}
.txIdContainer button {
background-color: transparent;
position: absolute;
right: 20px;
top: 20px;
font-size: 18px;
font-weight: bold;
z-index: 999;
border: none;
color: #606c84;
}
.txIdContainer button:focus {
border: none;
outline: none;
}
.copied-text{
width: 100%;
text-align: center;
color: var(--main-color);
}
.nopadding{
padding: 0!important;
}
.mutable-img{
margin-bottom: 5px;
}
+192
View File
@@ -0,0 +1,192 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box, Button } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// import BchWallet from 'minimal-slp-wallet'
const siteConfig = require('../../site-config')
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
let _this
class NewWallet extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
inFetch: false,
errMsg: ''
}
_this.BchWallet = BchWallet
}
render () {
return (
<>
<Row>
<Col sm={2} />
<Col sm={8}>
<Box
className='hover-shadow border-none mt-2'
loaded={!_this.state.inFetch}
>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='plus'
/>
<span>New Wallet</span>
</h1>
</Col>
<Col sm={12} className='text-center mt-2 mb-2'>
<Button
text='Create Wallet'
type='primary'
className='btn-lg'
onClick={_this.handleCreateWallet}
/>
</Col>
<Col sm={12} className='text-center '>
{_this.state.errMsg && (
<p className='error-color mt-2'>{_this.state.errMsg}</p>
)}
</Col>
</Row>
</Box>
</Col>
<Col sm={2} />
</Row>
</>
)
}
async handleCreateWallet () {
try {
const currentWallet = _this.props.walletInfo
if (currentWallet.mnemonic) {
console.warn('Wallet already exists')
/*
* TODO: notify the user that if it has an existing wallet,
* it will get overwritten
*/
}
_this.setState({
inFetch: true
})
const bchjsOptions = _this.getBchjsOptions()
const bchWalletLib = new _this.BchWallet(null, bchjsOptions)
// Update bchjs instances of minimal-slp-wallet libraries
bchWalletLib.tokens.sendBch.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
bchWalletLib.tokens.utxos.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
await bchWalletLib.walletInfoPromise // Wait for wallet to be created.
const walletInfo = bchWalletLib.walletInfo
walletInfo.from = 'created'
walletInfo.interface = bchjsOptions.interface
Object.assign(currentWallet, walletInfo)
const myBalance = await bchWalletLib.getBalance()
const currentRate = (await bchWalletLib.getUsd()) * 100
// console.log("myBalance", myBalance)
// Update redux state
_this.props.setWalletInfo(currentWallet)
_this.props.updateBalance({ myBalance, currentRate })
_this.props.setBchWallet(bchWalletLib)
_this.setState({
inFetch: false,
errMsg: ''
})
} catch (error) {
_this.handleError(error)
}
}
getBchjsOptions () {
try {
const currentWallet = _this.props.walletInfo
// force interface from props
if (_this.props.interface) {
currentWallet.interface = _this.props.interface
}
const _interface = currentWallet.interface || siteConfig.interface
const jwtToken = currentWallet.JWT
const restURL = currentWallet.selectedServer
const bchjsOptions = {}
if (_interface === 'consumer-api') {
bchjsOptions.interface = _interface
bchjsOptions.restURL = restURL
return bchjsOptions
}
if (jwtToken) {
bchjsOptions.apiToken = jwtToken
}
if (restURL) {
bchjsOptions.restURL = restURL
}
if (_interface === 'rest-api') {
bchjsOptions.interface = _interface
}
return bchjsOptions
} catch (error) {
console.warn(error)
}
}
handleError (error) {
// console.error(error)
let errMsg = ''
if (error.message) {
errMsg = error.message
}
if (error.error) {
if (error.error.match('rate limits')) {
errMsg = (
<span>
Rate limits exceeded, increase rate limits with a JWT token from
<a
style={{ marginLeft: '5px' }}
target='_blank'
href='https://fullstack.cash'
rel='noopener noreferrer'
>
FullStack.cash
</a>
</span>
)
} else {
errMsg = error.error
}
}
_this.setState({
inFetch: false,
errMsg: errMsg
})
}
}
NewWallet.propTypes = {
walletInfo: PropTypes.object.isRequired,
setWalletInfo: PropTypes.func.isRequired,
updateBalance: PropTypes.func.isRequired,
setBchWallet: PropTypes.func.isRequired
}
export default NewWallet
+277
View File
@@ -0,0 +1,277 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box, Button, Inputs } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// import BchWallet from 'minimal-slp-wallet'
const siteConfig = require('../../site-config')
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
const { Text } = Inputs
let _this
class ImportWallet extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
mnemonic: '',
privateKey: '',
errMsg: '',
inFetch: false
}
_this.BchWallet = BchWallet
}
render () {
return (
<Row className=''>
<Col sm={2} />
<Col sm={8}>
<Box
className='hover-shadow border-none mt-2'
loaded={!_this.state.inFetch}
>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='file-import'
/>
<span>Import Wallet</span>
</h1>
</Col>
<Col sm={12} className='text-center mt-2 mb-2'>
<Row className='flex justify-content-center'>
<Col sm={8}>
<form autoComplete='off'>
<Text
id='import-mnemonic'
name='mnemonic'
placeholder='12 word mnemonic'
label='12 word mnemonic'
labelPosition='above'
onChange={_this.handleUpdate}
/>
</form>
</Col>
</Row>
</Col>
<Col sm={12} className='text-center mb-2'>
<Button
text='Import'
type='primary'
className='btn-lg'
onClick={_this.handleImportWallet}
/>
</Col>
<Col sm={12} className='text-center'>
{_this.state.errMsg && (
<p className='error-color'>{_this.state.errMsg}</p>
)}
</Col>
</Row>
</Box>
</Col>
<Col sm={2} />
</Row>
)
}
componentDidMount () {
// add max length property to mnemonic input
// document.getElementById("import-mnemonic").maxLength = "12"
}
handleUpdate (event) {
let value = event.target.value
if (event.target.name === 'mnemonic') {
value = value.toLowerCase()
}
_this.setState({
[event.target.name]: value
})
}
async handleImportWallet () {
try {
_this.validateInputs()
const currentWallet = _this.props.walletInfo
if (currentWallet.mnemonic) {
console.warn('Wallet already exists')
/*
* TODO: notify the user that it has an existing wallet,
* and it will get overwritten
*/
}
_this.setState({
inFetch: true
})
const bchjsOptions = _this.getBchjsOptions()
const bchWalletLib = new _this.BchWallet(
_this.state.mnemonic,
bchjsOptions
)
// Update bchjs instances of minimal-slp-wallet libraries
bchWalletLib.tokens.sendBch.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
bchWalletLib.tokens.utxos.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
await bchWalletLib.walletInfoPromise // Wait for wallet to be created.
const walletInfo = bchWalletLib.walletInfo
walletInfo.from = 'imported'
walletInfo.interface = bchjsOptions.interface
Object.assign(currentWallet, walletInfo)
const myBalance = await bchWalletLib.getBalance()
const currentRate = (await bchWalletLib.getUsd()) * 100
_this.setState({
currentRate: currentRate
})
_this.props.updateBalance({ myBalance, currentRate })
// Update redux state
_this.props.setWalletInfo(currentWallet)
_this.props.updateBalance({ myBalance, currentRate })
_this.props.setBchWallet(bchWalletLib)
// Reset form and component state
_this.resetValues()
_this.setState({
inFetch: false
})
} catch (error) {
_this.handleError(error)
}
}
getBchjsOptions () {
try {
const currentWallet = _this.props.walletInfo
// force interface from props
if (_this.props.interface) {
currentWallet.interface = _this.props.interface
}
const _interface = currentWallet.interface || siteConfig.interface
const jwtToken = currentWallet.JWT
const restURL = currentWallet.selectedServer
const bchjsOptions = {}
if (_interface === 'consumer-api') {
bchjsOptions.interface = _interface
bchjsOptions.restURL = restURL
return bchjsOptions
}
if (jwtToken) {
bchjsOptions.apiToken = jwtToken
}
if (restURL) {
bchjsOptions.restURL = restURL
}
if (_interface === 'rest-api') {
bchjsOptions.interface = _interface
}
return bchjsOptions
} catch (error) {
console.warn(error)
}
}
// Reset form and component state
resetValues () {
_this.setState({
mnemonic: '',
privateKey: '',
errMsg: ''
})
const mnemonicEle = document.getElementById('import-mnemonic')
mnemonicEle.value = ''
}
validateInputs () {
const { mnemonic } = _this.state
if (mnemonic) {
const spaceCount = mnemonic.split(' ').length // mnemonic.match(/ /g).length
if (spaceCount !== 12) {
// console.log('reject')
throw new Error('mnemonic must contain 12 words')
}
} else {
throw new Error('12 word mnemonic is required')
}
}
handleError (error) {
// console.error(error)
let errMsg = ''
if (error.message) {
errMsg = error.message
}
if (error.error) {
if (error.error.match('rate limits')) {
errMsg = (
<span>
Rate limits exceeded, increase rate limits with a JWT token from
<a
style={{ marginLeft: '5px' }}
target='_blank'
href='https://fullstack.cash'
rel='noopener noreferrer'
>
FullStack.cash
</a>
</span>
)
} else {
errMsg = error.error
}
}
_this.setState({
inFetch: false,
errMsg: errMsg
})
}
}
ImportWallet.propTypes = {
walletInfo: PropTypes.object.isRequired,
setWalletInfo: PropTypes.func.isRequired,
updateBalance: PropTypes.func.isRequired,
setBchWallet: PropTypes.func.isRequired
}
export default ImportWallet
/*
CT 07/10/2020
I removed this element because it doesn't seem to be working. I need to do some
additional research to figure out if it's possible to generate a mnemonic from
a WIF private key.
<Text
id='privateKey'
name='privateKey'
placeholder='Private Key'
label='Private Key'
labelPosition='above'
onChange={_this.handleUpdate}
/>
*/
+67
View File
@@ -0,0 +1,67 @@
/* eslint-disable */
import React from 'react'
import PropTypes from 'prop-types'
import { Content } from 'adminlte-2-react'
import ImportWallet from './import'
import NewWallet from './create'
import InfoWallets from './info'
import WalletInfo from './wallet-info'
import { interfaces } from 'mocha'
let _this
class Wallet extends React.Component {
constructor(props) {
super(props)
_this = this
this.state = {}
}
render() {
return (
<Content>
<InfoWallets />
{/* Show wallet info is this exists */}
{_this.props.walletInfo.mnemonic && (
<WalletInfo walletInfo={_this.props.walletInfo} />
)}
{/** Shows the 'create' and 'import' cards
* if there's not a wallet created
*/}
{!_this.props.walletInfo.mnemonic && (
<>
<NewWallet
updateBalance={_this.props.updateBalance}
setWalletInfo={_this.props.setWalletInfo}
setBchWallet={_this.props.setBchWallet}
walletInfo={_this.props.walletInfo}
interface={_this.props.interface}
/>
<ImportWallet
updateBalance={_this.props.updateBalance}
setWalletInfo={_this.props.setWalletInfo}
setBchWallet={_this.props.setBchWallet}
walletInfo={_this.props.walletInfo}
interface={_this.props.interface}
/>
</>
)}
{this.props.importComponents}
</Content>
)
}
}
Wallet.propTypes = {
setWalletInfo: PropTypes.func.isRequired,
walletInfo: PropTypes.object.isRequired,
updateBalance: PropTypes.func.isRequired,
setBchWallet: PropTypes.func.isRequired,
interface :PropTypes.string
}
export default Wallet
+47
View File
@@ -0,0 +1,47 @@
import React from 'react'
import { Row, Col, Box } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
class InfoWallets extends React.Component {
// state = {}
render () {
return (
<Row>
<Col sm={2} />
<Col sm={8}>
<Box className='hover-shadow border-none mt-2'>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='exclamation-triangle'
/>
<span>Web Wallet</span>
</h1>
</Col>
<Col sm={12} className='text-center mt-2 mb-2'>
<p>
This is an open source, non-custodial web wallet
supporting Bitcoin Cash (BCH) and SLP tokens. Web wallets
offer user convenience, but they are inherently insecure and
bad for privacy.{' '}
<b>Storing large amounts of money on a web wallet is not
recommended!
</b>
</p>
</Col>
</Row>
</Box>
</Col>
<Col sm={2} />
</Row>
)
}
}
export default InfoWallets
@@ -0,0 +1,291 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Box } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './wallet.css'
let _this
class WalletInfo extends React.Component {
constructor (props) {
super(props)
_this = this
this.state = {
walletInfo: _this.props.walletInfo,
mnemonic: '',
privateKey: '',
cashAddress: '',
address: '',
slpAddress: '',
legacyAddress: '',
hdPath: '',
blurredMnemonic: true,
blurredPrivateKey: true,
copySuccess: ''
}
}
render () {
const eyeIcon = {
mnemonic: _this.state.blurredMnemonic ? 'eye-slash' : 'eye',
privateKey: _this.state.blurredPrivateKey ? 'eye-slash' : 'eye'
}
return (
<Row>
<Col sm={3} lg={2} />
<Col sm={8}>
<Box className='hover-shadow border-none mt-2'>
<Row>
<Col sm={12} className='text-center'>
<h1>
<FontAwesomeIcon
className='title-icon'
size='xs'
icon='wallet'
/>
<span>My Wallet</span>
</h1>
</Col>
<Col sm={12} className='text-center wallet-info-container'>
<Row className='wallet-info-content mt-1 text-left'>
<Col xs={8} sm={9} lg={10}>
<span>
<b>Mnemonic:</b>{' '}
<span
className={_this.state.blurredMnemonic ? 'blurred' : ''}
>
{' '}
{_this.state.mnemonic}{' '}
</span>
</span>
</Col>
<Col xs={4} sm={3} lg={2} className='text-right'>
{_this.state.copySuccess === 'mnemonic'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<>
<FontAwesomeIcon
className='icon btn-animation'
size='lg'
onClick={() => _this.blurMnemonic()}
icon={eyeIcon.mnemonic}
/>
<FontAwesomeIcon
className='icon btn-animation ml-1'
size='lg'
onClick={() => _this.copyToClipBoard('mnemonic')}
icon='copy'
/>
</>
)}
</Col>
</Row>
<Row className='wallet-info-content mt-1 text-left'>
<Col xs={8} sm={9} lg={10}>
<span>
<b>Private Key: </b>{' '}
<span
className={
_this.state.blurredPrivateKey ? 'blurred' : ''
}
>
{' '}
{_this.state.privateKey}{' '}
</span>
</span>
</Col>
<Col xs={4} sm={3} lg={2} className='text-right'>
{_this.state.copySuccess === 'privateKey'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<>
<FontAwesomeIcon
className='icon btn-animation'
size='lg'
onClick={() => _this.blurPrivateKey()}
icon={eyeIcon.privateKey}
/>
<FontAwesomeIcon
className='icon btn-animation ml-1'
size='lg'
onClick={() => _this.copyToClipBoard('privateKey')}
icon='copy'
/>
</>
)}
</Col>
</Row>
<Row className='wallet-info-content mt-1 text-left'>
<Col xs={8} sm={9} lg={10}>
<span>
<b>Cash Address: </b> {_this.state.cashAddress}
</span>
</Col>
<Col xs={4} sm={3} lg={2} className='text-right'>
{_this.state.copySuccess === 'cashAddress'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<FontAwesomeIcon
className='icon btn-animation'
size='lg'
onClick={() => _this.copyToClipBoard('cashAddress')}
icon='copy'
/>
)}
</Col>
</Row>
<Row className='wallet-info-content mt-1 text-left'>
<Col xs={8} sm={9} lg={10}>
<span>
<b>Slp Address: </b> {_this.state.slpAddress}
</span>
</Col>
<Col xs={4} sm={3} lg={2} className='text-right'>
{_this.state.copySuccess === 'slpAddress'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<FontAwesomeIcon
className='icon btn-animation'
size='lg'
onClick={() => _this.copyToClipBoard('slpAddress')}
icon='copy'
/>
)}
</Col>
</Row>
<Row className='wallet-info-content mt-1 text-left'>
<Col xs={8} sm={9} lg={10}>
<span>
<b>Legacy Address: </b> {_this.state.legacyAddress}
</span>
</Col>
<Col xs={4} sm={3} lg={2} className='text-right'>
{_this.state.copySuccess === 'legacyAddress'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<FontAwesomeIcon
className='icon btn-animation'
size='lg'
onClick={() => _this.copyToClipBoard('legacyAddress')}
icon='copy'
/>
)}
</Col>
</Row>
<Row className='wallet-info-content mt-1 text-left'>
<Col xs={8} sm={9} lg={10}>
<span>
<b>HD Path: </b>
<span id='hdpathValue'>{_this.state.hdPath}</span>
</span>
</Col>
<Col xs={4} sm={3} lg={2} className='text-right'>
{_this.state.copySuccess === 'hdPath'
? (
<div className='copied-text'>
<span>Copied!</span>
</div>
)
: (
<FontAwesomeIcon
className='icon btn-animation'
size='lg'
onClick={() => _this.copyToClipBoard('hdPath')}
icon='copy'
/>
)}
</Col>
</Row>
</Col>
</Row>
</Box>
</Col>
<Col sm={3} lg={2} />
</Row>
)
}
blurMnemonic () {
_this.setState({
blurredMnemonic: !_this.state.blurredMnemonic
})
}
blurPrivateKey () {
_this.setState({
blurredPrivateKey: !_this.state.blurredPrivateKey
})
}
// copy info to clipboard
copyToClipBoard (key) {
const val = _this.state[key]
const textArea = document.createElement('textarea')
textArea.value = val // copyText.textContent;
document.body.appendChild(textArea)
textArea.select()
document.execCommand('Copy')
textArea.remove()
_this.handleCopySuccess(key)
}
handleCopySuccess (key) {
_this.setState({
copySuccess: key
})
setTimeout(() => {
_this.setState({
copySuccess: ''
})
}, 1000)
}
componentDidMount () {
// set component state
const wallet = _this.props.walletInfo
Object.entries(wallet).forEach(([key, value]) => {
_this.setState({
[key]: value
})
})
}
componentDidUpdate () {
// set component state
if (_this.props.walletInfo.mnemonic !== _this.state.mnemonic) {
const wallet = _this.props.walletInfo
Object.entries(wallet).forEach(([key, value]) => {
_this.setState({
[key]: value
})
})
}
}
}
WalletInfo.propTypes = {
walletInfo: PropTypes.object.isRequired
}
export default WalletInfo
@@ -0,0 +1,28 @@
.wallet-info-content{
/* border:0.7px solid var(--main-color); */
border-radius: 5px;
max-width: 100%;
padding-top: 4px;
padding-bottom: 4px;
margin-right: 0px;
margin-left: 0px;
border-top: none;
border-right: none;
border-left: none;
}
.wallet-info-content div{
overflow:hidden;
white-space:nowrap;
text-overflow: ellipsis;
}
.blurred {
filter: blur(2.7px);
-webkit-filter: blur(2.7px);
}
.copied-text{
width: 100%;
text-align: center;
color: var(--main-color);
}
+38
View File
@@ -0,0 +1,38 @@
/* Variables ============ */
:root {
--main-color:#00A74F;
--footer-height: 170px;
}
.skin-blue .main-header .logo {
background-color: var(--main-color)!important
}
.skin-blue .main-header .navbar {
background-color: var(--main-color)!important
}
.btn-primary{
background-color: var(--main-color)!important;
border-color: var(--main-color)!important;
}
.modal-header{
background-color: var(--main-color);
}
.modal-header button , .modal-header h4{
color: white;
opacity: inherit;
}
.box{
border-color: var(--main-color)!important;
}
.error-color{
color: red;
}
.input-group-btn svg{
color : var(--main-color)
}
+79
View File
@@ -0,0 +1,79 @@
#footer {
width: 100%;
/*max-height: 50px;*/
min-height: var(--footer-height);
background-color: var(--main-color);
/*display: flex;
justify-content: center;
align-items: center;*/
padding: 1em;
}
#footer a {
color: white;
font-size: 16px;
text-decoration: underline white;
}
#footer ul {
margin: 0px;
list-style-type: lower-roman;
display: grid;
}
#footer li {
display: inline;
margin-left: 1em;
word-break: break-all;
}
#footer li b {
margin-right: 1em;
}
.footer-section {
text-align: start;
color: white;
margin-top: 15px;
}
.footer-section .section-tittle {
color: white;
font-weight: bold;
font-size: 17px;
margin-bottom: 6px;
}
/* .footer-section div{
width: fit-content;
float: right;
} */
#footer ul li::before {
content: "\2022";
color: white;
display: inline-block;
margin-right: 10px;
font-size: 25px;
line-height: 15px;
vertical-align: -6px;
}
#footer svg {
margin-right: 5px;
}
ul #web span{
margin-right: 3.5px;
}
ul #tor span{
margin-right: 9px;
}
@media screen and (max-width: 500px) {
.section-tittle{
margin-bottom: 1rem!important;
}
#footer ul li{
margin-left: 0;
margin-bottom: 1em;
}
#footer ul li a{
display: block!important;
}
#footer ul li .bar-space{
display: none!important;
}
}
+101
View File
@@ -0,0 +1,101 @@
import React from 'react'
import './footer.css'
import { Row, Col } from 'adminlte-2-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import PropTypes from 'prop-types'
// Get the IPFS hash from the BCH Blockchain.
// import Memo from '../../services/memo-hash'
const siteConfig = require('../site-config')
let _this
class Footer extends React.Component {
constructor (props) {
super(props)
_this = this
_this.state = {
ipfsHash: 'No Result',
ipfsHashLink: ''
}
// this.memo = new Memo({
// bchWallet: props.bchWallet,
// bchAddr: siteConfig.memoAddr
// })
}
async componentDidMount () {
// Get hash using memo service
// await this.handleMemoService()
}
// This function retrieves the IPFS CID for the latest copy of this app. That
// CID is written to the BCH blockchain.
// async handleMemoService () {
// // This is a hard-coded hash or 'checkpoint' to use in times when the
// // connection fails.
// let hash = 'QmXT85Xoi7xMRD9m7Ta4Cx8Yrsd2WSzLrq2VRo26KW4xLu'
//
// // Try to retrieve the hash from the BCH blockchain.
// try {
// hash = await this.memo.findHash()
// console.log(`IPFS hash found: ${hash}`)
//
// if (!hash) {
// throw new Error('Hash not found! Falling back to hard coded IPFS hash.')
// }
// } catch (err) {
// console.error('Error trying to retrieve IPFS hash for the site: ', err)
// }
//
// this.setState({
// ipfsHash: hash,
// ipfsHashLink: `https://ipfs.io/ipfs/${hash}`
// })
// }
render () {
return (
<section id='footer'>
<Row className='footer-container'>
<Col md={12} className='footer-section'>
<Row>
<Col md={12} className='mb-1'>
<p className='section-tittle'>Produced By</p>
<a
href={siteConfig.hostUrl}
target='_blank'
rel='noopener noreferrer'
>
{siteConfig.hostText}
</a>
</Col>
<Col md={12}>
<p className='section-tittle'>Source Code</p>
<FontAwesomeIcon className='' size='lg' icon={faGithub} />
<a
href={siteConfig.sourceCode}
target='_blank'
rel='noopener noreferrer'
>
Github
</a>
</Col>
</Row>
</Col>
</Row>
</section>
)
}
}
// Props prvided by redux
Footer.propTypes = {
bchWallet: PropTypes.object // get minimal-slp-wallet instance
}
export default Footer
+42
View File
@@ -0,0 +1,42 @@
import { Link } from 'gatsby'
import PropTypes from 'prop-types'
import React from 'react'
const Header = ({ siteTitle }) => (
<header
style={{
background: 'rebeccapurple',
marginBottom: '1.45rem'
}}
>
<div
style={{
margin: '0 auto',
maxWidth: 960,
padding: '1.45rem 1.0875rem'
}}
>
<h1 style={{ margin: 0 }}>
<Link
to='/'
style={{
color: 'white',
textDecoration: 'none'
}}
>
{siteTitle}
</Link>
</h1>
</div>
</header>
)
Header.propTypes = {
siteTitle: PropTypes.string
}
Header.defaultProps = {
siteTitle: ''
}
export default Header
+39
View File
@@ -0,0 +1,39 @@
import React from 'react'
// import { useStaticQuery, graphql } from 'gatsby'
// import Img from 'gatsby-image'
import { StaticImage } from 'gatsby-plugin-image'
/*
* This component is built using `gatsby-image` to automatically serve optimized
* images with lazy loading and reduced file sizes. The image is loaded using a
* `useStaticQuery`, which allows us to load the image from directly within this
* component, rather than having to pass the image data down from pages.
*
* For more information, see the docs:
* - `gatsby-image`: https://gatsby.dev/gatsby-image
* - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/
*/
const Image = () => {
/* const data = useStaticQuery(graphql`
query {
placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) {
childImageSharp {
fixed {
...GatsbyImageSharpFixed
}
}
}
}
`) */
return (
<>
<StaticImage
src='../images/gatsby-astronaut.png'
alt='please include an alt'
/>
</>
)
}
export default Image
+741
View File
@@ -0,0 +1,741 @@
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: 'Source Sans Pro', sans-serif!important;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
main,
menu,
nav,
section,
summary {
display: block;
}
audio,
canvas,
progress,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
progress {
vertical-align: baseline;
}
[hidden],
template {
display: none;
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:active,
a:hover {
outline-width: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: inherit;
font-weight: bolder;
}
dfn {
font-style: italic;
}
h1 {
font-size: 36px;;
margin: 0.67em 0;
}
mark {
background-color: #ff0;
color: #000;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
figure {
margin: 1em 40px;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
button,
input,
optgroup,
select,
textarea {
font: inherit;
margin: 0;
}
optgroup {
font-weight: 700;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[type="reset"],
[type="submit"],
button,
html [type="button"] {
-webkit-appearance: button;
}
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring,
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
border: 1px solid silver;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
html {
font: 112.5%/1.45em georgia, serif;
box-sizing: border-box;
overflow-y: scroll;
}
* {
box-sizing: inherit;
}
*:before {
box-sizing: inherit;
}
*:after {
box-sizing: inherit;
}
body {
color: hsla(0, 0%, 0%, 0.8);
font-family: georgia, serif;
font-weight: normal;
word-wrap: break-word;
font-kerning: normal;
-moz-font-feature-settings: "kern", "liga", "clig", "calt";
-ms-font-feature-settings: "kern", "liga", "clig", "calt";
-webkit-font-feature-settings: "kern", "liga", "clig", "calt";
font-feature-settings: "kern", "liga", "clig", "calt";
}
img {
max-width: 100%;
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
h1 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1rem;
color: inherit;
font-family: 'Source Sans Pro', sans-serif!important;
font-weight: 500;
text-rendering: optimizeLegibility;
font-size: 36px;
line-height: 1.1;
}
h2 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1rem;
color: inherit;
font-family: 'Source Sans Pro', sans-serif!important;
font-weight: 500;
text-rendering: optimizeLegibility;
font-size: 1.62671rem;
line-height: 1.1;
}
h3 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1rem;
color: inherit;
font-family: 'Source Sans Pro', sans-serif!important;
font-weight: 500;
text-rendering: optimizeLegibility;
font-size: 1.38316rem;
line-height: 1.1;
}
h4 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1rem;
color: inherit;
font-family:'Source Sans Pro', sans-serif!important;
font-weight: 500;
text-rendering: optimizeLegibility;
font-size: 1rem;
line-height: 1.1;
}
h5 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1rem;
color: inherit;
font-family: 'Source Sans Pro', sans-serif!important;
font-weight: 500;
text-rendering: optimizeLegibility;
font-size: 0.85028rem;
line-height: 1.1;
}
h6 {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
color: inherit;
font-family:'Source Sans Pro', sans-serif!important;
font-weight: bold;
text-rendering: optimizeLegibility;
font-size: 0.78405rem;
line-height: 1.1;
}
hgroup {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
ul {
margin-left: 1.45rem;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
list-style-position: outside;
list-style-image: none;
}
ol {
margin-left: 1.45rem;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
list-style-position: outside;
list-style-image: none;
}
dl {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
dd {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
p {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
figure {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
pre {
margin-left: 0;
margin-right: 0;
margin-top: 0;
margin-bottom: 1.45rem;
font-size: 0.85rem;
line-height: 1.42;
background: hsla(0, 0%, 0%, 0.04);
border-radius: 3px;
overflow: auto;
word-wrap: normal;
padding: 1.45rem;
}
table {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
font-size: x-small;
line-height: 1.45rem;
border-collapse: collapse;
width: 100%;
}
fieldset {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
blockquote {
margin-left: 1.45rem;
margin-right: 1.45rem;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
form {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
noscript {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
iframe {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
hr {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: calc(1.45rem - 1px);
background: hsla(0, 0%, 0%, 0.2);
border: none;
height: 1px;
}
address {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
padding-top: 0;
margin-bottom: 1.45rem;
}
b {
font-weight: bold;
}
strong {
font-weight: bold;
}
dt {
font-weight: bold;
}
th {
font-weight: bold;
}
li {
margin-bottom: calc(1.45rem / 2);
}
ol li {
padding-left: 0;
}
ul li {
padding-left: 0;
}
li > ol {
margin-left: 1.45rem;
margin-bottom: calc(1.45rem / 2);
margin-top: calc(1.45rem / 2);
}
li > ul {
margin-left: 1.45rem;
margin-bottom: calc(1.45rem / 2);
margin-top: calc(1.45rem / 2);
}
blockquote *:last-child {
margin-bottom: 0;
}
li *:last-child {
margin-bottom: 0;
}
p *:last-child {
margin-bottom: 0;
}
li > p {
margin-bottom: calc(1.45rem / 2);
}
code {
font-size: 0.85rem;
line-height: 1.45rem;
}
kbd {
font-size: 0.85rem;
line-height: 1.45rem;
}
samp {
font-size: 0.85rem;
line-height: 1.45rem;
}
abbr {
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
cursor: help;
}
acronym {
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
cursor: help;
}
abbr[title] {
border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
cursor: help;
text-decoration: none;
}
thead {
text-align: left;
}
td,
th {
text-align: left;
border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
font-feature-settings: "tnum";
-moz-font-feature-settings: "tnum";
-ms-font-feature-settings: "tnum";
-webkit-font-feature-settings: "tnum";
padding-left: 0.96667rem;
padding-right: 0.96667rem;
padding-top: 0.725rem;
padding-bottom: calc(0.725rem - 1px);
}
th:first-child,
td:first-child {
padding-left: 0;
}
th:last-child,
td:last-child {
padding-right: 0;
}
tt,
code {
background-color: hsla(0, 0%, 0%, 0.04);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
"Liberation Mono", Menlo, Courier, monospace;
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
}
pre code {
background: none;
line-height: 1.42;
}
code:before,
code:after,
tt:before,
tt:after {
letter-spacing: -0.2em;
content: " ";
}
pre code:before,
pre code:after,
pre tt:before,
pre tt:after {
content: "";
}
@media only screen and (max-width: 480px) {
html {
font-size: 100%;
}
}
.text-right{
text-align: right;
}
.text-left{
text-align: left;
}
.text-center{
text-align: center;
}
.flex{
display: flex;
}
.justify-content-center{
justify-content: center;
}
.justify-item-center{
align-items: center;
}
.max-width{
width: 100%!important;
}
.max-height{
height: 100vh!important;
}
.border-none{
border: none!important ;
}
.hover-shadow:hover{
-webkit-box-shadow: 0px 0px 16px -3px rgba(0, 0, 0, 0.25);
-moz-box-shadow: 0px 0px 16px -3px rgba(0, 0, 0, 0.25);
box-shadow: 0px 0px 16px -3px rgba(0, 0, 0, 0.25);
}
.mt-1{
margin-top: 1em;
}
.mt-2{
margin-top: 2em;
}
.mt-3{
margin-top: 3em;
}
.mb-1{
margin-bottom: 1em;
}
.mb-2{
margin-bottom: 2em;
}
.mb-3{
margin-bottom: 3em;
}
.mr-1{
margin-right: 1em;
}
.mr-2{
margin-right: 2em;
}
.ml-3{
margin-right: 3em;
}
.ml-1{
margin-left: 1em;
}
.ml-2{
margin-left: 2em;
}
.ml-3{
margin-left: 3em;
}
.title-icon{
margin-right: 7px;
color: var(--main-color);
}
.icon{
cursor: pointer;
color: var(--main-color);
}
.btn-animation:active {
box-shadow: 0 5px #666;
transform: translateY(4px);
}
.version-status{
position: absolute;
top: 0;
width: 60%;
left: calc(50% - 30%) ;
display: flex;
align-items: center;
height: 100%;
}
.version-status div{
width: 100%;
color:white;
background-color: #FF4500;
height: 70%;
display: flex;
/* align-items: center; */
justify-content: center;
padding-right: 1em;
padding-left: 1em;
}
.version-status div p{
font-size: 12px;
font-weight: 800;
margin-top: 8px;
}
@media screen and (max-width: 500px) {
.version-status {
width: 80%;
left: calc(42% - 30%) ;
}
}
.breadcrumb{
display:none
}
.background-none{
background: none!important;
}
.display-none{
display: none!important;
}
+43
View File
@@ -0,0 +1,43 @@
/**
* Layout component that queries for data
* with Gatsby's useStaticQuery component
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React from 'react'
import PropTypes from 'prop-types'
// import { useStaticQuery, graphql } from "gatsby"
// import Header from "./header"
import './app-colors.css'
import './layout.css'
const Footer = typeof window !== 'undefined' ? require('./footer').default : null
// import Footer from "./footer"
const Layout = ({ children, bchWallet }) => {
/* const data = useStaticQuery(graphql`
query SiteTitleQuery {
site {
siteMetadata {
title
}
}
}
`) */
return (
<>
<main>{children}</main>
{Footer && <Footer bchWallet={bchWallet} />}{' '}
</>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
bchWallet: PropTypes.object // get minimal-slp-wallet instance
}
export default Layout
+14
View File
@@ -0,0 +1,14 @@
// Local storage handler
const localStorageWalletKey = 'fullstack-wallet-info'
// Detect if the app is running in a browser.
export const isBrowser = () => typeof window !== 'undefined'
export const getWalletInfo = () =>
isBrowser() && window.localStorage.getItem(localStorageWalletKey)
? JSON.parse(window.localStorage.getItem(localStorageWalletKey))
: {}
export const setWalletInfo = wallet =>
window.localStorage.setItem(localStorageWalletKey, JSON.stringify(wallet))
+40
View File
@@ -0,0 +1,40 @@
import React from 'react'
import { Sidebar } from 'adminlte-2-react'
import Wallet from './admin-lte/wallet'
import Tokens from './admin-lte/tokens'
import Configure from './admin-lte/configure'
import SendReceive from './admin-lte/send-receive'
const { Item } = Sidebar
const MenuComponents = props => {
return [
{
key: 'Tokens',
component: <Tokens key='Tokens' {...props} />,
menuItem: (
<Item icon='fas-coins' key='Tokens' text='Tokens' />
)
},
{
key: 'Send/Receive BCH',
component: <SendReceive key='Send/Receive BCH' {...props} />,
menuItem: (
<Item icon='fa-exchange-alt' key='Send/Receive BCH' text='Send/Receive BCH' />
)
},
{
key: 'Wallet',
component: <Wallet key='Wallet' {...props} />,
menuItem: <Item icon='fa-wallet' key='Wallet' text='Wallet' />
},
{
key: 'Configure',
component: <Configure key='Configure' {...props} />,
menuItem: <Item icon='fas-cog' key='Configure' text='Configure' />
}
]
}
export default MenuComponents
+42
View File
@@ -0,0 +1,42 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Content, Button } from 'adminlte-2-react'
import QScanner from './qr-scanner'
let _this
class ScannerModal extends Component {
constructor (props) {
super(props)
_this = this
this.state = {}
this.modalFooter = (
<>
<Button text='Close' pullLeft onClick={this.props.handleOnHide} />
</>
)
}
render () {
return (
<Content
title='Qr Scanner'
modal
modalFooter={this.modalFooter}
show={_this.props.show}
modalCloseButton
onHide={_this.props.handleOnHide}
>
<QScanner onError={_this.props.handleOnError} onScan={_this.props.handleOnScan} />
</Content>
)
}
}
ScannerModal.propTypes = {
show: PropTypes.bool.isRequired,
handleOnHide: PropTypes.func.isRequired,
handleOnError: PropTypes.func,
handleOnScan: PropTypes.func
}
export default ScannerModal
+15
View File
@@ -0,0 +1,15 @@
.QRScanner-container{
text-align: center;
}
.change-button {
margin-bottom: 2em;
margin-top: 2em;
}
.qr-result {
margin-top: 2em;
word-break: break-all;
margin-left: 2em;
margin-right: 2em;
}
+64
View File
@@ -0,0 +1,64 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './qr-scanner.css'
import QrReader from 'react-qr-reader'
let _this
class QScanner extends Component {
constructor (props) {
super(props)
_this = this
this.state = {
result: 'No Result',
facingMode: 'environment'
}
this.handleScan = data => {
if (data) {
this.setState({
result: data
})
_this.props.onScan && _this.props.onScan(data)
}
}
this.handleError = err => {
console.error(err)
_this.props.onError ? _this.props.onError(err) : console.error(err)
}
}
render () {
return (
<div className='QRScanner-container'>
<h4>Facing Mode: {_this.state.facingMode}</h4>
<button className='change-button' onClick={_this.handleChangeMode}>
Change
</button>
<QrReader
delay={300}
onError={this.handleError}
onScan={this.handleScan}
facingMode={_this.state.facingMode}
/>
<b>
<p className='qr-result'>{this.state.result}</p>
</b>
</div>
)
}
handleChangeMode () {
const mode = _this.state.facingMode === 'user' ? 'environment' : 'user'
console.log(`changing to ${mode} mode`)
_this.setState({
facingMode: mode
})
}
}
QScanner.propTypes = {
onError: PropTypes.func,
onScan: PropTypes.func
}
export default QScanner
+32
View File
@@ -0,0 +1,32 @@
/*
This file is intended to be overwritten. It provides a common place to store
site configuration data.
*/
const config = {
title: 'FullStack.cash',
titleShort: 'PSF',
balanceText: 'BCH Balance',
balanceIcon: 'fab-bitcoin',
// The BCH address used in a memo.cash account. Used for tracking the IPFS
// hash of the mirror of this site.
memoAddr: 'bitcoincash:qqwdv3hkmvd5vk0uhwqrqnef54542e5ctvy3ppt0nq',
// Footer Information
hostText: 'Permissionless Software Foundation',
hostUrl: 'https://psfoundation.cash/',
sourceCode:
'https://github.com/Permissionless-Software-Foundation/gatsby-theme-bch-wallet',
torUrl: '2egutot63q765ciwsenlcy5zdyxwxt7olzbldr5dx5i3ixsef2nvrzid.onion',
clearWebUrl: 'https://gatsby-ipfs-web-wallet.fullstack.cash',
// Interface used by minial-slp-wallet
interface: 'consumer-api',
restURL: 'https://free-bch.fullstack.cash'
// restURL: 'http://localhost:5005'
// interface: 'rest-api',
// restURL: 'https://bchn.fullstack.cash/v5/'
}
module.exports = config
+28
View File
@@ -0,0 +1,28 @@
import React from 'react'
class VersionStatus extends React.Component {
constructor (props) {
super(props)
this.state = {
show: false
}
}
render () {
return (
<>
{this.state.show && (
<div className='version-status'>
<div>
<p>
<b>Warning: Open Beta - this app is under active develpment.</b>
</p>
</div>
</div>
)}
</>
)
}
}
export default VersionStatus
+73
View File
@@ -0,0 +1,73 @@
import React from 'react'
import PropTypes from 'prop-types'
// import { withPrefix, Link } from 'gatsby'
// window && typeof window !== 'undefined' && window.test = 'testing'
// import Logo from './images/loader.gif'
export default function HTML (props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet='utf-8' />
<meta httpEquiv='x-ua-compatible' content='ie=edge' />
<meta
name='viewport'
content='width=device-width, initial-scale=1, shrink-to-fit=no'
/>
{/* Loading animation should be loaded very first thing. */}
<div
className='test'
key='loader'
id='___loader'
style={{
alignItems: 'center',
backgroundColor: '#F2F2F2',
display: 'flex',
justifyContent: 'center',
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
zIndex: 9000,
flexDirection: 'column'
}}
>
<img src='https://i.imgur.com/8n8PYAi.gif' alt='' width='250' />
Loading...
</div>
{/* minimal-slp-wallet */}
<script src='https://unpkg.com/minimal-slp-wallet' />
{/* bch-message-lib */}
{/* <script src='https://unpkg.com/bch-message-lib' /> */}
{props.headComponents}
</head>
<body {...props.bodyAttributes}>
{props.preBodyComponents}
<div>
<div
key='body'
id='___gatsby'
dangerouslySetInnerHTML={{ __html: props.body }}
/>
</div>
{props.postBodyComponents}
</body>
</html>
)
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+113
View File
@@ -0,0 +1,113 @@
/*
This library leverages the p-retry and p-queue libraries, to create a
validation queue with automatic retry.
New nodes syncing will attempt to rapidly validate a lot of entries.
A promise-based queue allows this to happen while respecting rate-limits
of the blockchain service provider.
pay-to-write-access-controller.js depends on this library.
*/
import PQueue from 'p-queue'
import pRetry from 'p-retry'
// const pRetry = require('p-retry')
let _this
class RetryQueue {
constructor (localConfig = {}) {
// if (!localConfig.bchjs) {
// throw new Error(
// 'Must pass instance of bch-js when instantiating RetryQueue Class.'
// )
// }
// this.bchjs = localConfig.bchjs
// Encapsulate dependencies
this.validationQueue = new PQueue({ concurrency: 1 })
this.pRetry = pRetry
this.attempts = 5
this.retryPeriod = 5000
_this = this
}
// Add an async function to the queue, and execute it with the input object.
async addToQueue (funcHandle, inputObj) {
try {
console.log('addToQueue inputObj: ', inputObj)
if (!funcHandle) {
throw new Error('function handler is required')
}
if (!inputObj) {
throw new Error('input object is required')
}
const returnVal = await _this.validationQueue.add(() =>
_this.retryWrapper(funcHandle, inputObj)
)
return returnVal
} catch (err) {
console.error('Error in addToQueue()')
throw err
}
}
// Wrap the p-retry library.
// This function returns a promise that will resolve to the output of the
// function 'funcHandle'.
async retryWrapper (funcHandle, inputObj) {
try {
// console.log('retryWrapper inputObj: ', inputObj)
if (!funcHandle) {
throw new Error('function handler is required')
}
if (!inputObj) {
throw new Error('input object is required')
}
console.log('Entering retryWrapper()')
// Add artificial delay to prevent 429 errors.
// await this.sleep(this.retryPeriod)
return this.pRetry(
async () => {
return await funcHandle(inputObj)
},
{
onFailedAttempt: _this.handleValidationError,
retries: this.attempts // Retry 5 times
}
)
} catch (err) {
console.error('Error in retryWrapper()')
throw err
}
}
// Notifies the user that an error occured and that a retry will be attempted.
// It tracks the number of retries until it fails.
async handleValidationError (error) {
try {
const errorMsg = `Attempt ${error.attemptNumber} to validate entry. There are ${error.retriesLeft} retries left. Waiting before trying again.`
console.log(errorMsg)
const SLEEP_TIME = _this.retryPeriod
console.log(`Waiting ${SLEEP_TIME} milliseconds before trying again.\n`)
await _this.sleep(SLEEP_TIME) // 30 sec
} catch (err) {
console.error('Error in handleValidationError()')
throw err
}
}
sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
export default RetryQueue
+12
View File
@@ -0,0 +1,12 @@
import React from 'react'
import Layout from '../components/layout'
const NotFoundPage = () => (
<Layout>
<h1>NOT FOUND</h1>
<p>You just hit a route that doesn&#39;t exist... the sadness.</p>
</Layout>
)
export default NotFoundPage
+48
View File
@@ -0,0 +1,48 @@
import React from 'react'
import { connect } from 'react-redux'
const AdminLTE =
typeof window !== 'undefined'
? require('../components/admin-lte').default
: null
// Maps the props that are going to be sended
// to the component connected with Redux
const mapStateToProps = ({ walletInfo, bchBalance, bchWallet, tokensInfo, currentRate, menuNavigation }) => {
return { walletInfo, bchBalance, bchWallet, tokensInfo, currentRate, menuNavigation }
}
// Send each action of the reducer as props
// to the component connected with Redux
const mapDispatchToProps = dispatch => {
return {
setWalletInfo: value => dispatch({ type: 'SET_WALLET_INFO', value }),
updateBalance: value => dispatch({ type: 'UPDATE_BALANCE', value }),
setBchWallet: value => dispatch({ type: 'SET_BCH_WALLET', value }),
setTokensInfo: value => dispatch({ type: 'SET_TOKENS_INFO', value }),
setMenuNavigation: value => dispatch({ type: 'MENU_NAVIGATION', value })
}
}
// Component connected with redux
const ConnectedDashboard = AdminLTE
? connect(mapStateToProps, mapDispatchToProps)(AdminLTE)
: null
const AdminLTEPage = props => (
<>
{ConnectedDashboard && (
<ConnectedDashboard menuComponents={props.pageContext.menuComponents} />
)}
</>
)
// const AdminLTEPage = props => {
// <>
// {ConnectedDashboard &&
// (<ConnectedDashboard menuComponents={props.pageContext.menuComponents} />)}
// </>
// }
export default AdminLTEPage
+158
View File
@@ -0,0 +1,158 @@
import { createStore as reduxCreateStore } from 'redux'
// Wallet from localStorage
import { getWalletInfo, setWalletInfo } from '../components/localWallet'
// import BchWallet from 'minimal-slp-wallet'
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
const siteConfig = require('../components/site-config')
const reducer = (state, action) => {
// Update walletInfo state property
if (action.type === 'SET_WALLET_INFO') {
const walletInfo = action.value
// persist JWT when create or import wallet
if (state.walletInfo.JWT && !walletInfo.JWT) {
walletInfo.JWT = state.walletInfo.JWT
}
setWalletInfo(walletInfo) // Add wallet to local storage
return Object.assign({}, state, {
walletInfo: walletInfo
})
}
// Update bchBalance state property
if (action.type === 'UPDATE_BALANCE') {
// Convert satoshis to bch
const { myBalance, currentRate } = action.value
console.log(`currentRate: ${currentRate}`)
const satoshis = myBalance
const bch = satoshis / 100000000
const bchBalance = Number(bch.toFixed(8))
const _usdBalance = bchBalance * (currentRate / 100)
const usdBalance = Number(_usdBalance.toFixed(2)) // usd balance
return Object.assign({}, state, {
bchBalance: { bchBalance, usdBalance },
currentRate
})
}
// Adds the minimal-slp-wallet instance to the Redux state
if (action.type === 'SET_BCH_WALLET') {
return Object.assign({}, state, {
bchWallet: action.value
})
}
// Get or update tokens information
if (action.type === 'SET_TOKENS_INFO') {
return Object.assign({}, state, {
tokensInfo: action.value
})
}
// Fuctionality to change between the different sections of the menu
// a "data" property can be added with information
// that can be handled by the other menu components
if (action.type === 'MENU_NAVIGATION') {
return Object.assign({}, state, {
menuNavigation: {
changeTo: action.value.changeTo || state.menuNavigation.changeTo,
data: action.value.data
}
})
}
return state
}
// Wallet info from local storage
const localStorageInfo = getWalletInfo()
// Creates an instance of minimal-slp-wallet, with
// the local storage information if it exists
const instanceWallet = () => {
try {
if (!localStorageInfo.mnemonic) return null
const bchjsOptions = getBchjsOptions()
const bchWalletLib = new BchWallet(localStorageInfo.mnemonic, bchjsOptions)
// Update bchjs instances of minimal-slp-wallet libraries
bchWalletLib.tokens.sendBch.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
bchWalletLib.tokens.utxos.bchjs = new bchWalletLib.BCHJS(bchjsOptions)
return bchWalletLib
} catch (error) {
console.warn(error)
}
}
const getBchjsOptions = () => {
try {
const _interface = localStorageInfo.interface || siteConfig.interface
const jwtToken = localStorageInfo.JWT
const restURL = localStorageInfo.selectedServer
const bchjsOptions = {}
if (_interface === 'consumer-api') {
bchjsOptions.interface = _interface
bchjsOptions.restURL = restURL
return bchjsOptions
}
if (jwtToken) {
bchjsOptions.apiToken = jwtToken
}
if (restURL) {
bchjsOptions.restURL = restURL
}
if (_interface === 'rest-api') {
bchjsOptions.interface = _interface
}
// Assign the tx fee based on environment variable
const FEE = process.env.FEE ? process.env.FEE : 1
bchjsOptions.fee = FEE
console.log(`Using ${bchjsOptions.fee} sats per byte for tx fees.`)
return bchjsOptions
} catch (error) {
console.warn(error)
}
}
// initial state
const initialState = {
walletInfo: localStorageInfo, // Object wallet info
bchBalance: { bchBalance: 0, usdBalance: 0 }, // Wallet Balance
bchWallet: instanceWallet(), // minimal-slp-wallet instance
tokensInfo: [],
currentRate: 0,
menuNavigation: {
changeTo: () => {},
data: null
}
}
let createStore
typeof window !== 'undefined'
? (createStore = () =>
reduxCreateStore(
reducer,
initialState,
/** Redux DevTools
* https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=es
*
*/
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
))
: (createStore = () => reduxCreateStore(reducer, initialState))
export default createStore
+40
View File
@@ -0,0 +1,40 @@
/*
This library It's used to download a more easily maintained list of Back End servers.
*/
const axios = require('axios')
class Gist {
constructor () {
this.axios = axios
}
// Retrieve a JSON file from a GitHub Gist
async getServerList () {
try {
// https://gist.github.com/christroutner/e818ecdaed6c35075bfc0751bf222258
const gistUrl =
'https://api.github.com/gists/63c5513782181f8b8ea3eb89f7cadeb6'
// Retrieve the gist from github.com.
const result = await this.axios.get(gistUrl)
// console.log('result.data: ', result.data)
// Get the current content of the gist.
const content = result.data.files['psf-consumer-apis.json'].content
// console.log('content: ', content)
// Parse the JSON string into an Object.
const object = JSON.parse(content)
// console.log('object: ', object)
return object.consumerApis
} catch (err) {
console.error('Error in getCRList()')
throw err
}
}
}
module.exports = Gist
+84
View File
@@ -0,0 +1,84 @@
/*
This service file contains functions for retrieving an IPFS hash from the
BCH blockchain, in a fasion similar to PS001:
https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps001-media-sharing.md
*/
// These libraries are retrieved in /src/html.js
// minimal-slp-wallet-web
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
// bch-message-lib
const BchMessage = typeof window !== 'undefined' ? window.BchMessage : null
class Memo {
constructor (config) {
console.log('Memo config', config)
// Throw an error if this class is instantiated without passing a BCH address.
if (!config || !config.bchAddr) {
throw new Error('Must pass a BCH address to Memo constructor.')
} else this.bchAddr = config.bchAddr
// Use the bchWallet instance if already exists otherwise
// creates a new one with default values
if (config && config.bchWallet) {
this.wallet = config.bchWallet
this.bchjs = this.wallet.bchjs
// The way bchjs is used on the previous line
// contains the configuration stablished by
// the user ( apiToken, restURL )
// for a new instance we could use the folowing code
// const BCHJS = props.bchWallet.BCHJS
// this.bchjs = new BCHJS()
// debugger
} else if (BchWallet) {
this.wallet = new BchWallet()
// bchjs instance with default values
this.bchjs = this.wallet.bchjs
// debugger
} else {
console.error('Could not access minimal-slp-wallet library.')
}
if (BchMessage) {
this.bchMessage = new BchMessage({ bchjs: this.bchjs })
} else {
console.error('Could not access bch-message-lib library.')
}
}
// Walk the transactions associated with an address until a proper IPFS hash is
// found. If one is not found, will return false.
//
// See this Issue with ABC wallets:
// https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet/issues/136
async findHash () {
try {
console.log(`finding latest IPFS hash for address: ${this.bchAddr}...`)
const txs = await this.bchMessage.memo.memoRead(
this.bchAddr,
'IPFS UPDATE'
)
// console.log(`txs: ${JSON.stringify(txs, null, 2)}`)
// If the array is empty, then return false.
if (txs.length === 0) return false
const hash = txs[0].subject
console.log(`...found this IPFS hash: ${hash}`)
// The transactions should automatically be sorted by the bchMessage
// library. So Just return the subject.
return hash
} catch (err) {
console.warn('Could not find IPFS hash in transaction history.')
return false
}
}
}
export default Memo
+169
View File
@@ -0,0 +1,169 @@
/*
Unit tests for the retry-queue library.
*/
import chai from 'chai'
import sinon from 'sinon'
// const BCHJS = require('@psf/bch-js')
import RetryQueue from '../../src/lib/retry-queue.mjs'
const assert = chai.assert
// const bchjs = new BCHJS()
let uut
let sandbox
describe('#retry-queue.js', () => {
beforeEach(() => {
uut = new RetryQueue()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#_retryWrapper', () => {
it('should throw an error if function handler is not provided', async () => {
try {
await uut.retryWrapper()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'function handler is required')
}
})
it('should throw an error if input object is not provided', async () => {
try {
const funcHandler = () => {}
await uut.retryWrapper(funcHandler)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'input object is required')
}
})
it('should execute the given function.', async () => {
const inputTest = 'test'
// func mock to execute into the retry wrapper
const funcHandle = sinon.spy()
await uut.retryWrapper(funcHandle, inputTest)
assert.equal(inputTest, funcHandle.getCall(0).args[0])
assert.equal(funcHandle.callCount, 1)
})
it('should call handleValidationError() when p-retry error is thrown', async () => {
try {
// Mock for ignore sleep time
sandbox.stub(uut, 'sleep').resolves({})
const inputTest = 'test'
const funcHandle = () => {
throw new Error('test error')
}
uut.attempts = 1
await uut.retryWrapper(funcHandle, inputTest)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should retry the specific number of times before giving up', async () => {
// Mock for ignore sleep time
sandbox.stub(uut, 'sleep').resolves({})
const inputTest = 'test'
const funcHandle = () => {
throw new Error('test error')
}
// func handler
const spy = sinon.spy(funcHandle)
// p-retry attempts
const attempts = 1
try {
uut.attempts = attempts
await uut.retryWrapper(spy, inputTest)
assert.fail('unexpected code path')
} catch (error) {
assert.equal(spy.callCount, attempts + 1)
}
})
})
describe('#addToQueue', () => {
it('should throw an error if function handler is not provided', async () => {
try {
await uut.addToQueue()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'function handler is required')
}
})
it('should throw an error if input object is not provided', async () => {
try {
const funcHandler = () => {}
await uut.addToQueue(funcHandler)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'input object is required')
}
})
it('should add a function and input object to the queue and execute them', async () => {
const inputTest = 'test'
// func mock to execute into the retry wrapper
const funcHandle = sinon.spy()
await uut.addToQueue(funcHandle, inputTest)
assert.equal(inputTest, funcHandle.getCall(0).args[0])
assert.equal(funcHandle.callCount, 1)
})
it('should catch and throw an error', async () => {
try {
// Mock for ignore sleep time
sandbox.stub(uut, 'sleep').resolves({})
const inputTest = 'test'
const funcHandle = () => {
throw new Error('test error')
}
uut.attempts = 1
await uut.retryWrapper(funcHandle, inputTest)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleValidationError', () => {
it('should catch and throw an error', async () => {
try {
await uut.handleValidationError()
assert.fail('unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'Cannot read property')
}
})
})
})
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
import { Provider } from 'react-redux'
import createStore from './src/redux/createStore'
// eslint-disable-next-line
export default ({ element }) => {
// Instantiating store in `wrapRootElement` handler ensures:
// - there is fresh store for each SSR page
// - it will be called only once in browser, when React mounts
const store = createStore()
return <Provider store={store}>{element}</Provider>
}