Skip to content

Commit

Permalink
Merge pull request #2491 from ethereum/ipfs
Browse files Browse the repository at this point in the history
ipfs settings
  • Loading branch information
yann300 authored Jun 6, 2022
2 parents d12b690 + 618432d commit f451219
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 33 deletions.
7 changes: 6 additions & 1 deletion apps/remix-ide-e2e/src/tests/publishContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ module.exports = {
.openFile('contracts/3_Ballot.sol')
.verifyContracts(['Ballot'])
.click('#publishOnIpfs')
.pause(2000)
.waitForElementVisible('[data-id="publishToStorageModalDialogModalBody-react"]', 60000)
.click('[data-id="publishToStorage-modal-footer-ok-react"]')
.pause(8000)
.waitForElementVisible('[data-id="publishToStorageModalDialogModalBody-react"]', 60000)
.getText('[data-id="publishToStorageModalDialogModalBody-react"]', (result) => {
Expand Down Expand Up @@ -63,7 +66,9 @@ module.exports = {
.waitForElementVisible('*[data-id="Deploy - transact (not payable)"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.pause(5000)
.waitForElementVisible('[data-id="udappModalDialogModalBody-react"]')
.waitForElementVisible('[data-id="udappModalDialogModalBody-react"]', 60000)
.modalFooterOKClick('udapp')
.pause(8000)
.getText('[data-id="udappModalDialogModalBody-react"]', (result) => {
const value = typeof result.value === 'string' ? result.value : null

Expand Down
67 changes: 50 additions & 17 deletions libs/remix-ui/publish-to-storage/src/lib/publish-to-storage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { publishToSwarm } from './publishOnSwarm'

export const PublishToStorage = (props: RemixUiPublishToStorageProps) => {
const { api, storage, contract, resetStorage } = props
const [modalShown, setModalShown] = useState(false)
const [state, setState] = useState({
modal: {
title: '',
Expand All @@ -22,7 +23,7 @@ export const PublishToStorage = (props: RemixUiPublishToStorageProps) => {
const storageService = async () => {
if ((contract.metadata === undefined || contract.metadata.length === 0)) {
modal('Publish To Storage', 'This contract may be abstract, it may not implement an abstract parent\'s methods completely or it may not invoke an inherited contract\'s constructor correctly.')

} else {
if (storage === 'swarm') {
try {
Expand All @@ -38,12 +39,32 @@ export const PublishToStorage = (props: RemixUiPublishToStorageProps) => {
modal('Swarm Publish Failed', publishMessageFailed(storage, parseError))
}
} else {
try {
const result = await publishToIPFS(contract, api)

modal(`Published ${contract.name}'s Metadata and Sources`, publishMessage(result.uploaded))
} catch (err) {
modal('IPFS Publish Failed', publishMessageFailed(storage, err))
if (!api.config.get('settings/ipfs-url') && !modalShown) {
modal('IPFS Settings', <div>You have not set your own custom IPFS settings.<br></br>
<br></br>
We won’t be providing a public endpoint anymore for publishing your contracts to IPFS.<br></br>Instead of that, 4 options are now available:<br></br>
<br></br>
<ul className='pl-3'>
<li>
DEFAULT OPTION:
Use the public INFURA node. This will not guarantee your data will persist.
</li>
<li>
Use your own INFURA IPFS node. This requires a subscription. <a href='https://infura.io/product/ipfs' target={'_blank'}>Learn more</a>
</li>
<li>
Use any external IPFS which doesn’t require any authentification.
</li>
<li>
Use your own local ipfs node (which usually runs under http://localhost:5001)
</li>
</ul>
You can update your IPFS settings in the SETTINGS tab.
<br></br>
Now the default option will be used.
</div>, async () => await ipfs(contract, api))
} else {
await ipfs(contract, api)
}
}
}
Expand All @@ -54,18 +75,29 @@ export const PublishToStorage = (props: RemixUiPublishToStorageProps) => {
}
}, [storage])


const ipfs = async (contract, api) => {
try {
const result = await publishToIPFS(contract, api)
modal(`Published ${contract.name}'s Metadata and Sources`, publishMessage(result.uploaded))
} catch (err) {
modal('IPFS Publish Failed', publishMessageFailed(storage, err))
}
setModalShown(true)
}

const publishMessage = (uploaded) => (
<span> Metadata and sources of "{contract.name.toLowerCase()}" were published successfully. <br />
<pre>
<div>
{ uploaded.map((value, index) => <div key={index}><b>{ value.filename }</b> : <pre>{ value.output.url }</pre></div>) }
{uploaded.map((value, index) => <div key={index}><b>{value.filename}</b> : <pre>{value.output.url}</pre></div>)}
</div>
</pre>
</span>
)

const publishMessageFailed = (storage, err) => (
<span>Failed to publish metadata file and sources to { storage }, please check the { storage } gateways is available. <br />
<span>Failed to publish metadata file and sources to {storage}, please check the {storage} gateways is available. <br />
{err}
</span>
)
Expand All @@ -77,15 +109,16 @@ export const PublishToStorage = (props: RemixUiPublishToStorageProps) => {
resetStorage()
}

const modal = async (title: string, message: string | JSX.Element) => {
const modal = async (title: string, message: string | JSX.Element, okFn: any = () => { }) => {
await setState(prevState => {
return {
...prevState,
modal: {
...prevState.modal,
hide: false,
message,
title
title,
okFn
}
}
})
Expand All @@ -94,13 +127,13 @@ export const PublishToStorage = (props: RemixUiPublishToStorageProps) => {
return (
<ModalDialog
id={props.id || 'publishToStorage'}
title={ state.modal.title }
message={ state.modal.message }
hide={ state.modal.hide }
title={state.modal.title}
message={state.modal.message}
hide={state.modal.hide}
okLabel='OK'
okFn={() => {}}
handleHide={ handleHideModal }>
{ (typeof state.modal.message !== 'string') && state.modal.message }
okFn={state.modal.okFn}
handleHide={handleHideModal}>
{(typeof state.modal.message !== 'string') && state.modal.message}
</ModalDialog>
)
}
Expand Down
39 changes: 26 additions & 13 deletions libs/remix-ui/publish-to-storage/src/lib/publishToIPFS.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import IpfsClient from 'ipfs-mini'
import IpfsHttpClient from 'ipfs-http-client'

const ipfsNodes = [
new IpfsClient({ host: 'ipfs.remixproject.org', port: 443, protocol: 'https' }),
new IpfsClient({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' }),
new IpfsClient({ host: '127.0.0.1', port: 5001, protocol: 'http' })
]


let ipfsNodes = []

export const publishToIPFS = async (contract, api) => {
ipfsNodes = [
IpfsHttpClient({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' })
]
if (api.config.get('settings/ipfs-url')) {
const auth = api.config.get('settings/ipfs-project-id') ? 'Basic ' + Buffer.from(api.config.get('settings/ipfs-project-id') + ':' + api.config.get('settings/ipfs-project-secret')).toString('base64') : null
const ipfs = IpfsHttpClient({
host: api.config.get('settings/ipfs-url'),
port: api.config.get('settings/ipfs-port'),
protocol: api.config.get('settings/ipfs-protocol'),
headers: {
Authorization: auth
}
})
ipfsNodes.push(ipfs)
}

// gather list of files to publish
const sources = []
let metadata
Expand Down Expand Up @@ -58,13 +72,12 @@ export const publishToIPFS = async (contract, api) => {
console.log(error)
reject(error)
})
})
})
}))
// publish the list of sources in order, fail if any failed
await Promise.all(sources.map(async (item) => {
try {
const result = await ipfsVerifiedPublish(item.content, item.hash, api)

try {
item.hash = result.url.match('dweb:/ipfs/(.+)')[1]
} catch (e) {
Expand Down Expand Up @@ -104,12 +117,12 @@ export const publishToIPFS = async (contract, api) => {
const ipfsVerifiedPublish = async (content, expectedHash, api) => {
try {
const results = await severalGatewaysPush(content)

if (expectedHash && results !== expectedHash) {
return { message: 'hash mismatch between solidity bytecode and uploaded content.', url: 'dweb:/ipfs/' + results, hash: results }
const hash: any = (results as any).path
if (expectedHash && hash !== expectedHash) {
return { message: 'hash mismatch between solidity bytecode and uploaded content.', url: 'dweb:/ipfs/' + hash, hash }
} else {
api.writeFile('ipfs/' + results, content)
return { message: 'ok', url: 'dweb:/ipfs/' + results, hash: results }
api.writeFile('ipfs/' + hash, content)
return { message: 'ok', url: 'dweb:/ipfs/' + hash, hash }
}
} catch (error) {
throw new Error(error)
Expand Down
1 change: 1 addition & 0 deletions libs/remix-ui/settings/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const enablePersonalModeText = ' Enable Personal Mode for web3 provider.
export const matomoAnalytics = 'Enable Matomo Analytics. We do not collect personally identifiable information (PII). The info is used to improve the site’s UX & UI. See more about '
export const swarmSettingsTitle = 'Swarm Settings'
export const swarmSettingsText = 'Swarm Settings'
export const ipfsSettingsText = 'IPFS Settings'
export const labels = {
'gist': {
'link': gitAccessTokenLink,
Expand Down
111 changes: 109 additions & 2 deletions libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useState, useReducer, useEffect, useCallback } from 'react' // eslint-disable-line
import { CopyToClipboard } from '@remix-ui/clipboard' // eslint-disable-line

import { enablePersonalModeText, ethereunVMText, labels, generateContractMetadataText, matomoAnalytics, textDark, textSecondary, warnText, wordWrapText, swarmSettingsTitle } from './constants'
import { enablePersonalModeText, ethereunVMText, labels, generateContractMetadataText, matomoAnalytics, textDark, textSecondary, warnText, wordWrapText, swarmSettingsTitle, ipfsSettingsText } from './constants'

import './remix-ui-settings.css'
import { ethereumVM, generateContractMetadat, personal, textWrapEventAction, useMatomoAnalytics, saveTokenToast, removeTokenToast, saveSwarmSettingsToast } from './settingsAction'
import { ethereumVM, generateContractMetadat, personal, textWrapEventAction, useMatomoAnalytics, saveTokenToast, removeTokenToast, saveSwarmSettingsToast, saveIpfsSettingsToast } from './settingsAction'
import { initialState, toastInitialState, toastReducer, settingReducer } from './settingsReducer'
import { Toaster } from '@remix-ui/toaster'// eslint-disable-line
import { RemixUiThemeModule, ThemeModule} from '@remix-ui/theme-module'
Expand All @@ -26,6 +26,12 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const [privateBeeAddress, setPrivateBeeAddress] = useState('')
const [postageStampId, setPostageStampId] = useState('')
const [resetState, refresh] = useState(0)
const [ipfsUrl, setipfsUrl] = useState('')
const [ipfsPort, setipfsPort] = useState('')
const [ipfsProtocol, setipfsProtocol] = useState('')
const [ipfsProjectId, setipfsProjectId] = useState('')
const [ipfsProjectSecret, setipfsProjectSecret] = useState('')


const initValue = () => {
const metadataConfig = props.config.get('settings/generate-contract-metadata')
Expand Down Expand Up @@ -59,6 +65,29 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
if (configPostageStampId) {
setPostageStampId(configPostageStampId)
}

const configipfsUrl = props.config.get('settings/ipfs-url')
if (configipfsUrl) {
setipfsUrl(configipfsUrl)
}
const configipfsPort = props.config.get('settings/ipfs-port')
if (configipfsPort) {
setipfsPort(configipfsPort)
}
const configipfsProtocol = props.config.get('settings/ipfs-protocol')
if (configipfsProtocol) {
setipfsProtocol(configipfsProtocol)
}
const configipfsProjectId = props.config.get('settings/ipfs-project-id')
if (configipfsProjectId) {
setipfsProjectId(configipfsProjectId)
}
const configipfsProjectSecret = props.config.get('settings/ipfs-project-secret')
if (configipfsProjectSecret) {
setipfsProjectSecret(configipfsProjectSecret)
}


}, [themeName, state.message])

useEffect(() => {
Expand Down Expand Up @@ -237,13 +266,91 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
</div>
)

// ipfs settings

const handleSaveIpfsProjectId = useCallback(
(event) => {
setipfsProjectId(event.target.value)
}
, [ipfsProjectId]
)

const handleSaveIpfsSecret = useCallback(
(event) => {
setipfsProjectSecret(event.target.value)
}
, [ipfsProjectSecret]
)

const handleSaveIpfsUrl = useCallback(
(event) => {
setipfsUrl(event.target.value)
}
, [ipfsUrl]
)

const handleSaveIpfsPort = useCallback(
(event) => {
setipfsPort(event.target.value)
}
, [ipfsPort]
)

const handleSaveIpfsProtocol = useCallback(
(event) => {
setipfsProtocol(event.target.value)
}
, [ipfsProtocol]
)

const saveIpfsSettings = () => {
saveIpfsSettingsToast(props.config, dispatchToast, ipfsUrl, ipfsProtocol, ipfsPort, ipfsProjectId, ipfsProjectSecret)
}

const ipfsSettings = () => (
<div className="border-top">
<div className="card-body pt-3 pb-2">
<h6 className="card-title">{ ipfsSettingsText }</h6>
<div className="pt-2 mb-1"><label>IPFS HOST:</label>
<div className="text-secondary mb-0 h6">
<input placeholder='e.g. ipfs.infura.io' id="settingsIpfsUrl" data-id="settingsIpfsUrl" className="form-control" onChange={handleSaveIpfsUrl} value={ ipfsUrl } />
</div>
</div>
<div className=""><label>IPFS PROTOCOL:</label>
<div className="text-secondary mb-0 h6">
<input placeholder='e.g. https' id="settingsIpfsProtocol" data-id="settingsIpfsProtocol" className="form-control" onChange={handleSaveIpfsProtocol} value={ ipfsProtocol } />
</div>
</div>
<div className=""><label>IPFS PORT:</label>
<div className="text-secondary mb-0 h6">
<input placeholder='e.g. 5001' id="settingsIpfsPort" data-id="settingsIpfsPort" className="form-control" onChange={handleSaveIpfsPort} value={ ipfsPort } />
</div>
</div>
<div className=""><label>IPFS PROJECT ID [ INFURA ]:</label>
<div className="text-secondary mb-0 h6">
<input id="settingsIpfsProjectId" data-id="settingsIpfsProjectId" className="form-control" onChange={handleSaveIpfsProjectId} value={ ipfsProjectId } />
</div>
</div>
<div className=""><label>IPFS PROJECT SECRET [ INFURA ]:</label>
<div className="text-secondary mb-0 h6">
<input id="settingsIpfsProjectSecret" data-id="settingsIpfsProjectSecret" className="form-control" type="password" onChange={handleSaveIpfsSecret} value={ ipfsProjectSecret } />
</div>
</div>
<div className="d-flex justify-content-end pt-2">
<input className="btn btn-sm btn-primary ml-2" id="saveIpfssettings" data-id="settingsTabSaveIpfsSettings" onClick={() => saveIpfsSettings()} value="Save" type="button"></input>
</div>
</div>
</div>)


return (
<div>
{state.message ? <Toaster message= {state.message}/> : null}
{generalConfig()}
{token('gist')}
{token('etherscan')}
{swarmSettings()}
{ipfsSettings()}
<RemixUiThemeModule themeModule={props._deps.themeModule} />
</div>
)
Expand Down
9 changes: 9 additions & 0 deletions libs/remix-ui/settings/src/lib/settingsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,12 @@ export const saveSwarmSettingsToast = (config, dispatch, privateBeeAddress, post
config.set('settings/swarm-postage-stamp-id', postageStampId)
dispatch({ type: 'save', payload: { message: 'Swarm settings have been saved' } })
}

export const saveIpfsSettingsToast = (config, dispatch, ipfsURL, ipfsProtocol, ipfsPort, ipfsProjectId, ipfsProjectSecret) => {
config.set('settings/ipfs-url', ipfsURL)
config.set('settings/ipfs-protocol', ipfsProtocol)
config.set('settings/ipfs-port', ipfsPort)
config.set('settings/ipfs-project-id', ipfsProjectId)
config.set('settings/ipfs-project-secret', ipfsProjectSecret)
dispatch({ type: 'save', payload: { message: 'IPFS settings have been saved' } })
}

0 comments on commit f451219

Please sign in to comment.