From 582f6b7f5ab7d543d288578c839d559aa292608d Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 8 Jun 2017 21:10:22 +0200 Subject: [PATCH] Use/store IPFS metadata for people and proposals closes #3 closes #4 --- index.js | 143 ++++++++++++++++++++++++++++++++++++++++++--------- package.json | 3 ++ 2 files changed, 123 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index ee9feb6..26f9195 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,14 @@ // KREDITS_ROOM: Kredit proposals are posted to this chatroom // KREDITS_WALLET_PATH: Path to a etherum wallet JSON file // KREDITS_WALLET_PASSWORD: Wallet password +// KREDITS_CONTRACT_ADDRESS: Address of Kredits contract +// KREDITS_PROVIDER_URL: Ethereum JSON-RPC URL (default 'http://localhost:8545') +// IPFS_API_HOST: Host/domain (default 'localhost') +// IPFS_API_PORT: Port number (default '5001') +// IPFS_API_PROTOCOL: Protocol, e.g. 'http' or 'https' (default 'http') // const fs = require('fs'); +const util = require('util'); const fetch = require('node-fetch'); const kreditsContracts = require('kredits-contracts'); const ProviderEngine = require('web3-provider-engine'); @@ -15,23 +21,24 @@ const Wallet = require('ethereumjs-wallet'); const WalletSubprovider = require('ethereumjs-wallet/provider-engine'); const Web3Subprovider = require('web3-provider-engine/subproviders/web3.js'); const Web3 = require('web3'); +const ipfsAPI = require('ipfs-api'); +const schemas = require('kosmos-schemas'); +const tv4 = require('tv4'); (function() { "use strict"; + // + // Instantiate ethereum client and wallet + // let engine = new ProviderEngine(); - let walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json'; - let walletJson = fs.readFileSync(walletPath); - let wallet = Wallet.fromV3(JSON.parse(walletJson), process.env.KREDITS_WALLET_PASSWORD); + let walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json'; + let walletJson = fs.readFileSync(walletPath); + let wallet = Wallet.fromV3(JSON.parse(walletJson), process.env.KREDITS_WALLET_PASSWORD); let providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:8545'; let hubotWalletAddress = '0x' + wallet.getAddress().toString('hex'); - let config = {}; - if (process.env.KREDITS_CONTRACT_ADDRESS) { - config = { Kredits: { address: process.env.KREDITS_CONTRACT_ADDRESS }}; - } - engine.addProvider(new WalletSubprovider(wallet, {})); engine.addProvider(new Web3Subprovider(new Web3.providers.HttpProvider(providerUrl))); // TODO only start engine if providerURL is accessible @@ -40,9 +47,29 @@ const Web3 = require('web3'); let web3 = new Web3(engine); web3.eth.defaultAccount = hubotWalletAddress; - let contracts = kreditsContracts(web3, config); + // + // Instantiate contracts + // + let contractConfig = {}; + if (process.env.KREDITS_CONTRACT_ADDRESS) { + contractConfig = { Kredits: { address: process.env.KREDITS_CONTRACT_ADDRESS }}; + } + let contracts = kreditsContracts(web3, contractConfig); let kredits = contracts['Kredits']; + // + // Instantiate IPFS API client + // + let ipfsConfig = {}; + if (process.env.IPFS_API_HOST) { + ipfsConfig = { + host: process.env.IPFS_API_HOST, + port: process.env.IPFS_API_PORT, + protocol: process.env.IPFS_API_PROTOCOL + }; + } + let ipfs = ipfsAPI(ipfsConfig); + module.exports = function(robot) { robot.logger.info('[hubot-kredits] Wallet address: ' + hubotWalletAddress); @@ -75,20 +102,57 @@ const Web3 = require('web3'); }); } + function loadProfileFromIPFS(contributor) { + let promise = new Promise((resolve, reject) => { + return ipfs.cat(contributor.ipfsHash, { buffer: true }).then(res => { + let content = res.toString(); + let profile = JSON.parse(content); + + contributor.name = profile.name; + contributor.kind = profile.kind; + + let accounts = profile.accounts; + let github = accounts.find(a => a.site === 'github.com'); + let wiki = accounts.find(a => a.site === 'wiki.kosmos.org'); + + if (github) { + contributor.github_username = github.username; + contributor.github_uid = github.uid; + } + if (wiki) { + contributor.wiki_username = wiki.username; + } + + resolve(contributor); + }).catch((err) => { + console.log(err); + reject(err); + }); + }); + + return promise; + } + function getContributorData(i) { let promise = new Promise((resolve, reject) => { getValueFromContract('contributorAddresses', i).then(address => { robot.logger.debug('address', address); getValueFromContract('contributors', address).then(person => { robot.logger.debug('person', person); - let contributor = { + let c = { address: address, - github_username: person[1], - github_uid: person[0], + name: person[1], + id: person[0], ipfsHash: person[2] }; - robot.logger.debug('[kredits] contributor', contributor); - resolve(contributor); + if (c.ipfsHash) { + loadProfileFromIPFS(c).then(contributor => { + robot.logger.debug('[kredits] contributor', contributor); + resolve(contributor); + }).catch(() => console.log('[kredits] error fetching contributor info from IPFS for '+c.name)); + } else { + resolve(c); + } }); }).catch(err => reject(err)); }); @@ -150,15 +214,42 @@ const Web3 = require('web3'); return amount; } - function createProposal(recipient, amount, url/*, metaData*/) { - return new Promise((resolve, reject) => { - // TODO write metaData to IPFS - robot.logger.debug(`Creating proposal to issue ${amount}₭S to ${recipient} for ${url}...`); + function createContributionDocument(contributor, url, description, details) { + let contribution = { + "@context": "https://schema.kosmos.org", + "@type": "Contribution", + contributor: { + ipfs: contributor.ipfsHash + }, + kind: 'dev', + url: url, + description: description, + details: details + }; + if (! tv4.validate(contribution, schemas["contribution"])) { + console.log('[kredits] invalid contribution data: ', util.inspect(contribution)); + return Promise.reject('invalid contribution data'); + } + + return ipfs.add(new ipfs.Buffer(JSON.stringify(contribution))) + .then(res => { return res[0].hash; }) + .catch(err => console.log(err)); + } + + function createProposal(recipient, amount, url, description, details) { + robot.logger.debug(`Creating proposal to issue ${amount}₭S to ${recipient} for ${url}...`); + + return new Promise((resolve, reject) => { + // Get contributor details for GitHub user getContributorByGithubUser(recipient).then(c => { - kredits.addProposal(c.address, amount, url, '', (e/* , d */) => { - if (e) { reject(); return; } - messageRoom(`Let's give ${recipient} some kredits for ${url}! We just need two votes: https://kredits.kosmos.org`); + // Create document containing contribution data on IPFS + createContributionDocument(c, url, description, details).then(ipfsHash => { + // Create proposal on ethereum blockchain + kredits.addProposal(c.address, amount, url, ipfsHash, (e/* , d */) => { + if (e) { reject(e); return; } + messageRoom(`Let's give ${recipient} some kredits for ${url}: https://kredits.kosmos.org`); + }); }); }, () => { messageRoom(`I wanted to propose giving kredits to ${recipient} for ${url}, but I can't find their contact data. Please add them as a contributor: https://kredits.kosmos.org`); @@ -185,8 +276,11 @@ const Web3 = require('web3'); recipients = [issue.user.login]; } + let repoName = issue.repository_url.match(/.*\/(.+\/.+)$/)[1]; + let description = `${repoName}: ${issue.title}`; + recipients.forEach(recipient => { - createProposal(recipient, amount, web_url, issue); + createProposal(recipient, amount, web_url, description, issue); }); resolve(); @@ -220,8 +314,11 @@ const Web3 = require('web3'); let amount = amountFromIssueLabels(issue); if (amount === 0) { resolve(); return; } + let repoName = pull_request.base.repo.full_name; + let description = `${repoName}: ${pull_request.title}`; + recipients.forEach(recipient => { - createProposal(recipient, amount, web_url, pull_request); + createProposal(recipient, amount, web_url, description, pull_request); }); resolve(); diff --git a/package.json b/package.json index d69dd07..5e8fe2e 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,12 @@ }, "dependencies": { "ethereumjs-wallet": "mvayngrib/ethereumjs-wallet", + "ipfs-api": "^14.0.3", + "kosmos-schemas": "^1.1.2", "kredits-contracts": "2.0.0", "node-fetch": "^1.6.3", "prompt": "^1.0.0", + "tv4": "^1.3.0", "web3": "^0.18.4", "web3-provider-engine": "^12.0.3" },