Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connect with Decentraland IPFS node #75

Merged
merged 8 commits into from
Mar 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
LAND_REGISTRY_CONTRACT_ADDRESS=
IPFS_GATEWAY=
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.vs/
.vscode/
.idea
bower_components/
node_modules/
tmp/
Expand All @@ -9,7 +10,9 @@ tmp/
dist/

.next/
.decentraland/ipns
.decentraland
scene.html
scene.json

# Until API is stable
package-lock.json
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CLI tool for parcel management.
* [x] Uploading scenes to IPFS
* [x] Hot reloading
* [x] Linking Ethereum to the scene
* [x] Pinning scene to Decentraland IPFS node
* [ ] Editor modifying local files and “uploading” to the directory
* [ ] Optimizing objects, textures
* [ ] Warnings and linting of scenes
Expand Down Expand Up @@ -68,7 +69,7 @@ Note: You need to have IPFS daemon running for this to work!
$ dcl upload
```

- Link Ethereum to the current scene:
- Link Ethereum to the current scene and pin scene to Decentraland IPFS node:

```bash
$ dcl link
Expand Down Expand Up @@ -104,6 +105,8 @@ $ npm update -g decentraland

For CLI tool development, run `npm start` in your terminal. The CLI will use the mainnet address for the LANDProxy contract by default. If you want to change it, you can add a `.env` file on the root folder, with a `LAND_REGISTRY_CONTRACT_ADDRESS` var. It'll use [dotenv](https://github.com/motdotla/dotenv#faq) to fetch the value. You can check the current contract addresses [here](https://contracts.decentraland.org/addresses.json).

For the Decentraland IPFS node, we are getting the url from [here](decentraland.github.io/ipfs-node/url.json). If you want to set a different url set the `IPFS_GATEWAY` var in the `.env` file.

You can run CLI commands in development mode like this: `npm start -- init`

You can do incremental compilations by running `npm run watch`, but you will need to run `npm run build` at least once before to build the `linker-app`, and if you make changes to the linker you will need to re-run `npm run build`.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"axios": "^0.17.1",
"babel-polyfill": "^6.26.0",
"chalk": "^2.3.1",
"decentraland-commons": "^2.10.0",
"decentraland-commons": "^3.1.1",
"docker-names": "^1.0.3",
"fs-extra": "^5.0.0",
"get-installed-path": "^4.0.8",
Expand Down
89 changes: 75 additions & 14 deletions pages/linker.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import "babel-polyfill";
import React from 'react';
import Router from 'next/router';
import { eth } from 'decentraland-commons';
import { LANDRegistry } from 'decentraland-commons/dist/contracts/LANDRegistry';
import { eth, txUtils, contracts } from 'decentraland-commons';
const { LANDRegistry } = contracts

async function ethereum() {
const { address } = await getContractAddress()
const land = new LANDRegistry(address)

await eth.connect({ contracts: [land]})
await eth.connect({
contracts: [land]
})

return {
address: await eth.getAddress(),
land,
web3: eth.web3
web3: eth.web3,
land
}
}

Expand All @@ -33,38 +35,62 @@ async function getIPFSKey() {
return ipfsKey;
}

async function getPeerId() {
const res = await fetch('/api/get-ipfs-peerid');
const peerId = await res.json();
return peerId;
}

async function closeServer(ok, message) {
console.log('closing server:', message)
await fetch(`/api/close?ok=${ok}&reason=${message}`);
}

async function pinFiles (peerId, x, y) {
const res = await fetch(`/api/pin-files/${peerId}/${x}/${y}`);
const { ok } = await res.json()
return ok
}

