first commit

pull/5370/head
Solid Studio 4 years ago committed by Joseph Izang
parent 8643ec9fb8
commit eb7991a36d
  1. 2
      .coveralls.yml
  2. 2
      .gitignore
  3. 119
      .legacy.ts
  4. BIN
      public/favicon.ico
  5. 77
      public/index.html
  6. BIN
      public/logo192.png
  7. BIN
      public/logo512.png
  8. 25
      public/manifest.json
  9. 3
      public/robots.txt
  10. 9
      src/App.css
  11. 84
      src/App.tsx
  12. 17
      src/AppContext.tsx
  13. 37
      src/hooks/useLocalStorage.tsx
  14. 11
      src/index.tsx
  15. 1
      src/react-app-env.d.ts
  16. 36
      src/routes.tsx
  17. 5
      src/setupTests.ts
  18. 11
      src/types.ts
  19. 30
      src/utils/faker.ts
  20. 2
      src/utils/index.ts
  21. 40
      src/utils/publisher.test.ts
  22. 13
      src/utils/publisher.ts
  23. 4272
      src/utils/sample-data/file.json
  24. 4287
      src/utils/sample-data/sample-artifact-with-comments.json
  25. 3771
      src/utils/sample-data/sample-artifact.json
  26. 122
      src/utils/template.ts
  27. 36
      src/utils/types.ts
  28. 93
      src/utils/utils.test.ts
  29. 94
      src/utils/utils.ts
  30. 31
      src/views/ErrorView.tsx
  31. 128
      src/views/HomeView.tsx
  32. 2
      src/views/index.ts
  33. 16
      tslint.json

@ -0,0 +1,2 @@
service_name: circleci
repo_token: $COVERALLS_REPO_TOKEN

2
.gitignore vendored

@ -60,4 +60,4 @@ testem.log
libs/remix-node/ libs/remix-node/
libs/remix-niks/ libs/remix-niks/
apps/remix-react apps/remix-react

@ -0,0 +1,119 @@
import {
CompilationResult,
CompiledContract,
DeveloperDocumentation,
UserDocumentation,
DevMethodDoc,
FunctionDescription,
UserMethodDoc,
ABIParameter
} from '@remixproject/plugin-iframe'
type TemplateDoc<T> = { [key in keyof T]: (...params: any[]) => string }
/** Create documentation for a compilation result */
export function createDoc(result: CompilationResult) {
return Object.keys(result.contracts).reduce((acc, fileName) => {
const contracts = result.contracts[fileName]
Object.keys(contracts).forEach((name) => (acc[name] = getContractDoc(name, contracts[name])))
return acc
}, {})
}
//////////////
// CONTRACT //
//////////////
type ContractDoc = DeveloperDocumentation & UserDocumentation
/** Map of the content to display for a contract */
const contractDocTemplate: TemplateDoc<ContractDoc> = {
author: (author: string) => `> Created By ${author}\n`,
details: (details: string) => `${details}`,
title: (title: string) => `## ${title}`,
notice: (notice: string) => `_${notice}_`,
methods: () => '' // Methods is managed by getMethod()
}
/** Create the documentation for a contract */
function getContractDoc(name: string, contract: CompiledContract) {
const methods = { ...contract.userdoc.methods, ...contract.devdoc.methods }
const contractDoc = { ...contract.userdoc, ...contract.devdoc, methods }
const methodsDoc = contract.abi
.map((def: FunctionDescription) => {
if (def.type === 'constructor') {
def.name = 'constructor'
// because "constructor" is a string and not a { notice } object for userdoc we need to do that
const methodDoc = {
...(contract.devdoc.methods.constructor || {}),
notice: contract.userdoc.methods.constructor as string
}
return getMethodDoc(def, methodDoc)
} else {
if (def.type === 'fallback') def.name = 'fallback'
const method = Object.keys(contractDoc.methods).find((key) => key.includes(def.name))
const methodDoc = contractDoc.methods[method]
return getMethodDoc(def, methodDoc)
}
})
.join('\n')
const doc = Object.keys(contractDoc)
.filter((key) => key !== 'methods')
.map((key) => contractDocTemplate[key](contractDoc[key]))
.join('\n')
return `# ${name}\n${doc}\n${methodsDoc}`
}
////////////
// METHOD //
////////////
type MethodDoc = DevMethodDoc & UserMethodDoc
/** Map of the content to display for a method */
const devMethodDocTemplate: TemplateDoc<MethodDoc> = {
author: (author: string) => `> Created By ${author}\n`,
details: (details: string) => details,
return: (value: string) => `Return : ${value}`,
notice: (notice: string) => notice,
returns: () => '', // Implemented by getParams()
params: () => '' // Implemented by getParams()
}
/** Create a table of param */
const getParams = (params: string[]) =>
params.length === 0
? '_No parameters_'
: `|name |type |description
|-----|-----|-----------
${params.join('\n')}`
/** Get the details of a method */
const getMethodDetails = (devMethod: Partial<MethodDoc>) =>
!devMethod
? '**Add Documentation for the method here**'
: Object.keys(devMethod)
.filter((key) => key !== 'params')
.map((key) => devMethodDocTemplate[key](devMethod[key]))
.join('\n')
function extractParams(params: ABIParameter[], devparams: any) {
return params.map((input) => {
const description = devparams[input.name] || ''
return `|${input.name}|${input.type}|${description}`
})
}
/** Get the doc for a method */
function getMethodDoc(def: FunctionDescription, devdoc?: Partial<MethodDoc>) {
const doc = devdoc || {}
const devparams = doc.params || {}
const params = extractParams(def.inputs || [], devparams)
const returns = extractParams(def.outputs || [], devparams)
return `
## ${def.name} - ${def.constant ? 'view' : 'read'}
${getParams(params)}
${getMethodDetails(devdoc)}
${`Returns:\n${getParams(returns)}`}`
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-146337217-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '');
</script>
<style>
*,
*:after,
*:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body,
html {
margin: 0;
padding: 0;
height: 100%;
}
body {
padding: 0.5em;
}
</style>
<title>Remix Plugin</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

