mirror of
https://github.com/fullstack-cash/fullstack-gatsby-theme-bch-wallet.git
synced 2026-09-21 16:52:06 -07:00
Forked from gatsby-ipfs-web-wallet
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "standard",
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 10
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.cache
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
public
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"semi": false
|
||||||
|
}
|
||||||
+33
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Wallet Visual Customization
|
||||||
|
|
||||||
|
[](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')
|
||||||
|
```
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import wrapWithProvider from './wrap-with-provider'
|
||||||
|
export const wrapRootElement = wrapWithProvider
|
||||||
@@ -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
|
||||||
@@ -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')
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import wrapWithProvider from './wrap-with-provider'
|
||||||
|
export const wrapRootElement = wrapWithProvider
|
||||||
Generated
+69504
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 })}>
|
||||||
|
×
|
||||||
|
</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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
*/
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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))
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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 |
@@ -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
|
||||||
@@ -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't exist... the sadness.</p>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default NotFoundPage
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user