export default class Page extends React.Component {
constructor(...args) {
super(...args);

this.state = {
loading: true,
transactionLoading: false,
pinningLoading: false,
error: false,
address: null,
tx: null
}

this.onUnload = this.onUnload.bind(this)
}

onUnload(event) {
event.returnValue = 'Please, wait until the transaction and pinning are completed'
}

componentWillUnmount() {
window.removeEventListener("beforeunload", this.onUnload)
}

async componentDidMount() {
try {
window.addEventListener("beforeunload", this.onUnload)
try {
let land, address, web3

try {
const res = await ethereum()
land = res.land
address = res.address
web3 = res.web3

this.setState({
loading: false,
address
})
} catch(err) {
console.log(err.message)
this.setState({
error: `Could not connect to MetaMask`
});
Expand All @@ -84,7 +110,8 @@ export default class Page extends React.Component {

try {
const ipfsKey = await getIPFSKey();
this.setState({ ipfsKey });
const peerId = await getPeerId();
this.setState({ ipfsKey, peerId });
} catch(err) {
this.setState({
error: `There was a problem getting IPNS hash of your scene.\nTry to re-upload with dcl upload.`
Expand All @@ -106,7 +133,7 @@ export default class Page extends React.Component {
let oldData
try {
console.log('oldData coordinates', coordinates[0].x, coordinates[0].y)
oldData = await land.getData(coordinates[0].x, coordinates[0].y)
oldData = await land.landData(coordinates[0].x, coordinates[0].y)
console.log('oldData data', oldData)
} catch(e) {
console.error('oldData error', e)
Expand All @@ -129,8 +156,8 @@ export default class Page extends React.Component {
try {
console.log('update land data', coordinates, data)
const tx = await land.updateManyLandData(coordinates, data)
this.setState({ tx })
closeServer(true, 'transaction successful')
this.watchTransactions(tx, coordinates[0].x, coordinates[0].y)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this watch the transaction for the whole Estate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait until transaction is completed by success or fail

this.setState({ tx, transactionLoading: true })
} catch(err) {
this.setState({loading: false, error: 'Transaction Rejected'})
closeServer(false, 'transaction rejected')
Expand All @@ -141,6 +168,22 @@ export default class Page extends React.Component {
}
}

async watchTransactions (txId, x, y) {
const {peerId} = this.state
const tx = await txUtils.waitForCompletion(txId)
if (!txUtils.isFailure(tx)) {
this.setState({ transactionLoading: false, pinningLoading: true })
const success = await pinFiles(peerId, x, y)
this.setState({
pinningLoading: false,
error: !success ? 'Failed pinning files to ipfs' : null
})
} else {
this.setState({transactionLoading: false, error: 'Transaction failed'})
}
window.removeEventListener("beforeunload", this.onUnload)
}

renderTxHash = () => (
this.state.tx ? (
<p>Transaction:<br />
Expand All @@ -155,6 +198,22 @@ export default class Page extends React.Component {
this.state.error ? <p style={{ color: 'red' }}>{this.state.error}</p> : null
)

renderTransactionStatus = () => (
this.state.tx ?
!this.state.transactionLoading ?
<p style={{ color: 'green' }}>{`Transaction confirmed.`}</p>
: <p style={{ color: 'orange' }}>{`Transaction pending. Will take a while...`}</p>
: null
)

renderPinningIPFSStatus = () => (
!this.state.error && this.state.tx && !this.state.transactionLoading ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if(..) {
  return
} else if(..) {
  return
}
return null

Is probably easier to read

!this.state.pinningLoading ?
<p style={{ color: 'green' }}>{`Pinning Success.`}</p>
: <p style={{ color: 'orange' }}>{`Pinning pending. Will take a while...`}</p>
: null
)

render() {
return (
<div className="dcl-linker-main">
Expand All @@ -163,8 +222,10 @@ export default class Page extends React.Component {
<p>MetaMask address:<br />
{this.state.loading ? "loading..." : this.state.address}
</p>
{this.renderTxHash()}
{this.renderError()}
{ this.renderTxHash() }
{ this.renderTransactionStatus() }
{ this.renderPinningIPFSStatus() }
{ this.renderError() }
<style jsx>{`
.dcl-icon {
width: 52px;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function init(vorpal: any) {
{ type: 'input', name: 'owner', message: chalk.blue('Your MetaMask address: ') },
{ type: 'input', name: 'contact.name', message: chalk.blue('Your name: ') },
{ type: 'input', name: 'contact.email', message: chalk.blue('Your email: ') },
{ type: 'input', name: 'main', message: chalk.blue('Main: '), default: 'scene' },
{ type: 'input', name: 'main', message: chalk.blue('Main: '), default: 'scene.html' },
{ type: 'input', name: 'tags', message: chalk.blue('Tags: ') },
{ type: 'input', name: 'scene.parcels', message: `${chalk.blue('Parcels')} ${chalk.grey('(use the format \'x,y; x,y; x,y ...\'):')} ` },
{ type: 'input', name: 'communications.type', message: chalk.blue('Communication type: '), default: 'webrtc' },
Expand Down
15 changes: 15 additions & 0 deletions src/utils/get-ipfs-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from 'axios';
import { env } from 'decentraland-commons';
env.load();

export const getIPFSURL = async () => {
let ipfsURL: string = null;
try {
const { data } = await axios.get('https://decentraland.github.io/ipfs-node/url.json');
ipfsURL = data.staging;
} catch (error) {
// fallback to ENV
}

return env.get('IPFS_GATEWAY', () => ipfsURL);
}
64 changes: 49 additions & 15 deletions src/utils/linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import serve = require('koa-static');
import axios from 'axios';
import { env } from 'decentraland-commons';
import * as project from '../utils/project';
import { prompt } from './prompt';
import opn = require('opn');
import { getRoot } from './get-root';
import { getIPFSURL } from './get-ipfs-url';
import path = require('path');

export async function linker(vorpal: any, args: any, callback: () => void) {
const root = getRoot()

const root = getRoot();
const isDclProject = await fs.pathExists(path.join(root, 'scene.json'));
if (!isDclProject) {
vorpal.log(`Seems like this is not a Decentraland project! ${chalk.grey('(\'scene.json\' not found.)')}`);
vorpal.log(
`Seems like this is not a Decentraland project! ${chalk.grey(
`('scene.json' not found.)`,
)}`,
);
callback();
return;
}
Expand All @@ -26,7 +29,9 @@ export async function linker(vorpal: any, args: any, callback: () => void) {
);

if (!hasLinker) {
vorpal.log(`Looks like linker app is missing. Try to re-initialize your project.`);
vorpal.log(
`Looks like linker app is missing. Try to re-initialize your project.`,
);
callback();
return;
}
Expand All @@ -44,35 +49,64 @@ export async function linker(vorpal: any, args: any, callback: () => void) {
ctx.body = await fs.readJson(path.join(root, 'scene.json'));
});

router.get('/api/get-ipfs-key', async (ctx) => {
let project
router.get('/api/get-ipfs-key', async ctx => {
let project;
try {
project = JSON.parse(fs.readFileSync(path.join(root, '.decentraland', 'project.json'), 'utf-8'))
} catch (error) {
vorpal.log(chalk.red('Could not find `.decentraland/project.json`'))
process.exit(1)
vorpal.log(chalk.red('Could not find `.decentraland/project.json`'));
process.exit(1);
}
ctx.body = JSON.stringify(project.ipfsKey);
});

router.get('/api/contract-address', async (ctx) => {
router.get('/api/get-ipfs-peerid', async ctx => {
let project;
try {
project = JSON.parse(
fs.readFileSync(path.join(root, '.decentraland', 'project.json'), 'utf-8'),
);
} catch (error) {
vorpal.log(chalk.red('Could not find `.decentraland/project.json`'));
process.exit(1);
}
ctx.body = JSON.stringify(project.peerId);
});

router.get('/api/contract-address', async ctx => {
let LANDRegistryAddress: string = null;

try {
const { data } = await axios.get('https://contracts.decentraland.org/addresses.json');
const { data } = await axios.get(
'https://contracts.decentraland.org/addresses.json',
);
LANDRegistryAddress = data.mainnet.LANDProxy;
} catch (error) {
// fallback to ENV
}

LANDRegistryAddress = env.get('LAND_REGISTRY_CONTRACT_ADDRESS', () => LANDRegistryAddress);
LANDRegistryAddress = env.get(
'LAND_REGISTRY_CONTRACT_ADDRESS',
() => LANDRegistryAddress,
);

ctx.body = JSON.stringify({
address: LANDRegistryAddress
address: LANDRegistryAddress,
});
});

router.get('/api/close', async (ctx) => {
router.get('/api/pin-files/:peerId/:x/:y', async ctx => {
const { peerId, x, y } = ctx.params;
let ipfsURL: string = await getIPFSURL();

const { ok, message } = await axios
.get(`${ipfsURL}/pin/${peerId}/${x}/${y}`)
.then(response => response.data)
.catch(error => ({ ok: false, message: error.message }));
ctx.body = JSON.stringify({ ok, message });
});

router.get('/api/close', async ctx => {
ctx.res.end();
const ok = require('url').parse(ctx.req.url, true).query.ok;
if (ok === 'true') {
Expand All @@ -83,7 +117,7 @@ export async function linker(vorpal: any, args: any, callback: () => void) {
process.exit(0);
});

router.get('*', async (ctx) => {
router.get('*', async ctx => {
ctx.respond = false;
});

Expand Down
Loading