@ -0,0 +1,9 @@
body {
margin: 0;
}
#ethdoc .active {
color: #212529;
background-color: #e9ecef;
border-color: #e9ecef;
}

@ -0,0 +1,84 @@
import React, { useState, useEffect, useRef } from "react"
import {
createIframeClient,
CompilationFileSources,
CompilationResult,
Status
} from "@remixproject/plugin"
import { AppContext } from "./AppContext"
import { Routes } from "./routes"
import { useLocalStorage } from "./hooks/useLocalStorage"
import { createDocumentation } from "./utils/utils"
import "./App.css"
import { ContractName, Documentation } from "./types"
const devMode = { port: 8080 }
export const getNewContractNames = (compilationResult: CompilationResult) => {
const compiledContracts = compilationResult.contracts
let result: string[] = []
for (const file of Object.keys(compiledContracts)) {
const newContractNames = Object.keys(compiledContracts[file])
result = [...result, ...newContractNames]
}
return result
}
const sampleMap = new Map<ContractName, Documentation>()
const App = () => {
const [clientInstance, setClientInstance] = useState(undefined as any)
const [contracts, setContracts] = useState(sampleMap)
const [sites, setSites] = useLocalStorage('sites', [])
const clientInstanceRef = useRef(clientInstance)
clientInstanceRef.current = clientInstance
const contractsRef = useRef(contracts)
contractsRef.current = contracts
useEffect(() => {
console.log("Remix EthDoc loading...")
const client = createIframeClient({ devMode })
const loadClient = async () => {
await client.onload()
setClientInstance(client)
console.log("Remix EthDoc Plugin has been loaded")
client.solidity.on("compilationFinished", (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => {
console.log("New compilation received")
const existingMap = contractsRef.current;
const newContractsMapWithDocumentation = createDocumentation(fileName, data)
const newMap = new Map([...existingMap, ...newContractsMapWithDocumentation])
console.log("New Map", newMap)
const status: Status = { key: 'succeed', type: 'success', title: 'New documentation ready' }
clientInstanceRef.current.emit('statusChanged', status)
setContracts(newMap)
})
}
loadClient()
}, [])
return (
<AppContext.Provider
value={{
clientInstance,
contracts,
setContracts,
sites,
setSites
}}>
<Routes />
</AppContext.Provider>
)
}
export default App

@ -0,0 +1,17 @@
import React from "react"
import { PluginApi, IRemixApi, Api, PluginClient, CompilationResult } from "@remixproject/plugin"
import { ContractName, Documentation, PublishedSite } from "./types"
export const AppContext = React.createContext({
clientInstance: {} as PluginApi<Readonly<IRemixApi>> &
PluginClient<Api, Readonly<IRemixApi>>,
contracts: new Map<ContractName, Documentation>(),
setContracts: (contracts: Map<ContractName, Documentation>) => {
console.log("Calling Set Contract Names")
},
sites: [],
setSites: (sites: PublishedSite[]) => {
console.log("Calling Set Sites")
}
})

@ -0,0 +1,37 @@
import { useState } from "react"
export function useLocalStorage(key: string, initialValue: any) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return initialValue
console.log(error)
return initialValue
}
})
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: any) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(valueToStore)
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error)
}
}
return [storedValue, setValue]
}

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Routes } from './routes'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

