Forked from gatsby-ipfs-web-wallet

This commit is contained in:
Chris Troutner
2021-11-24 16:06:03 -08:00
commit b5289ceb46
70 changed files with 77185 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"
}
}
+69
View File
@@ -0,0 +1,69 @@
# 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
+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
+23
View File
@@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2021 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.
+27
View File
@@ -0,0 +1,27 @@
# gatsby-ipfs-web-wallet
This repository is a Gatsby Theme. It can be accessed via the [gatsby-ipfs-web-wallet npm package](https://www.npmjs.com/package/gatsby-ipfs-web-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`
## License
[MIT](./LICENSE.md)
+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
+65
View File
@@ -0,0 +1,65 @@
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'
}
}
})
// 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
+1
View File
@@ -0,0 +1 @@
// noop
+69504
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
{
"name": "gatsby-ipfs-web-wallet",
"description": "A GatsbyJS Theme with AdminLTE integration and basic Bitcoin Cash wallet functionality.",
"version": "1.9.13",
"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",
"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": {
"@chris.troutner/gatsby-plugin-ipfs": "^2.0.3",
"@chris.troutner/ipfs": "^2.0.2",
"adminlte-2-react": "^0.1.27",
"assert": "^2.0.0",
"browserify-zlib": "^0.2.0",
"copy-to-clipboard": "^3.3.1",
"crypto-browserify": "^3.12.0",
"gatsby": "^4.1.0",
"gatsby-plugin-image": "^2.1.0",
"gatsby-plugin-manifest": "^4.1.0",
"gatsby-plugin-offline": "^4.7.1",
"gatsby-plugin-react-helmet": "^5.1.0",
"gatsby-plugin-sharp": "^4.1.0",
"gatsby-source-filesystem": "^4.1.0",
"gatsby-transformer-sharp": "^4.1.0",
"https-browserify": "^1.0.0",
"ipfs-coord": "^6.7.4",
"jsonrpc-lite": "^2.2.0",
"p-queue": "^7.1.0",
"p-retry": "^5.0.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"prop-types": "^15.7.2",
"qrcode.react": "^1.0.1",
"react": "^17.0.1",
"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.4",
"semver": "^7.3.5",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0"
},
"devDependencies": {
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.2.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-standard": "^5.0.0",
"husky": "^4.3.8",
"mocha": "^9.1.3",
"prettier": "2.2.1",
"semantic-release": "^17.3.7",
"standard": "^16.0.3"
},
"keywords": [
"gatsby",
"gatsby-plugin",
"gatsby-theme"
],
"repository": {
"type": "git",
"url": "https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet"
},
"bugs": {
"url": "https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet/issues"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
}
}
+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
@@ -0,0 +1,404 @@
import React from 'react'
import { Content, Row, Col, Inputs, Box } from 'adminlte-2-react'
import IpfsControl from '../lib/ipfs-control'
import IPFSTabs from './ipfs-tabs'
import CommandRouter from '../lib/commands'
import './ipfs.css'
import RetryQueue from '../../../../lib/retry-queue.mjs'
const queue = new RetryQueue()
const WalletService = require('../lib/wallet-service')
const BchWallet = typeof window !== 'undefined' ? window.SlpWallet : null
const { Checkbox } = Inputs
let _this
class IPFS extends React.Component {
constructor (props) {
super(props)
this.state = {
isStarted: false,
ipfsConnection: false,
appStatusOutput: '',
statusOutput: '',
commandOutput: "Enter 'help' to see available commands.",
commandInput: '',
peers: [],
chatOutputs: {
All: {
output: '',
nickname: ''
}
}
}
_this = this
_this.BchWallet = BchWallet
_this.WalletService = WalletService
this.initIPFSControl()
}
render () {
const { ipfsConnection } = _this.state
return (
<Content>
<Row>
<Col sm={12}>
<Box className='text-center ipfs-checkbox-container'>
<Checkbox
id='ipfs-checkbox'
className='ipfs-checkbox'
value={ipfsConnection} // mark as checked
text='Connect to wallet services over IPFS'
labelPosition='none'
labelXs={0}
name='ipfsConnection'
onChange={this.handleIpfs}
/>
</Box>
</Col>
</Row>
{ipfsConnection && (
<IPFSTabs
ipfsControl={_this.ipfsControl}
handleCommandLog={_this.onCommandLog}
commandOutput={_this.state.commandOutput}
statusOutput={_this.state.statusOutput}
appStatusOutput={_this.state.appStatusOutput}
/>
)}
</Content>
)
}
async handleIpfs () {
const connect = !_this.state.ipfsConnection
const { isStarted } = _this.state
_this.setState(prevState => ({
ipfsConnection: connect
}))
// Save checkbox state into localstorage
const { walletInfo } = _this.props
walletInfo.ipfsService = connect
_this.props.setWalletInfo(walletInfo)
if (!isStarted && connect) {
try {
await _this.ipfsControl.startIpfs()
const nodeInfo = _this.ipfsControl.getNodeInfo()
console.log('nodeInfo', nodeInfo)
_this.setState({
isStarted: true
})
} catch (error) {
console.warn(error.message)
}
}
}
async componentDidMount () {
await _this.getLastIpfsCoordInstance()
}
async componentWillUnmount () {
try {
const data = {
ipfsInfo: {
ipfsIsStarted: true,
savedState: _this.state,
ipfsControl: _this.ipfsControl
}
}
// Save the current state
_this.props.setMenuNavigation({ data })
} catch (error) {
console.warn(error)
}
}
async getLastIpfsCoordInstance () {
try {
const { menuNavigation, walletInfo } = _this.props
const data = menuNavigation.data
// console.log('menuNavigation', menuNavigation)
// console.log('walletInfo', walletInfo)
// console.log('data', data)
// Get local info and
// Verify if checkbox has been marked
// Return for unmarked checkbox
if (!walletInfo.ipfsService) return
_this.setState(prevState => ({
ipfsConnection: true
}))
// Don't start ipfs if it has started already
if (!data || !data.ipfsInfo.ipfsIsStarted) {
await this.ipfsControl.startIpfs()
_this.setState({
isStarted: true
})
}
// Loads the previous information and states
if (data && data.ipfsInfo) {
const { savedState } = data.ipfsInfo
_this.setState(savedState)
}
} catch (error) {
console.warn(error)
}
}
initIPFSControl (bchWallet) {
try {
const ipfsConfig = {
statusLog: _this.onStatusLog,
// handleChatLog: _this.onCommandLog
handleChatLog: _this.incommingChat,
bchWallet: bchWallet || _this.props.bchWallet, // bch wallet instance
privateLog: _this.privLogChat,
pollLog: _this.onAppStatus
}
// Retrieve last ipfs control
const { menuNavigation } = _this.props
if (
menuNavigation &&
menuNavigation.data &&
menuNavigation.data.ipfsInfo.ipfsControl
) {
this.ipfsControl = menuNavigation.data.ipfsInfo.ipfsControl
} else {
// Instantiate a new ipfs control
this.ipfsControl = new IpfsControl(ipfsConfig)
}
this.commandRouter = new CommandRouter({ ipfsControl: this.ipfsControl })
} catch (err) {
console.error(err)
}
}
// Adds a line to the Status terminal
onStatusLog (str) {
console.log('onStatusLog', str)
try {
// Update the Status terminal
_this.setState({
statusOutput: _this.state.statusOutput + ' ' + str + '\n'
})
// If a new peer is found, trigger handleNewPeer()
if (str.includes('New peer found:')) {
const ipfsId = str.substring(24)
_this.handleNewPeer(ipfsId)
}
} catch (error) {
console.warn(error)
}
}
// Handle chat messages coming in from the IPFS network.
incommingChat (str) {
try {
const { chatOutputs, connectedPeer } = _this.state
// console.log(`connectedPeer: ${JSON.stringify(connectedPeer, null, 2)}`)
// console.log(`incommingChat str: ${JSON.stringify(str, null, 2)}`)
const msg = str.data.data.message
const handle = str.data.data.handle
const terminalOut = `${handle}: ${msg}`
if (str.data && str.data.apiName && str.data.apiName.includes('chat')) {
// If the message is marked as 'chat' data, then post it to the public
// chat terminal.
chatOutputs.All.output = chatOutputs.All.output + terminalOut + '\n'
} else {
// Asigns the output to the corresponding peer
chatOutputs[connectedPeer].output =
chatOutputs[connectedPeer].output + terminalOut + '\n'
}
_this.setState({
chatOutputs
})
} catch (err) {
console.warn(err)
// Don't throw an error as this is a top-level handler.
}
}
// Handle decrypted, private messages and send them to the right terminal.
privLogChat (str, from) {
try {
console.log(`privLogChat str: ${str}`)
// console.log(`privLogChat from: ${from}`)
_this.handleRpcDataQueue(str)
const { chatOutputs } = _this.state
const terminalOut = `peer: ${str}`
// Asigns the output to the corresponding peer
chatOutputs[from].output = chatOutputs[from].output + terminalOut + '\n'
_this.setState({
chatOutputs
})
} catch (err) {
console.warn('Error in privLogChat():', err)
}
}
// This function is triggered when a new peer is detected.
handleNewPeer (ipfsId) {
try {
console.log(`New IPFS peer discovered. ID: ${ipfsId}`)
// Use the peer IPFS ID to identify the peers state.
const { peers, chatOutputs } = _this.state
// Add the new peer to the peers array.
peers.push(ipfsId)
// Add a chatOutput entry for the new peer.
const obj = {
output: '',
nickname: ''
}
// chatOutputs[shortIpfsId] = obj
chatOutputs[ipfsId] = obj
_this.setState({
peers,
chatOutputs
})
} catch (err) {
console.warn('Error in handleNewPeer(): ', err)
}
}
// Adds a line to the Command terminal
onCommandLog (msg) {
try {
let commandOutput
if (!msg) {
commandOutput = ''
} else {
commandOutput = _this.state.commandOutput + ' ' + msg + '\n'
}
_this.setState({
commandOutput
})
} catch (error) {
console.warn(error)
}
}
// pollForServices callback
// This method is called from lib/ipfs-control.js. It executes when a BCH
// wallet service is found.
async onAppStatus (msg) {
try {
let output
if (!msg) {
output = ''
} else {
output = _this.state.appStatusOutput + ' ' + msg + '\n'
}
_this.setState({
appStatusOutput: output
})
// Updates the bchWallet instance so it works
// under the ipfs services
await _this.reInitialize()
} catch (error) {
console.warn(error)
// Don't throw an error as this is a top-level handler.
}
}
// Updates the bchWallet instance so it works
// under the ipfs services
async reInitialize () {
try {
_this.onStatusLog('waiting for re-initialize...')
const currentWallet = _this.props.walletInfo
const { mnemonic } = currentWallet
const walletService = new _this.WalletService({
ipfsControl: this.ipfsControl
})
const advancedConfig = {
interface: 'json-rpc',
jsonRpcWalletService: walletService
}
// Initialize the wallet, using auth-rety on failure.
const walletIn = { mnemonic, advancedConfig }
await queue.retryWrapper(_this.initWallet, walletIn)
// Update redux state
_this.props.setBchWallet(_this.bchWalletLib)
_this.onStatusLog('re-initialize success!')
} catch (error) {
_this.onStatusLog('Error in reInitialize()')
_this.onStatusLog(error.message)
// Don't throw an error as this is a top-level handler.
}
}
// Initialize the wallet and retrieve the UTXOs for the wallet.
// This function is called by the queue library, to do automatic retry on
// network failure.
async initWallet (walletIn) {
try {
const { mnemonic, advancedConfig } = walletIn
_this.bchWalletLib = new _this.BchWallet(mnemonic, advancedConfig)
// Wait for wallet to be created.
await _this.bchWalletLib.walletInfoPromise
// If auto UTXO initialization fails, do it manually.
let utxos = _this.bchWalletLib.utxos.utxoStore
if (!utxos) {
utxos = await _this.bchWalletLib.getUtxos()
}
_this.onStatusLog(
`utxo initialization succeeded: ${JSON.stringify(utxos, null, 2)}`
)
return _this.bchWalletLib
} catch (err) {
console.error('Error in initWallet(): ', err)
}
}
// Fill the queue with the incoming data (private messages) from the petitions
// So it will be sweeped by the lib/wallet-service/waitForRPCResponse() function
handleRpcDataQueue (data) {
try {
_this.bchWalletLib.ar.jsonRpcWalletService.rpcHandler(data)
} catch (error) {
console.warn('Error in handleRpcDataQueue', error)
// Don't throw an error as this is a top-level handler.
}
}
sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
IPFS.propTypes = {}
export default IPFS
@@ -0,0 +1,180 @@
import React from 'react'
import { Row, Col, Inputs } from 'adminlte-2-react'
import { Tabs, Tab } from 'react-bootstrap'
import PropTypes from 'prop-types'
import CommandRouter from '../lib/commands'
import 'adminlte-2-react/src/adminlte/css/AdminLTE.css'
const { Text } = Inputs
let _this
class IPFSTabs extends React.Component {
constructor (props) {
super(props)
this.state = {
commandOutput: "Enter 'help' to see available commands."
}
_this = this
// Starts ipfs control if there is a wallet registered already
// console.log('props', props)
if (props && props.ipfsControl) {
this.ipfsControl = props.ipfsControl
this.commandRouter = new CommandRouter({ ipfsControl: this.ipfsControl })
// console.log('this.commandRouter: ', this.commandRouter)
}
}
render () {
const { commandOutput } = _this.state
const { statusOutput, appStatusOutput } = _this.props
return (
<Row>
<Col md={12}>
<Tabs
defaultActiveKey='status'
id='ipfs-coord-tabs'
className='mb-3 nav-tabs-custom'
>
<Tab eventKey='status' title='Status'>
<Text
id='statusLog'
name='statusLog'
inputType='textarea'
labelPosition='none'
rows={20}
readOnly
value={appStatusOutput}
onChange={() => {
// Prevents DOM error
}}
/>
</Tab>
<Tab eventKey='ipfs-coord' title='IPFS Coord'>
<Text
id='ipfsCoordLog'
name='ipfsCoordLog'
inputType='textarea'
labelPosition='none'
rows={20}
readOnly
value={statusOutput}
onChange={() => {
// Prevents DOM error
}}
/>
</Tab>
<Tab eventKey='command' title='Command'>
<Text
id='commandLog'
name='commandLog'
inputType='textarea'
labelPosition='none'
rows={20}
readOnly
value={`${commandOutput ? `${commandOutput}>` : '>'}`}
onChange={() => {
// Prevents DOM error
}}
/>
<Text
id='commandInput'
name='commandInput'
inputType='tex'
labelPosition='none'
value={this.state.commandInput}
onChange={this.handleTextInput}
onKeyDown={_this.handleCommandKeyDown}
/>
</Tab>
</Tabs>
</Col>
</Row>
)
}
// Handles text typed into the input box.
handleTextInput (event) {
event.preventDefault()
const target = event.target
const value = target.value
const name = target.name
// console.log('value: ', value)
_this.setState({
[name]: value
})
}
componentDidUpdate () {
if (_this.state.commandOutput !== _this.props.commandOutput) {
_this.setState({
commandOutput: _this.props.commandOutput
})
}
}
/* START command handling functions */
// Handles when the Enter key is pressed while in the chat input box.
async handleCommandKeyDown (e) {
if (e.key === 'Enter') {
// _this.submitMsg()
// console.log("Enter key");
// Send a chat message to the chat pubsub room.
// const now = new Date();
// const msg = `Message from BROWSER at ${now.toLocaleString()}`
const msg = _this.state.commandInput
// console.log(`Sending this message: ${msg}`);
// _this.handleCommandLog(`me: ${msg}`);
// console.log('_this.commandRouter: ', _this.commandRouter)
const outMsg = await _this.commandRouter.route(msg, _this.ipfsControl)
if (outMsg === 'clear') {
_this.props.handleCommandLog('')
} else {
_this.handleCommandLog(`\n${outMsg}`)
}
// Clear the input text box.
_this.setState({
commandInput: ''
})
}
}
// Adds a line to the terminal
async handleCommandLog (msg) {
try {
// console.log("msg: ", msg);
_this.props.handleCommandLog(msg)
// Add a slight delay, to give the browser time to render the DOM.
await this.sleep(250)
// _this.keepScrolled();
// _this.keepCommandScrolled()
} catch (error) {
console.warn(error)
}
}
sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
IPFSTabs.propTypes = {}
IPFSTabs.propTypes = {
handleCommandLog: PropTypes.func,
commandOutput: PropTypes.string,
statusOutput: PropTypes.string,
appStatusOutput: PropTypes.string
}
export default IPFSTabs
@@ -0,0 +1,14 @@
.ipfs-checkbox-container > div > div > div >input{
height: 1em;
width: 1em;
cursor: pointer;
margin-right: 5px;
}
.ipfs-checkbox-container > div > div > div {
display: flex;
justify-content: center;
align-items: center;
}
+142
View File
@@ -0,0 +1,142 @@
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 apiToken = _this.state.JWT
// Update instance with JWT
if (mnemonic) {
const bchjsOptions = { apiToken: apiToken }
if (selectedServer) {
bchjsOptions.restURL = selectedServer
}
// 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,139 @@
/*
Handles commands from command terminal.
*/
let _this
class CommandRouter {
constructor (cmdConfig) {
if (cmdConfig && cmdConfig.ipfsControl) {
this.ipfsControl = cmdConfig.ipfsControl
}
_this = this
}
// Parse and route a command to the proper handler.
async route (command, appIpfs) {
try {
// console.log(`command: ${command}`)
// Split the command into an array of words separated by a space
const words = command.toString().split(' ')
// console.log(`words: ${JSON.stringify(words, null, 2)}`)
switch (words[0]) {
case 'help':
return this.help()
case 'list':
return await this.list(command, appIpfs)
case 'clear':
return 'clear'
case 'pubsub':
return await this.pubsub(command, appIpfs)
default:
return ''
}
} catch (err) {
console.error('Error in commandRouter()')
throw err
}
}
// Display the help
help () {
const msg = `
Available commands:
- help - this help.
- clear - clear the command terminal.
- list peers - list all known ipfs-coord peers.
- list relays - list all known circuit relays and their state.
- pubsub list - list all subscribed pubsub channels.
`
return msg
}
async list (command, appIpfs) {
const words = command.toString().split(' ')
switch (words[1]) {
case 'relays':
return this.listRelays(_this.ipfsControl)
case 'peers':
return this.listPeers(_this.ipfsControl)
default:
return ''
}
}
async pubsub (command, appIpfs) {
const words = command.toString().split(' ')
switch (words[1]) {
case 'list':
return this.listPubsubChannels(appIpfs)
default:
return ''
}
}
// List known ipfs-coord peers.
async listPeers (appIpfs) {
try {
// console.log('appIpfs: ', appIpfs)
const relays = `Known ipfs-coord peers:\n${JSON.stringify(
appIpfs.ipfsCoord.thisNode.peerData,
null,
2
)}`
return relays
// return "test"
} catch (err) {
console.error('Error in listPeers(): ', err)
return 'Error in listPeers()'
}
}
// List the relays connected to this IPFS node.
async listRelays (appIpfs) {
try {
// console.log('appIpfs: ', appIpfs)
const relays = `Known Circuit Relays:\n${JSON.stringify(
appIpfs.ipfsCoord.thisNode.relayData,
null,
2
)}`
return relays
// return "test"
} catch (err) {
console.error('Error in listRelays(): ', err)
return 'Error in listRelays()'
}
}
async listPubsubChannels (appIpfs) {
try {
// console.log('appIpfs: ', appIpfs)
const channels = await appIpfs.ipfs.pubsub.ls()
const outStr = `Pubsub Subscriptions:\n${JSON.stringify(
channels,
null,
2
)}`
return outStr
} catch (err) {
console.error('Error in listPubsubChannels(): ', err)
return 'Error in listPubsubChannels()'
}
}
}
export default CommandRouter
@@ -0,0 +1,235 @@
/*
This library controls the IPFS interface for the app.
*/
/*
This library contains the logic around the browser-based IPFS full node.
*/
import IPFS from '@chris.troutner/ipfs'
import IpfsCoord from 'ipfs-coord'
const semver = require('semver')
// CHANGE THESE VARIABLES
const CHAT_ROOM_NAME = 'psf-ipfs-chat-001'
const MIN_BCH_WALLET_VERSION = '1.11.0'
const WALLET_PROTOCOL = 'bch-wallet'
// JSON-LD schema used in announcement.
// Customize this data for your own app.
const name = 'Browser Chat ' + Math.floor(Math.random() * 1000)
const announceJsonLd = {
'@context': 'https://schema.org/',
'@type': 'WebAPI',
name: name,
description: 'This is a browser-based IPFS node.',
documentation: '',
provider: {
'@type': 'Organization',
name: 'Permissionless Software Foundation',
url: 'https://PSFoundation.cash'
}
}
let _this
class IpfsControl {
constructor (ipfsConfig) {
this.statusLog = ipfsConfig.statusLog
this.handleChatLog = ipfsConfig.handleChatLog
this.wallet = ipfsConfig.bchWallet
this.privateLog = ipfsConfig.privateLog
this.pollLog = ipfsConfig.pollLog
this.serviceProviders = []
this.selectedServiceProvider = null
this.semver = semver
_this = this
}
// Top level function for controlling the IPFS node. This funciton is called
// by the componentDidMount() function of the page.
async startIpfs () {
try {
console.log('Setting up instance of IPFS...')
this.statusLog('Setting up instance of IPFS...')
// Use DHT routing and ipfs.io delegates.
const ipfsOptions = {
config: {
Bootstrap: [],
Swarm: {
ConnMgr: {
HighWater: 30,
LowWater: 10
},
AddrFilters: []
},
Routing: {
Type: 'dhtclient'
},
preload: {
enabled: false
},
offline: true
},
libp2p: {
config: {
dht: {
enabled: true,
clientMode: true
}
}
}
}
// const ipfsOptions = {
// Bootstrap: [],
// Swarm: {
// ConnMgr: {
// HighWater: 30,
// LowWater: 10
// },
// AddrFilters: []
// }
// }
this.ipfs = await IPFS.create(ipfsOptions)
this.statusLog('IPFS node created.')
// Set a 'low-power' profile for the IPFS node.
await this.ipfs.config.profiles.apply('lowpower')
// Generate a new wallet.
// this.wallet = new BchWallet()
// console.log("this.wallet: ", this.wallet);
if (!this.wallet) {
throw new Error('Wallet Not Found.! . Create or import a wallet')
}
// Wait for the wallet to initialize.
await this.wallet.walletInfoPromise
// Instantiate the IPFS Coordination library.
this.ipfsCoord = new IpfsCoord({
ipfs: this.ipfs,
type: 'browser',
statusLog: this.statusLog, // Status log
bchjs: this.wallet.bchjs,
mnemonic: this.wallet.walletInfo.mnemonic,
privateLog: this.privateLog,
announceJsonLd
})
this.statusLog('ipfs-coord library instantiated.')
// Wait for the coordination stuff to be setup.
await this.ipfsCoord.start()
const nodeConfig = await this.ipfs.config.getAll()
console.log(
`IPFS node configuration: ${JSON.stringify(nodeConfig, null, 2)}`
)
// subscribe to the 'chat' chatroom.
await this.ipfsCoord.adapters.pubsub.subscribeToPubsubChannel(
CHAT_ROOM_NAME,
this.handleChatLog,
this.ipfsCoord.thisNode
)
// Pass the IPFS instance to the window object. Makes it easy to debug IPFS
// issues in the browser console.
if (typeof window !== 'undefined') window.ipfs = this.ipfs
// Get this nodes IPFS ID
const id = await this.ipfs.id()
this.ipfsId = id.id
this.statusLog(`This IPFS node ID: ${this.ipfsId}`)
console.log('IPFS node setup complete.')
this.statusLog('IPFS node setup complete.')
_this.statusLog(' ')
setInterval(this.pollForServices, 10000)
} catch (err) {
console.error('Error in startIpfs(): ', err)
this.statusLog(
'Error trying to initialize IPFS node! Have you created a wallet?'
)
}
}
// This funciton handles incoming chat messages.
handleChatMsg (msg) {
try {
console.log('handleChatMsg msg: ', msg)
} catch (err) {
console.error('Error in handleChatMsg(): ', err)
}
}
getNodeInfo () {
return {
ipfsId: this.ipfsId,
announceJsonLd
}
}
// Poll the ipfs-coord coordination channel for available service providers.
pollForServices () {
try {
// An array of IPFS IDs of other nodes in the coordination pubsub channel.
const peers = _this.ipfsCoord.thisNode.peerList
// console.log(`peers: ${JSON.stringify(peers, null, 2)}`)
// Array of objects. Each object is the IPFS ID of the peer and contains
// data about that peer.
const peerData = _this.ipfsCoord.thisNode.peerData
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
for (let i = 0; i < peers.length; i++) {
const thisPeer = peers[i]
const thisData = peerData.filter(x => x.from === thisPeer)
const thisPeerData = thisData[0]
// Create a 'fingerprint' that defines the wallet service.
const protocol = thisPeerData.data.jsonLd.protocol
const version = thisPeerData.data.jsonLd.version
// console.log(
// `debug: peer ${thisPeer} uses protocol: ${protocol} v${version}`,
// )
let versionMatches = false
if (version) {
versionMatches = _this.semver.gt(version, MIN_BCH_WALLET_VERSION)
}
// Ignore any peers that don't match the fingerprint for a BCH wallet
// service.
if (protocol && protocol.includes(WALLET_PROTOCOL) && versionMatches) {
// console.log('Matching peer: ', thisPeerData)
// Temporary business logic.
// Use the first available wallet service detected.
if (_this.serviceProviders.length === 0) {
_this.selectedServiceProvider = thisPeer
// Persist the config setting, so it can be used by other commands.
// _this.conf.set('selectedService', thisPeer)
const pollLog = `---->BCH wallet service selected: ${thisPeer}`
console.log(pollLog)
_this.pollLog(pollLog)
}
// Add the peer to the list of serviceProviders.
_this.serviceProviders.push(thisPeer)
}
}
} catch (err) {
console.error('Error in pollForServices(): ', err)
// Do not throw error. This is a top-level function.
}
}
}
// module.exports = AppIpfs
export default IpfsControl
@@ -0,0 +1,225 @@
/*
This library interacts with the ipfs-bch-wallet-service via the JSON RPC
over IPFS.
*/
const { v4: uid } = require('uuid')
const jsonrpc = require('jsonrpc-lite')
// Public npm libraries.
const axios = require('axios')
// const Conf = require('conf')
class WalletService {
constructor (localConfig = {}) {
// Encapsulate dependencies
this.axios = axios
// this.conf = new Conf()
// this.ipfsControl = localConfig.ipfsControl
this.uid = uid
this.jsonrpc = jsonrpc
this.ipfsControl = localConfig.ipfsControl
// A queue for holding RPC data that has arrived.
this.rpcDataQueue = []
}
// This handler is triggered when RPC data comes in over IPFS.
// Handle RPC input, and match the input to the RPC queue.
// NOTE: This function is called when a private message
// is sent to this node
rpcHandler (data) {
try {
// Convert string input into an object.
const jsonData = JSON.parse(data)
// console.log(
// 'rest-api.js/rpcHandler() data: ',
// JSON.stringify(jsonData, null, 2),
// )
console.log(`JSON RPC response for ID ${jsonData.id} received.`)
this.rpcDataQueue.push(jsonData)
} catch (err) {
console.error('Error in rest-api.js/rpcHandler(): ', err)
// Do not throw error. This is a top-level function.
}
}
checkServiceId () {
try {
// this.conf = new Conf()
const serviceId = this.ipfsControl.selectedServiceProvider
console.log(`serviceId : ${serviceId}`)
if (!serviceId) {
throw new Error('Wallet service ID does not exist')
}
return serviceId
} catch (error) {
console.error('Error in checkServiceId()')
throw error
}
}
// Get up to 20 addresses.
async getBalances (addrs) {
try {
// Input validation.
if (!addrs || !Array.isArray(addrs)) {
throw new Error(
'addrs input to getBalance() must be an array, of up to 20 addresses.'
)
}
const serviceId = this.checkServiceId()
// console.log(`serviceId: ${serviceId}`)
const rpcId = this.uid()
const rpcData = {
endpoint: 'balance',
addresses: addrs
}
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'bch', rpcData)
const cmdStr = JSON.stringify(cmd)
const thisNode = this.ipfsControl.ipfsCoord.thisNode
await this.ipfsControl.ipfsCoord.useCases.peer.sendPrivateMessage(
serviceId,
cmdStr,
thisNode
)
const data = await this.waitForRPCResponse(rpcId)
return data
} catch (err) {
console.error('Error in getBalance()')
throw err
}
}
// Get hydrated UTXOs for an address
async getUtxos (addr) {
try {
// Input validation
if (!addr || typeof addr !== 'string') {
throw new Error('getUtxos() input address must be a string.')
}
const serviceId = this.checkServiceId()
// console.log(`serviceId: ${serviceId}`)
const rpcId = this.uid()
const rpcData = {
endpoint: 'utxos',
address: addr
}
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'bch', rpcData)
const cmdStr = JSON.stringify(cmd)
const thisNode = this.ipfsControl.ipfsCoord.thisNode
console.log('cmdStr', cmdStr)
await this.ipfsControl.ipfsCoord.useCases.peer.sendPrivateMessage(
serviceId,
cmdStr,
thisNode
)
// Wait for data to come back from the wallet service.
const data = await this.waitForRPCResponse(rpcId)
return data
} catch (err) {
console.error('Error in getUtxos()', err)
throw err
}
}
// Broadcast a transaction to the network.
async sendTx (hex) {
try {
// Input validation
if (!hex || typeof hex !== 'string') {
throw new Error('sendTx() input hex must be a string.')
}
const serviceId = this.checkServiceId()
// console.log(`serviceId: ${serviceId}`)
const rpcId = this.uid()
const rpcData = {
endpoint: 'broadcast',
hex
}
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'bch', rpcData)
const cmdStr = JSON.stringify(cmd)
console.log('cmdStr', cmdStr)
const thisNode = this.ipfsControl.ipfsCoord.thisNode
await this.ipfsControl.ipfsCoord.useCases.peer.sendPrivateMessage(
serviceId,
cmdStr,
thisNode
)
const data = await this.waitForRPCResponse(rpcId)
return data
} catch (err) {
console.error('Error in sendTx()')
throw err
}
}
// Returns a promise that resolves to data when the RPC response is recieved.
async waitForRPCResponse (rpcId) {
try {
// Initialize variables for tracking the return data.
let dataFound = false
let cnt = 0
let data = {
success: false,
message: 'request timed out',
data: ''
}
// Loop that waits for a response from the service provider.
do {
for (let i = 0; i < this.rpcDataQueue.length; i++) {
const rawData = this.rpcDataQueue[i]
// console.log(`rawData: ${JSON.stringify(rawData, null, 2)}`)
if (rawData.id === rpcId) {
dataFound = true
// console.log('data was found in the queue')
data = rawData.result.value
// Remove the data from the queue
this.rpcDataQueue.splice(i, 1)
break
}
}
// Wait between loops.
// await this.sleep(1000)
await this.ipfsControl.wallet.bchjs.Util.sleep(4500)
cnt++
// Exit if data was returned, or the window for a response expires.
} while (!dataFound && cnt < 10)
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
console.log('waitForRPCResponse', data)
return data
} catch (err) {
console.error('Error in waitForRPCResponse()')
throw err
}
}
}
module.exports = WalletService
@@ -0,0 +1,16 @@
import React from 'react'
import Ipfs from './ipfs-tab'
const MenuComponents = props => {
return [
{
key: 'IPFS',
icon: 'fas-message',
component: (
<>
<Ipfs {...props} />
</>
)
}
]
}
export default MenuComponents
@@ -0,0 +1,335 @@
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, 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
}
_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'
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 () {
try {
const walletInfo = _this.props.walletInfo
const selectOptions = []
// populate select field with data from localstorage
for (let i = 0; i < walletInfo.servers.length; i++) {
const option = {
value: walletInfo.servers[i],
text: walletInfo.servers[i]
}
selectOptions.push(option)
}
_this.setState({
selectOptions,
selectedServer: walletInfo.selectedServer
})
} catch (error) {
console.warn(error)
}
}
componentDidMount () {
_this.populateSelect()
}
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)
}
// Update the wallet instance state
updateWalletInstance () {
try {
const { mnemonic, JWT } = _this.props.walletInfo
const apiToken = JWT
const restURL = _this.state.selectedServer
// Update instance with the selected url
if (mnemonic) {
const bchjsOptions = {}
if (apiToken) {
bchjsOptions.apiToken = apiToken
}
bchjsOptions.restURL = restURL
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 bchjs = bchWalletLib.bchjs
let currentRate
if (bchjs.restURL.includes('abc.fullstack')) {
currentRate = (await bchjs.Price.getBchaUsd()) * 100
} else {
// BCHN price.
currentRate = (await bchjs.Price.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
})
}
}
// 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
+465
View File
@@ -0,0 +1,465 @@
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
let currentRate
if (bchjs.restURL.includes('abc.fullstack')) {
currentRate = (await bchjs.Price.getBchaUsd()) * 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://bchn.fullstack.cash/v5/'
const server2 = 'https://abc.fullstack.cash/v5/'
const servers = [server1, server2]
// Default server is BCHN.
let 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,513 @@
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 bchjs = bchWalletLib.bchjs
let currentRate
if (bchjs.restURL.includes('abc.fullstack')) {
currentRate = (await bchjs.Price.getBchaUsd()) * 100
} else {
// BCHN price.
currentRate = (await bchjs.Price.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 bchjs = bchWalletLib.bchjs
let currentRate
if (bchjs.restURL.includes('abc.fullstack')) {
currentRate = (await bchjs.Price.getBchaUsd()) * 100
} else {
// BCHN price.
currentRate = (await bchjs.Price.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
+274
View File
@@ -0,0 +1,274 @@
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'
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()
}
_this.setState({
tokens,
inFetch: false
})
if (!tokens.length) {
throw new Error('No tokens found on this wallet.')
}
_this.props.setTokensInfo(tokens)
} 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://explorer.bitcoin.com/bch/tx'
}
_this.setState({
explorerURL
})
}
}
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,386 @@
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 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)
_this.setState({
txId: result,
inFetch: false
})
_this.props.setTxId(result) /* Set the transaction ID in Tokens state */
_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,83 @@
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
// 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 '>
<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
+107
View File
@@ -0,0 +1,107 @@
#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;
}
+173
View File
@@ -0,0 +1,173 @@
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 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 apiToken = currentWallet.JWT
const restURL = currentWallet.selectedServer
const bchjsOptions = {}
if (apiToken || restURL) {
if (apiToken) {
bchjsOptions.apiToken = apiToken
}
if (restURL) {
bchjsOptions.restURL = restURL
}
}
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'
Object.assign(currentWallet, walletInfo)
const myBalance = await bchWalletLib.getBalance()
const bchjs = bchWalletLib.bchjs
let currentRate
if (bchjs.restURL.includes('abc.fullstack')) {
currentRate = await bchjs.Price.getBchaUsd() * 100
} else {
// BCHN price.
currentRate = (await bchjs.Price.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)
}
}
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
+256
View File
@@ -0,0 +1,256 @@
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 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 apiToken = currentWallet.JWT
const restURL = currentWallet.selectedServer
const bchjsOptions = {}
if (apiToken || restURL) {
if (apiToken) {
bchjsOptions.apiToken = apiToken
}
if (restURL) {
bchjsOptions.restURL = restURL
}
}
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'
Object.assign(currentWallet, walletInfo)
const myBalance = await bchWalletLib.getBalance()
const bchjs = bchWalletLib.bchjs
let currentRate
if (bchjs.restURL.includes('abc.fullstack')) {
currentRate = (await bchjs.Price.getBchaUsd()) * 100
} else {
// BCHN price.
currentRate = (await bchjs.Price.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)
}
}
// 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}
/>
*/
+62
View File
@@ -0,0 +1,62 @@
/* 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'
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}
/>
<ImportWallet
updateBalance={_this.props.updateBalance}
setWalletInfo={_this.props.setWalletInfo}
setBchWallet={_this.props.setBchWallet}
walletInfo={_this.props.walletInfo}
/>
</>
)}
{this.props.importComponents}
</Content>
)
}
}
Wallet.propTypes = {
setWalletInfo: PropTypes.func.isRequired,
walletInfo: PropTypes.object.isRequired,
updateBalance: PropTypes.func.isRequired,
setBchWallet: PropTypes.func.isRequired
}
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;
}
}
+145
View File
@@ -0,0 +1,145 @@
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()
}
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={5} 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>
<Col md={7} className='footer-section'>
<div className='pull-right'>
<p className='section-tittle'>Ways to access this web-app</p>
<ul>
<li id='web'>
<span>
<b>Web</b>
</span>
<b className='bar-space'>|</b>
<a href={siteConfig.clearWebUrl}>{siteConfig.clearWebUrl}</a>
</li>
<li id='tor'>
<span>
<b>Tor</b>
</span>
<b className='bar-space'>|</b>
<a href={`http://${siteConfig.torUrl}`}>
{siteConfig.torUrl}
</a>
</li>
<li id='ipfs'>
<span>
<b>IPFS</b>
</span>
<b className='bar-space'>|</b>
<a href={_this.state.ipfsHashLink}>{this.state.ipfsHash}</a>
</li>
<li id='memo'>
<span>
<b>Memo</b>
</span>
<b className='bar-space'>|</b>
<a
href={`https://memo.cash/profile/${siteConfig.memoAddr}`}
target='_blank'
rel='noopener noreferrer'
>
{siteConfig.memoAddr}
</a>
</li>
</ul>
</div>
</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
+24
View File
@@ -0,0 +1,24 @@
/*
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: 'FullStack.cash',
hostUrl: 'https://fullstack.cash/',
sourceCode: 'https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet',
torUrl: '2egutot63q765ciwsenlcy5zdyxwxt7olzbldr5dx5i3ixsef2nvrzid.onion',
clearWebUrl: 'https://gatsby-ipfs-web-wallet.fullstack.cash'
}
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
+72
View File
@@ -0,0 +1,72 @@
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
+133
View File
@@ -0,0 +1,133 @@
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 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 jwtToken = localStorageInfo.JWT
const restURL = localStorageInfo.selectedServer
const bchjsOptions = {}
if (jwtToken) {
bchjsOptions.apiToken = jwtToken
}
if (restURL) {
bchjsOptions.restURL = restURL
}
// 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.`)
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)
}
}
// 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
+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>
}