mirror of
https://github.com/fullstack-cash/fullstack-gatsby-theme-bch-wallet.git
synced 2026-09-22 01:02: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