@ -0,0 +1 @@
/// <reference types="react-scripts" />

@ -0,0 +1,36 @@
import React from "react"
import {
BrowserRouter as Router,
Switch,
Route,
RouteProps,
} from "react-router-dom"
import { ErrorView, HomeView } from "./views"
interface Props extends RouteProps {
component: any // TODO: new (props: any) => React.Component
from: string
}
const CustomRoute = ({ component: Component, ...rest }: Props) => {
return (
<Route
{...rest}
render={(matchProps) => (
<Component {...matchProps} />
)}
/>
)
}
export const Routes = () => (
<Router>
<Switch>
<CustomRoute exact path="/" component={HomeView} from="/" />
<Route path="/error">
<ErrorView />
</Route>
</Switch>
</Router>
)

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

@ -0,0 +1,11 @@
export type Documentation = string
export interface EthDocumentation {
[contractName: string]: Documentation
}
export type ContractName = string
export type FileName = string
export type PublishedSite = string

@ -0,0 +1,30 @@
import { CompiledContract, ABIParameter } from '@remixproject/plugin'
import sampleData from './sample-data/sample-artifact.json'
import sampleDataWithComments from './sample-data/sample-artifact-with-comments.json'
export const buildFakeArtifact: () => CompiledContract = () => {
const result = sampleData as never as CompiledContract
return result
}
export const buildFakeArtifactWithComments: () => CompiledContract = () => {
const result = sampleDataWithComments as never as CompiledContract
return result
}
export const buildFakeABIParameter: () => ABIParameter = () => {
return {
internalType: "address",
name: "allocator",
type: "address"
}
}
export const buildFakeABIParameterWithDocumentation: () => ABIParameter = () => {
return {
internalType: "address",
name: "allocator",
type: "address"
}
}

@ -0,0 +1,2 @@
export * from './utils'
export * from './publisher'

