mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Forked from nft-browser-v2
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
build/
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"srcDir": "",
|
||||
"destDir": "",
|
||||
"files": "**/*.js",
|
||||
"command": "npm run lint"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2022 Chris Troutner
|
||||
|
||||
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,6 @@
|
||||
# nft-browser-v2
|
||||
|
||||
This is a single page app (SPA) based on [react-bootstrap](https://www.npmjs.com/package/react-bootstrap). This app leverages the [Cash Stack](https://cashstack.info) web3 architecture to retrieve NFT information from the Bitcoin Cash (BCH) blockchain and display them.
|
||||
|
||||
## License
|
||||
[MIT](./LICENSE.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Developer Docs
|
||||
|
||||
This file contains notes taken during software development. These notes may eventually be edited into informaiton that goes into the top-level README, or other documentation.
|
||||
|
||||
## Main Features of this App
|
||||
|
||||
- [react-bootstrap](https://react-bootstrap.github.io/) is used for general style and layout control.
|
||||
- An easily customizable waiting modal component can be invoked while waiting for network calls to complete.
|
||||
- [minimal-slp-wallet](https://www.npmjs.com/package/minimal-slp-wallet) is used to access tokens and BCH on the Bitcoin Cash blockchain.
|
||||
- A 'server selection' dropdown allows the user to select from an array of redundent back end servers.
|
||||
- This site is statically compiled, uploaded to Filecoin, and served over IPFS for censorship resistance and version control.
|
||||
Generated
+31749
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "nft-browser-v2",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "0.27.2",
|
||||
"bootstrap": "5.1.3",
|
||||
"query-string": "7.1.1",
|
||||
"react": "17.0.2",
|
||||
"react-bootstrap": "2.0.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"use-query-params": "1.2.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "standard --env mocha --fix"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "17.0.0",
|
||||
"web3.storage": "4.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>React-Bootstrap CodeSandbox Starter</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Run this script with `npm run pub` after running `npm run build`
|
||||
|
||||
This script will upload the compiled site to Filecoin, then publish the new
|
||||
hash to the BCH blockchain. The page at troutsblog.com will automatically
|
||||
redirect users to the new site.
|
||||
*/
|
||||
|
||||
const FILE_PATH = './build'
|
||||
|
||||
const { Web3Storage, getFilesFromPath } = require('web3.storage')
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
// const BchWallet = require('minimal-slp-wallet/index')
|
||||
// const BchMessageLib = require('bch-message-lib/index')
|
||||
const fs = require('fs')
|
||||
|
||||
async function publish () {
|
||||
try {
|
||||
// Get the Filecoin token from the environment variable.
|
||||
const filecoinToken = process.env.FILECOIN_TOKEN
|
||||
if (!filecoinToken) {
|
||||
throw new Error(
|
||||
'Filecoin token not detected. Get a token from https://web3.storage and save it to the FILECOIN_TOKEN environment variable.'
|
||||
)
|
||||
}
|
||||
|
||||
// Get the WIF for updating Trout's blog from the environment variable.
|
||||
// const troutWif = process.env.TROUT_BLOG_WIF
|
||||
// if (!troutWif) {
|
||||
// throw new Error(
|
||||
// 'WIF for troutsblog.com not detect. Add it to the TROUT_BLOG_WIF environment variable.'
|
||||
// )
|
||||
// }
|
||||
|
||||
// Get a list of all the files to be uploaded.
|
||||
const fileAry = await getFileList()
|
||||
// console.log(`fileAry: ${JSON.stringify(fileAry, null, 2)}`)
|
||||
|
||||
// Upload the files to Filecoin.
|
||||
const cid = await uploadToFilecoin(fileAry, filecoinToken)
|
||||
// const cid = 'bafybeibgekzxiqr7irgs26g5pw5sv6xtvfiihccyagpmn7anzl6ffb2xc4'
|
||||
console.log('Content added with CID:', cid)
|
||||
console.log(`https://${cid}.ipfs.dweb.link/`)
|
||||
|
||||
// Initialize libraries for working with BCH blockchain.
|
||||
// const bchjs = new BCHJS()
|
||||
// const wallet = new BchWallet(troutWif, {
|
||||
// interface: 'consumer-api',
|
||||
// })
|
||||
// await wallet.walletInfoPromise
|
||||
// const bchMsg = new BchMessageLib({ wallet })
|
||||
//
|
||||
// // Publish the CID to the BCH blockchain.
|
||||
// const hex = await bchMsg.memo.memoPush(cid, 'IPFS UPDATE')
|
||||
//
|
||||
// // const txid = await bchjs.RawTransactions.sendRawTransaction(hex)
|
||||
// // Broadcast the transaction to the network.
|
||||
// const txid = await wallet.ar.sendTx(hex)
|
||||
// console.log(`BCH blockchain updated with new CID. TXID: ${txid}`)
|
||||
// console.log(`https://blockchair.com/bitcoin-cash/transaction/${txid}`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
publish()
|
||||
|
||||
function getFileList () {
|
||||
const fileAry = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(FILE_PATH, (err, files) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
files.forEach(file => {
|
||||
// console.log(file)
|
||||
fileAry.push(`${FILE_PATH}/${file}`)
|
||||
})
|
||||
|
||||
return resolve(fileAry)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadToFilecoin (fileAry, token) {
|
||||
const storage = new Web3Storage({ token })
|
||||
|
||||
const files = []
|
||||
for (let i = 0; i < fileAry.length; i++) {
|
||||
const thisPath = fileAry[i]
|
||||
// console.log('thisPath: ', thisPath)
|
||||
|
||||
const pathFiles = await getFilesFromPath(thisPath)
|
||||
// console.log('pathFiles: ', pathFiles)
|
||||
|
||||
files.push(...pathFiles)
|
||||
}
|
||||
|
||||
console.log(`Uploading ${files.length} files. Please wait...`)
|
||||
const cid = await storage.put(files)
|
||||
|
||||
return cid
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.header {
|
||||
text-align: center;
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
This is an SPA that displays information about NFTs on the BCH blockchain.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col } from 'react-bootstrap'
|
||||
import { useQueryParam, StringParam } from 'use-query-params'
|
||||
|
||||
// Local libraries
|
||||
import './App.css'
|
||||
import LoadScripts from './components/load-scripts'
|
||||
import NFTs from './components/nfts'
|
||||
import WaitingModal from './components/waiting-modal'
|
||||
import AsyncLoad from './services/async-load'
|
||||
import ServerSelect from './components/servers'
|
||||
import Footer from './components/footer'
|
||||
|
||||
// Token ID for Trout's NFTs
|
||||
const groupTokenId = '030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107'
|
||||
|
||||
// Default restURL for a back-end server.
|
||||
let serverURL = 'https://free-bch.fullstack.cash'
|
||||
|
||||
class App extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
// Encasulate dependencies
|
||||
this.asyncLoad = new AsyncLoad()
|
||||
|
||||
// Working array for storing modal output.
|
||||
this.modalBody = []
|
||||
this.tokenData = {}
|
||||
|
||||
this.state = {
|
||||
walletInitialized: false,
|
||||
wallet: false,
|
||||
modalBody: this.modalBody,
|
||||
hideSpinner: false
|
||||
}
|
||||
|
||||
this.cnt = 0
|
||||
}
|
||||
|
||||
async componentDidMount () {
|
||||
try {
|
||||
this.addToModal('Loading minimal-slp-wallet')
|
||||
|
||||
await this.asyncLoad.loadWalletLib()
|
||||
|
||||
this.addToModal('Initializing wallet')
|
||||
|
||||
const wallet = await this.asyncLoad.initWallet(serverURL)
|
||||
|
||||
this.addToModal('Getting Group Token Information')
|
||||
|
||||
// Get Group Token info
|
||||
const groupData = await this.asyncLoad.getGroupData(groupTokenId)
|
||||
// console.log(`groupData: ${JSON.stringify(groupData, null, 2)}`)
|
||||
|
||||
this.addToModal('Getting NFT Information')
|
||||
|
||||
/// Get NFT child info
|
||||
const nftData = []
|
||||
for (let i = 0; i < groupData.nfts.length; i++) {
|
||||
const tokenData = await this.asyncLoad.getTokenData(groupData.nfts[i])
|
||||
nftData.push(tokenData)
|
||||
}
|
||||
// console.log(`nft data: ${JSON.stringify(nftData, null, 2)}`)
|
||||
|
||||
this.tokenData = {
|
||||
groupData,
|
||||
nftData
|
||||
}
|
||||
|
||||
this.setState({
|
||||
wallet,
|
||||
walletInitialized: true
|
||||
})
|
||||
} catch (err) {
|
||||
this.modalBody = [
|
||||
`Error: ${err.message}`,
|
||||
`Try selecting a different back end server using the drop-down menu at the bottom of the app.`
|
||||
]
|
||||
|
||||
this.setState({
|
||||
modalBody: this.modalBody,
|
||||
hideSpinner: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
// console.log('App component rendered. this.state.wallet: ', this.state.wallet)
|
||||
|
||||
return (
|
||||
<>
|
||||
<GetRestUrl />
|
||||
<LoadScripts />
|
||||
{this.state.walletInitialized ? <InitializedView wallet={this.state.wallet} tokens={this.tokenData} /> : <UninitializedView modalBody={this.state.modalBody} hideSpinner={this.state.hideSpinner}/>}
|
||||
<ServerSelect />
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Add a new line to the waiting modal.
|
||||
addToModal (inStr) {
|
||||
this.modalBody.push(inStr)
|
||||
|
||||
this.setState({
|
||||
modalBody: this.modalBody
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is rendered *before* the BCH wallet is initialized.
|
||||
function UninitializedView (props) {
|
||||
// console.log('UninitializedView props: ', props)
|
||||
|
||||
const heading = 'Loading Blockchain Data...'
|
||||
|
||||
return (
|
||||
<Container style={{ backgroundColor: '#ddd' }}>
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col>
|
||||
<h1 className='header'>NFT Explorer</h1>
|
||||
|
||||
<WaitingModal heading={heading} body={props.modalBody} hideSpinner={props.hideSpinner} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// This is rendered *after* the BCH wallet is initialized.
|
||||
function InitializedView (props) {
|
||||
return (
|
||||
<>
|
||||
<Container style={{ backgroundColor: '#ddd' }}>
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col>
|
||||
<h1 className='header'>NFT Explorer</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<NFTs wallet={props.wallet} tokens={props.tokens} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function GetRestUrl (props) {
|
||||
const [restURL] = useQueryParam('restURL', StringParam)
|
||||
// console.log('restURL: ', restURL)
|
||||
|
||||
serverURL = restURL
|
||||
|
||||
return (<></>)
|
||||
}
|
||||
|
||||
// function sleep (ms) {
|
||||
// return new Promise(resolve => setTimeout(resolve, ms))
|
||||
// }
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div')
|
||||
ReactDOM.render(<App />, div)
|
||||
ReactDOM.unmountComponentAtNode(div)
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
A footer section for the SPA
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col } from 'react-bootstrap'
|
||||
|
||||
const IPFS_CID = 'bafybeiff7sjpdizarbtjgc3ftefyzndggsx3uxqs7l42ruzcwdkbuld6eq'
|
||||
|
||||
class Footer extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<Container style={{ backgroundColor: '#ddd' }}>
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col>
|
||||
<h6>Site Mirrors</h6>
|
||||
<ul>
|
||||
<li><a href='https://troutnfts.com' target='_blank' rel='noreferrer'>troutnfts.com</a></li>
|
||||
<li><a href={`https://${IPFS_CID}.ipfs.dweb.link/`} target='_blank' rel='noreferrer'>Filecoin</a></li>
|
||||
</ul>
|
||||
</Col>
|
||||
|
||||
<Col />
|
||||
|
||||
<Col>
|
||||
<h6>Source Code</h6>
|
||||
<ul>
|
||||
<li><a href='https://github.com/christroutner/nft-browser-v2' target='_blank' rel='noreferrer'>GitHub</a></li>
|
||||
</ul>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Load <script> libraries
|
||||
*/
|
||||
|
||||
import useScript from '../hooks/use-script'
|
||||
|
||||
function LoadScripts () {
|
||||
useScript('https://unpkg.com/minimal-slp-wallet')
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default LoadScripts
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
This is a top level component for displaying the NFTs.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col } from 'react-bootstrap'
|
||||
|
||||
// Local libraries
|
||||
import NFTCard from './nft-card'
|
||||
|
||||
class NFTs extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
console.log('props passed to NFT component: ', props)
|
||||
|
||||
this.groupData = this.props.tokens.groupData
|
||||
this.nftData = this.props.tokens.nftData
|
||||
|
||||
this.state = {
|
||||
nfts: []
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount () {
|
||||
// console.log('NFT component didMount(). nftData: ', this.nftData)
|
||||
|
||||
const nfts = []
|
||||
for (let i = 0; i < this.nftData.length; i++) {
|
||||
nfts.push(<NFTCard key={`nft${i}`} nftData={this.nftData[i]} />)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
nfts
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
// Load spinner at startup while the wallet is being initialized.
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className="text-break" style={{ textAlign: 'center' }}>
|
||||
<p>Loading NFTs associated with Group token{' '}
|
||||
<a
|
||||
href={`https://token.fullstack.cash/?tokenid=${this.groupData.tokenId}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{this.groupData.tokenId}
|
||||
</a>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row><hr /></Row>
|
||||
{this.state.nfts}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NFTs
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
This component controls the display of each NFT.
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import axios from 'axios'
|
||||
import { Container, Row, Col, Image } from 'react-bootstrap'
|
||||
|
||||
|
||||
class NFTCard extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
// console.log('NFTCard props: ', props)
|
||||
|
||||
this.tokenData = props.nftData
|
||||
|
||||
this.state = {
|
||||
mutableData: {},
|
||||
immutableData: {}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
// console.log('Rendering NFT card with this token data: ', this.tokenData)
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<Image className="d-md-none" src={this.tokenData.mutableData.tokenIcon} style={{border: 'black solid 5px', maxWidth: '300px'}} />
|
||||
<Image className="d-none d-md-block" src={this.tokenData.mutableData.tokenIcon} style={{border: 'black solid 5px'}} />
|
||||
</Col>
|
||||
<Col>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<br />
|
||||
<h3>{this.tokenData.genesisData.name} ({this.tokenData.genesisData.ticker})</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{ textAlign: 'left' }}>
|
||||
<Col className="text-break">
|
||||
<b>Token ID:</b> <a href={`https://token.fullstack.cash/?tokenid=${this.tokenData.genesisData.tokenId}`} target="_blank">{this.tokenData.genesisData.tokenId}</a><br />
|
||||
<b>Description: </b> {this.tokenData.mutableData.description}<br />
|
||||
<b>Content: </b>
|
||||
<ul>
|
||||
<li><a href={this.tokenData.mutableData.content.youtube} target="_blank">YouTube</a></li>
|
||||
<li><a href={this.tokenData.mutableData.content.rumble} target="_blank">Rumble</a></li>
|
||||
<li><a href={this.tokenData.mutableData.content.odysee} target="_blank">Odysee</a></li>
|
||||
<li><a href={this.tokenData.mutableData.content.filecoin} target="_blank">Filecoin</a> (download)</li>
|
||||
</ul>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NFTCard
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
This component contains a drop-down form that lets the user select from
|
||||
a range of Global Back End servers.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
// import Select from 'react-dropdown-select'
|
||||
import { Container, Row, Col, Form } from 'react-bootstrap'
|
||||
|
||||
// Local libraries
|
||||
import GistServers from '../../services/gist-servers'
|
||||
|
||||
const defaultOptions = [
|
||||
{ value: 'https://free-bch.fullstack.cash', label: 'https://free-bch.fullstack.cash' },
|
||||
{ value: 'https://bc01-ca-bch-consumer.fullstackcash.nl', label: 'https://bc01-ca-bch-consumer.fullstackcash.nl' },
|
||||
{ value: 'https://pdx01-usa-bch-consumer.fullstackcash.nl', label: 'https://pdx01-usa-bch-consumer.fullstackcash.nl' },
|
||||
{ value: 'https://wa-usa-bch-consumer.fullstackcash.nl', label: 'https://wa-usa-bch-consumer.fullstackcash.nl' }
|
||||
]
|
||||
|
||||
// const defaultOptions = [
|
||||
// 'https://free-bch.fullstack.cash',
|
||||
// 'https://bc01-ca-bch-consumer.fullstackcash.nl',
|
||||
// 'https://pdx01-usa-bch-consumer.fullstackcash.nl',
|
||||
// 'https://wa-usa-bch-consumer.fullstackcash.nl'
|
||||
// ]
|
||||
|
||||
class ServerSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
// this.wallet = props.wallet
|
||||
|
||||
this.state = {
|
||||
options: defaultOptions
|
||||
}
|
||||
}
|
||||
|
||||
// async componentDidMount () {
|
||||
// const servers = await this.getServers()
|
||||
// // console.log('Server list retrieved from GitHub Gist: ', servers)
|
||||
//
|
||||
// this.setState({
|
||||
// options: servers
|
||||
// })
|
||||
// }
|
||||
|
||||
// This is called when the a new drop-down item is selected.
|
||||
selectServer (event) {
|
||||
console.log('event.target.value: ', event.target.value)
|
||||
|
||||
const value = event.target.value
|
||||
|
||||
if (!value) return
|
||||
|
||||
window.location.href = `/?restURL=${value}`
|
||||
}
|
||||
|
||||
// render() {
|
||||
// return (
|
||||
// <div>
|
||||
// <Select options={this.state.options} onChange={(values) => this.selectServer(values)} />
|
||||
// <p style={{textAlign: 'center'}}>Having trouble loading the NFTs? If NFTs don't load after a minute, then
|
||||
// try selecting a different back-end server.</p>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
render () {
|
||||
const items = []
|
||||
for (let i = 0; i < this.state.options.length; i++) {
|
||||
const thisServer = this.state.options[i]
|
||||
|
||||
items.push(<option key={`server-${i}`} value={thisServer.value}>{thisServer.label}</option>)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col>
|
||||
<br />
|
||||
<h5 style={{ textAlign: 'center' }}>
|
||||
Having trouble loading the NFTs? If NFTs don't load after a minute,
|
||||
then try selecting a different back-end server.
|
||||
</h5>
|
||||
<Form.Select onChange={(values) => this.selectServer(values)}>
|
||||
<option>Choose a back-end server</option>
|
||||
{items}
|
||||
</Form.Select>
|
||||
<br />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// Retrieve an array of server URLs from the GitHub Gist.
|
||||
async getServers () {
|
||||
const gistLib = new GistServers()
|
||||
const gistServers = await gistLib.getServerList()
|
||||
|
||||
const serversAry = []
|
||||
|
||||
for (let i = 0; i < gistServers.length; i++) {
|
||||
serversAry.push({ value: gistServers[i].url, label: gistServers[i].url })
|
||||
}
|
||||
|
||||
return serversAry
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerSelect
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
This 'Waiting Modal' component displays a spinner animation and a status log.
|
||||
It's used to inform the user that the app is waiting for something, and to
|
||||
display progress.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Row, Col, Modal, Spinner } from 'react-bootstrap'
|
||||
|
||||
function ModalTemplate (props) {
|
||||
const [show, setShow] = useState(true)
|
||||
|
||||
const handleClose = () => setShow(false)
|
||||
// const handleShow = () => setShow(true)
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.heading}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<BodyList body={props.body} />
|
||||
{props.hideSpinner ? null : <Spinner animation='border' />}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function BodyList (props) {
|
||||
const items = props.body
|
||||
|
||||
const listItems = []
|
||||
|
||||
// List items
|
||||
// for(let i=0; i < items.length; i++) {
|
||||
// listItems.push(<li key={items[i]}>{items[i]}</li>)
|
||||
// }
|
||||
// return (
|
||||
// <ul>
|
||||
// {listItems}
|
||||
// </ul>
|
||||
// )
|
||||
|
||||
// Paragraphs
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
listItems.push(<p key={items[i]}><code>{items[i]}</code></p>)
|
||||
}
|
||||
|
||||
return (
|
||||
listItems
|
||||
)
|
||||
}
|
||||
|
||||
// export default WaitingModal
|
||||
export default ModalTemplate
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const useScript = url => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.src = url
|
||||
script.async = true
|
||||
|
||||
document.body.appendChild(script)
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script)
|
||||
}
|
||||
}, [url])
|
||||
}
|
||||
|
||||
export default useScript
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import { QueryParamProvider } from 'use-query-params'
|
||||
|
||||
// Importing the Bootstrap CSS
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<QueryParamProvider>
|
||||
<App />
|
||||
</QueryParamProvider>
|
||||
</>
|
||||
, document.getElementById('root'))
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
This library gets data that requires an async wait.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import axios from 'axios'
|
||||
|
||||
class AsyncLoad {
|
||||
constructor () {
|
||||
this.BchWallet = false
|
||||
}
|
||||
|
||||
// Load the minimal-slp-wallet which comes in as a <script> file and is
|
||||
// attached to the global 'window' object.
|
||||
async loadWalletLib () {
|
||||
do {
|
||||
if (typeof window !== 'undefined' && window.SlpWallet) {
|
||||
this.BchWallet = window.SlpWallet
|
||||
|
||||
return this.BchWallet
|
||||
} else {
|
||||
console.log('Waiting for wallet library to load...')
|
||||
}
|
||||
|
||||
await sleep(1000)
|
||||
} while (!this.BchWallet)
|
||||
}
|
||||
|
||||
// Initialize the BCH wallet
|
||||
async initWallet (restURL) {
|
||||
const options = {
|
||||
interface: 'consumer-api',
|
||||
restURL,
|
||||
noUpdate: true
|
||||
}
|
||||
|
||||
const wallet = new this.BchWallet(null, options)
|
||||
|
||||
await wallet.walletInfoPromise
|
||||
console.log(`mnemonic: ${wallet.walletInfo.mnemonic}`)
|
||||
|
||||
this.wallet = wallet
|
||||
|
||||
return wallet
|
||||
}
|
||||
|
||||
// Get token data for a given Token ID
|
||||
async getTokenData (tokenId) {
|
||||
const tokenData = await this.wallet.getTokenData(tokenId)
|
||||
|
||||
// Convert the IPFS CIDs into actual data.
|
||||
tokenData.immutableData = await this.getIpfsData(tokenData.immutableData)
|
||||
tokenData.mutableData = await this.getIpfsData(tokenData.mutableData)
|
||||
|
||||
return tokenData
|
||||
}
|
||||
|
||||
// Get data about a Group token
|
||||
async getGroupData (tokenId) {
|
||||
const tokenData = await this.getTokenData(tokenId)
|
||||
|
||||
const groupData = {
|
||||
immutableData: tokenData.immutableData,
|
||||
mutableData: tokenData.mutableData,
|
||||
nfts: tokenData.genesisData.nfts,
|
||||
tokenId: tokenData.genesisData.tokenId
|
||||
}
|
||||
|
||||
return groupData
|
||||
}
|
||||
|
||||
// Given an IPFS URI, this will download and parse the JSON data.
|
||||
async getIpfsData (ipfsUri) {
|
||||
const cid = ipfsUri.slice(7)
|
||||
|
||||
const downloadUrl = `https://${cid}.ipfs.dweb.link/data.json`
|
||||
|
||||
const response = await axios.get(downloadUrl)
|
||||
const data = response.data
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
function sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export default AsyncLoad
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This library downloads a dynamic list of back-end servers from a GitHub Gist.
|
||||
*/
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
class GistServers {
|
||||
constructor () {
|
||||
this.axios = axios
|
||||
}
|
||||
|
||||
// Retrieve a JSON file from a GitHub Gist
|
||||
async getServerList () {
|
||||
try {
|
||||
// https://gist.github.com/christroutner/e818ecdaed6c35075bfc0751bf222258
|
||||
const gistUrl =
|
||||
'https://api.github.com/gists/63c5513782181f8b8ea3eb89f7cadeb6'
|
||||
|
||||
// Retrieve the gist from github.com.
|
||||
const result = await this.axios.get(gistUrl)
|
||||
// console.log('result.data: ', result.data)
|
||||
|
||||
// Get the current content of the gist.
|
||||
const content = result.data.files['psf-consumer-apis.json'].content
|
||||
// console.log('content: ', content)
|
||||
|
||||
// Parse the JSON string into an Object.
|
||||
const object = JSON.parse(content)
|
||||
// console.log('object: ', object)
|
||||
|
||||
return object.consumerApis
|
||||
} catch (err) {
|
||||
console.error('Error in getCRList()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GistServers
|
||||
Reference in New Issue
Block a user