@ -0,0 +1,40 @@
import { publish } from './publisher';
const open = require('open')
jest.setTimeout(10000)
describe('Publisher tests', () => {
test('it can publish', async () => {
const result = await publish("hello 123")
expect(result).toBeDefined()
})
test('it can publish html', async () => {
const result = await publish(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="description"
content="Web site created using create-react-app"
/>
</head>
<body>
<div>Content custom</div>
</body>
</html>
`)
// Uncomment for testing
// const url = `https://ipfs.io/ipfs/${result}`;
// await open(url, { app: ['google chrome', '--incognito'] });
expect(result).toBeDefined()
})
})

@ -0,0 +1,13 @@
import { HTMLContent } from "./types";
const IpfsClient = require('ipfs-mini')
export const publish = async (content: HTMLContent) => {
const ipfs = new IpfsClient({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' });
const documentHash = await ipfs.add(content)
console.log("Document hash", documentHash)
return documentHash
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,122 @@
import { FunctionDocumentation, TemplateDoc, MethodDoc, ContractDoc, ContractDocumentation, ParameterDocumentation } from "./types"
type HTMLContent = string
export const htmlTemplate = (content: HTMLContent) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="description"
content="Web site created with EthDoc"
/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
${content}
</body>
</html>
`
export const template = (name: string, contractDoc: ContractDocumentation, functions: FunctionDocumentation[]) => `
<style>
#ethdoc-viewer{
font-size: 0.8em;
padding: 1em;
}
#ethdoc-viewer .lead{
font-size: 1em;
}
#ethdoc-viewer table {
width: 50%;
}
#ethdoc-viewer hr {
margin: 0;
margin-bottom: 0.5rem;
}
#ethdoc-viewer p{
margin-bottom: 0.5rem;
}
</style>
<div id="ethdoc-viewer">
${functions.length == 0 ? "No contract to display" : renderHeader(name, contractDoc)}
${functions.map((item) => `
<h6>${item.name} - ${item.type}</h6>
<hr>
${renderParameterDocumentation(item.inputs)}
${getMethodDetails(item.devdoc)}
<p>Returns:</p>
${renderParameterDocumentation(item.outputs)}
`).join('\n')}
</div>
`
// const contractDocTemplate: TemplateDoc<ContractDoc> = {
// author: (author: string) => '',//`Author: ${author}`,
// details: (details: string) => `<p class="lead text-muted">${details}</p>`,
// title: (title: string) => {
// return title ?
// `<small>${title}</small>`
// : ''
// },
// notice: (notice: string) => `<p class="lead text-muted">${notice}</p>`,
// methods: () => '' // Methods is managed by getMethod()
// }
const devMethodDocTemplate: Partial<TemplateDoc<MethodDoc>> = {
author: (author: string) => `<p>Created By ${author}</p>`,
details: (details: string) => `<p>${details}</p>`,
return: (value: string) => `<p>Return : ${value}</p>`,
notice: (notice: string) => `<p>${notice}</p>`,
// returns: () => '', // Implemented by getParams()
params: () => '' // Implemented by getParams()
}
export const renderHeader = (name: string, contractDoc: ContractDocumentation) => `
<h3>${name} ${contractDoc.title ? `<small>: ${contractDoc.title}</small>` : ''}</h3>
${contractDoc.notice ? `<p class="lead">${contractDoc.notice}</p>` : ''}
${contractDoc.author ? `<p>Author: ${contractDoc.author}</p>` : ''}
<p><strong>Functions</strong></p>
`
export const renderParameterDocumentation = (parameters: ParameterDocumentation[]) => `
${parameters.length > 0 ? `
<table class="table table-sm table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${parameters.map((output) => (`<tr>
<td>${output.name}</td>
<td>${output.type}</td>
<td>${output.description}</td>
</tr>`))}
</tbody>
</table>` :
'<p>No parameters</p>'}
`
export const getMethodDetails = (devMethod?: Partial<MethodDoc>) => {
return !devMethod
? '<p><strong>**Add Documentation for the method here**</strong></p>'
: Object.keys(devMethod)
.filter((key) => key !== 'params')
.map((key) => {
(devMethodDocTemplate as any)[key]((devMethod as any)[key])
})
.join('\n')
}

@ -0,0 +1,36 @@
import { UserMethodDoc, DevMethodDoc, DeveloperDocumentation, UserDocumentation } from "@remixproject/plugin";
export interface MethodsDocumentation {
[x: string]: UserMethodDoc | DevMethodDoc
}
export interface ContractDocumentation {
methods: MethodsDocumentation;
author: string;
title: string;
details: string;
notice: string;
}
export type MethodDoc = DevMethodDoc & UserMethodDoc
export type TemplateDoc<T> = { [key in keyof T]: (...params: any[]) => string }
// Contract
export type ContractDoc = DeveloperDocumentation & UserDocumentation
export interface FunctionDocumentation {
name: string
type: string
devdoc?: Partial<MethodDoc>
inputs: ParameterDocumentation[]
outputs: ParameterDocumentation[]
}
export interface ParameterDocumentation {
name: string
type: string
description: string
}
export type HTMLContent = string

@ -0,0 +1,93 @@
const open = require('open')
import { getContractDoc, mergeParametersWithDevdoc, getFunctionDocumentation, getContractDocumentation } from './utils';
import { FunctionDescription } from '@remixproject/plugin';
import { buildFakeArtifactWithComments, buildFakeABIParameter } from './faker'
jest.setTimeout(10000)
describe('Publisher tests', () => {
describe('getContractDocumentation', () => {
test('getContractDocumentation', () => {
const result = getContractDocumentation(buildFakeArtifactWithComments())
const result2 = {
methods:
{
'age(uint256)':
{
author: 'Mary A. Botanist',
details:
'The Alexandr N. Tetearing algorithm could increase precision',
params: [Object],
return: 'age in years, rounded up for partial years'
}
},
notice:
'You can use this contract for only the most basic simulation',
author: 'Larry A. Gardner',
details:
'All function calls are currently implemented without side effects',
title: 'A simulator for trees'
}
expect(result).toBeDefined()
})
})
describe('getContractDoc', () => {
test('getContractDoc', () => {
const template = getContractDoc("Fallout", buildFakeArtifactWithComments());
console.log("Template", template)
expect(template).toBeDefined()
})
})
describe('getFunctionDocumentation', () => {
test('getFunctionDocumentation', () => {
const abiItem: FunctionDescription = {
constant: false,
inputs: [],
name: "Fal1out",
outputs: [],
payable: true,
stateMutability: "payable",
type: "function"
}
const result = getFunctionDocumentation(abiItem, {})
expect(result).toBeDefined()
})
})
describe('mergeParametersWithDevdoc', () => {
test('mergeParametersWithDevdoc', () => {
const abiParameters = [buildFakeABIParameter()]
const devParams = {}
const result = mergeParametersWithDevdoc(abiParameters, devParams)
expect(result.length).toEqual(1)
})
test('mergeParametersWithDevdoc with documentation', () => {
const abiParameters = [buildFakeABIParameter()]
const devParams = {}
const result = mergeParametersWithDevdoc(abiParameters, devParams)
expect(result.length).toEqual(1)
})
})
test.skip('html generation', async () => {
await open('https://ipfs.io/ipfs/QmPYQyWyTrUZt3tjiPsEnkRQxedChYUjgEk9zLQ36SfpyW', { app: ['google chrome', '--incognito'] });
// start server
// generate html
// server it
})
})

@ -0,0 +1,94 @@
import { CompilationResult, CompiledContract, FunctionDescription, ABIDescription, DevMethodDoc, UserMethodDoc, ABIParameter, DeveloperDocumentation, UserDocumentation } from "@remixproject/plugin"
import { EthDocumentation, FileName, Documentation, ContractName } from '../types'
import { template } from './template'
import { ContractDocumentation, MethodDoc, FunctionDocumentation, ParameterDocumentation, MethodsDocumentation } from './types'
export const createDocumentation = (fileName: FileName, compilationResult: CompilationResult) => {
console.log("Filename", fileName)
const result = new Map<ContractName, Documentation>();
const contracts = compilationResult.contracts[fileName]
console.log("Contracts", contracts)
Object.keys(contracts).forEach((name) => {
console.log("CompiledContract", JSON.stringify(contracts[name]))
result.set(name, getContractDoc(name, contracts[name]))
})
return result
}
export const getContractDoc = (name: string, contract: CompiledContract) => {
const contractDoc: ContractDocumentation = getContractDocumentation(contract)
const functionsDocumentation = contract.abi
.map((def: ABIDescription) => {
if (def.type === 'constructor') {
def.name = 'constructor'
// because "constructor" is a string and not a { notice } object for userdoc we need to do that
const methodDoc = {
...(contract.devdoc.methods.constructor || {}),
notice: contract.userdoc.methods.constructor as string
}
return getFunctionDocumentation(def, methodDoc)
} else {
if (def.type === 'fallback') {
def.name = 'fallback'
}
const method = Object.keys(contractDoc.methods).find((key) => key.includes(def.name as string)) as string
const methodDoc = contractDoc.methods[method]
return getFunctionDocumentation(def as FunctionDescription, methodDoc)
}
})
return template(name, contractDoc, functionsDocumentation)
}
export const getContractDocumentation = (contract: CompiledContract) => {
let methods: MethodsDocumentation = {};
Object.keys(contract.userdoc.methods).map((item) => {
if (contract.devdoc.methods[item]) {
const finalResult = {
...contract.userdoc.methods[item],
...contract.devdoc.methods[item]
}
methods[item] = finalResult
} else {
methods[item] = contract.userdoc.methods[item]
}
})
const contractDoc = { ...contract.userdoc, ...contract.devdoc, methods }
return contractDoc
}
export const getFunctionDocumentation = (def: FunctionDescription, devdoc?: Partial<MethodDoc>) => {
const doc = devdoc || {}
const devparams = doc.params || {}
const inputsWithDescription = mergeParametersWithDevdoc(def.inputs || [], devparams)
const outputsWithDescription = mergeParametersWithDevdoc(def.outputs || [], devparams)
const type = def.constant ? 'view' : 'read'
return {
name: def.name,
type,
devdoc: devdoc,
inputs: inputsWithDescription,
outputs: outputsWithDescription
} as FunctionDocumentation
}
export const mergeParametersWithDevdoc = (params: ABIParameter[], devparams: any) => {
return params.map((input) => {
const description = devparams[input.name] || ''
return {
name: input.name,
type: input.type,
description
} as ParameterDocumentation
})
}

@ -0,0 +1,31 @@
import React from "react"
export const ErrorView: React.FC = () => {
return (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<img
style={{ paddingBottom: "2em" }}
width="250"
src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png"
alt="Error page"
></img>
<h5>Sorry, something unexpected happened. </h5>
<h5>
Please raise an issue:{" "}
<a
style={{ color: "red" }}
href=""
>
Here
</a>
</h5>
</div>
)
}

@ -0,0 +1,128 @@
import React, { useState, useEffect } from "react"
import { AppContext } from "../AppContext"
import { ContractName, Documentation } from "../types"
import { publish } from '../utils'
import { htmlTemplate } from "../utils/template"
export const HomeView: React.FC = () => {
const [activeItem, setActiveItem] = useState("")
const [isPublishing, setIsPublishing] = useState(false)
const [htmlDocumentation, setHtmlDocumentation] = useState("")
useEffect(() => {
async function publishDocumentation() {
try {
const hash = await publish(htmlDocumentation)
console.log("Hash", hash)
setIsPublishing(false)
const url = `https://ipfs.io/ipfs/${hash}`;
window.open(url);
} catch (error) {
setIsPublishing(false)
}
}
if (isPublishing) {
publishDocumentation()
}
}, [isPublishing])
const displayDocumentation = (client: any, contractName: ContractName, documentation: Documentation) => {
console.log("Display Documentation", contractName, documentation)
client.emit('documentation-generated', documentation)
}
return (
<AppContext.Consumer>
{({ clientInstance, contracts, setContracts }) =>
<div id="ethdoc">
{[...contracts.keys()].length === 0 &&
<p>Compile a contract with Solidity Compiler</p>
}
{[...contracts.keys()].length > 0 &&
<div>
<div className="list-group">
{[...contracts.keys()].map((item) => {
const documentation = contracts.get(item) as string;
return (
<button key={item}
className={activeItem === item ? 'list-group-item list-group-item-action active' : 'list-group-item list-group-item-action'}
aria-pressed="false"
onClick={() => {
setActiveItem(item)
displayDocumentation(clientInstance, item, documentation)
const documentationAsHtml = htmlTemplate(documentation)
setHtmlDocumentation(documentationAsHtml)
}}>
{item} Documentation
</button>
)
})}
</div>
<div style={{ float: "right" }}>
<button type="button" className="btn btn-sm btn-link" onClick={() => {
setContracts(new Map())
displayDocumentation(clientInstance, "", "")
}}>Clear</button>
</div>
<div style={{ width: "15em" }}>
{activeItem !== "" &&
<PublishButton isPublishing={isPublishing} onClick={() => {
console.log("Is publishing")
setIsPublishing(true);
}} />}
</div>
</div>}
</div>
}
</AppContext.Consumer>
)
}
interface PublishButtonProps {
isPublishing: boolean
onClick: any
}
export const PublishButton: React.FC<PublishButtonProps> = ({ isPublishing, onClick }) => {
return (<button
style={{ marginTop: "1em" }}
className="btn btn-secondary btn-sm btn-block"
disabled={isPublishing}
onClick={onClick}>
{!isPublishing && "Publish"}
{isPublishing && (
<div>
<span
className="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
style={{ marginRight: "0.3em" }}
/>
Publishing...Please wait
</div>
)}
</button>)
}
// <label class="btn btn-secondary active">
// <input type="radio" name="options" id="option1" checked> Active
// </label>
// <label class="btn btn-secondary">
// <input type="radio" name="options" id="option2"> Radio
// </label>
// <label class="btn btn-secondary">
// <input type="radio" name="options" id="option3"> Radio
// </label>

@ -0,0 +1,2 @@
export { HomeView } from "./HomeView"
export { ErrorView } from "./ErrorView"

@ -0,0 +1,16 @@
{
"extends": [
"tslint:recommended",
"tslint-react",
"tslint-config-prettier"
],
"rulesDirectory": [
"tslint-plugin-prettier"
],
"rules": {
"prettier": true,
"interface-name": false,
"no-console": false,
"jsx-no-lambda": false
}
}
Loading…
Cancel
Save