From 050e8ce79f993aefc4e83fb48d83dd0808d9c8d0 Mon Sep 17 00:00:00 2001 From: bumi Date: Wed, 18 Apr 2018 16:27:22 +0200 Subject: [PATCH] WIP update to use new kredits library Using the new kredits library to load contract data and create proposals. currently the kredits lib is only copied - which needs to be removed asap. --- index.js | 338 +++++++--------------------- kredits/contracts/base.js | 17 ++ kredits/contracts/contributor.js | 61 +++++ kredits/contracts/index.js | 9 + kredits/contracts/operator.js | 61 +++++ kredits/contracts/token.js | 4 + kredits/index.js | 2 + kredits/kredits.js | 102 +++++++++ kredits/serializers/contribution.js | 64 ++++++ kredits/serializers/contributor.js | 93 ++++++++ kredits/utils/ipfs.js | 54 +++++ package.json | 9 +- 12 files changed, 555 insertions(+), 259 deletions(-) create mode 100644 kredits/contracts/base.js create mode 100644 kredits/contracts/contributor.js create mode 100644 kredits/contracts/index.js create mode 100644 kredits/contracts/operator.js create mode 100644 kredits/contracts/token.js create mode 100644 kredits/index.js create mode 100644 kredits/kredits.js create mode 100644 kredits/serializers/contribution.js create mode 100644 kredits/serializers/contributor.js create mode 100644 kredits/utils/ipfs.js diff --git a/index.js b/index.js index 437f277..dfaf852 100644 --- a/index.js +++ b/index.js @@ -12,54 +12,27 @@ // 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'); -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() { +const ethers = require('ethers'); + +const kredits = require('./kredits'); + +(async 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 providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:8545'; - let hubotWalletAddress = '0x' + wallet.getAddress().toString('hex'); + let networkId = parseInt(process.env.KREDITS_NETWORK_ID); - engine.addProvider(new WalletSubprovider(wallet, {})); - engine.addProvider(new Web3Subprovider(new Web3.providers.HttpProvider(providerUrl))); - // TODO only start engine if providerURL is accessible - engine.start(); - - let web3 = new Web3(engine); - web3.eth.defaultAccount = hubotWalletAddress; - - // - // 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 = { @@ -68,144 +41,88 @@ const tv4 = require('tv4'); protocol: process.env.IPFS_API_PROTOCOL }; } - let ipfs = ipfsAPI(ipfsConfig); + + const wallet = await ethers.Wallet.fromEncryptedWallet(walletJson, process.env.KREDITS_WALLET_PASSWORD); + const ethProvider = new ethers.providers.JsonRpcProvider(providerUrl, {chainId: networkId}); + ethProvider.signer = wallet; + wallet.provider = ethProvider; + + const kredits = await Kredits.setup(ethProvider, wallet, ipfsConfig); + const Contributor = kredits.Contributor; + const Operator = kredits.Operator; module.exports = function(robot) { - robot.logger.info('[hubot-kredits] Wallet address: ' + hubotWalletAddress); + function messageRoom(message) { + robot.messageRoom(process.env.KREDITS_ROOM, message); + } - getBalance().then(balance => { - if (balance <= 0) { + robot.logger.info('[hubot-kredits] Wallet address: ' + wallet.address); + + ethProvider.getBalance(wallet.address).then(balance => { + robot.logger.info('[hubot-kredits] Wallet balance: ' + balance.toString()); + if (balance.toNumber() <= 0) { messageRoom(`Yo gang, I\'m broke! Please drop me some ETH to ${hubotWalletAddress}. kthxbai.`); } }); - function getBalance() { - return new Promise((resolve, reject) => { - web3.eth.getBalance(hubotWalletAddress, function (err, balance) { - if (err) { - robot.logger.error('[hubot-kredits] Error checking balance'); - reject(err); - return; + robot.respond(/got ETH\?/i, res => { + ethProvider.getBalance(wallet.address).then((balance) => { + res.send(`my wallet contains ${ethers.utils.formatEther(balance)} ETH`); + }); + }); + + robot.respond(/propose (\d*)\s?\S*\s?to (\S+)(?:\sfor (.*))?$"/i, res => { + let amount = res.match[1]; + let githubUser = res.match[2]; + let description = res.match[3]; + let url = null; + createProposal(githubUser, amount, description, url).then((result) => { + messageRoom('Proposal created'); + }); + }); + + robot.respond(/list open proposals/i, res => { + Operator.all().then((proposals) => { + proposals.forEach((proposal) => { + if (!proposal.executed) { + Contributor.getById(proposal.contributorId).then((contributor) => { + messageRoom(`* ${proposal.amount} kredits to ${contributor.name} for ${proposal.description}`); + }); } - resolve(balance); }); }); - } - - function getValueFromContract(contractMethod, ...args) { - return new Promise((resolve, reject) => { - kredits[contractMethod](...args, (err, data) => { - if (err) { reject(err); } - resolve(data); - }); - }); - } - - 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 c = { - address: address, - name: person[1], - id: person[0], - ipfsHash: person[2] - }; - if (c.ipfsHash) { - // robot.logger.debug('[kredits] loading contributor profile loaded for', c.name, c.ipfsHash, '...'); - loadProfileFromIPFS(c).then(contributor => { - // robot.logger.debug('[kredits] contributor profile loaded for', c.name); - resolve(contributor); - }).catch(() => console.log('[kredits] error fetching contributor info from IPFS for '+c.name)); - } else { - resolve(c); - } - }); - }).catch(err => reject(err)); - }); - return promise; - } - - function getContributors() { - return getValueFromContract('contributorsCount').then(contributorsCount => { - let contributors = []; - - for(var i = 0; i < contributorsCount.toNumber(); i++) { - contributors.push(getContributorData(i)); - } - - return Promise.all(contributors); - }); - } + }); function getContributorByGithubUser(username) { - let promise = new Promise((resolve, reject) => { - getContributors().then(contributors => { - let contrib = contributors.find(c => { - return c.github_username === username; - }); - if (contrib) { - resolve(contrib); - } else { - reject(); - } + return Contributor.all().then(contributors => { + let contrib = contributors.find(c => { + return c.github_username === username; }); + if (!contrib) { + throw new Errro(`No contributor found for ${username}`);A + } else { + return contrib; + } }); - return promise; } - function getContributorByAddress(address) { - let promise = new Promise((resolve, reject) => { - getContributors().then(contributors => { - let contrib = contributors.find(c => { - return c.address === address; + function createProposal(githubUser, amount, description, url, details) { + return getContributorByGithubUser(githubUser).then((contributor) => { + robot.logger.debug(`[kredits] Creating proposal to issue ${amount}₭S to ${githubUser} for ${url}...`); + let contributionAttr = { + contributorIpfsHash: contributor.ipfsHash, + url, + description, + details, + kind: 'dev' + }; + return Operator.add(contributionAttr).then((result) => { + robot.logger.debug('[kredits] proposal created:', util.inspect(result)); }); - if (contrib) { - resolve(contrib); - } else { - reject(); - } + }).catch(() => { + messageRoom(`I wanted to propose giving kredits to ${githubUser} for ${url}, but I can't find their contact data. Please add them as a contributor: https://kredits.kosmos.org`); }); - }); - return promise; - } - - function messageRoom(message) { - robot.messageRoom(process.env.KREDITS_ROOM, message); } function amountFromIssueLabels(issue) { @@ -231,55 +148,6 @@ const tv4 = require('tv4'); return amount; } - 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"])) { - robot.logger.error('[kredits] invalid contribution data: ', util.inspect(contribution)); - return Promise.reject('invalid contribution data'); - } - - // robot.logger.debug('[kredits] creating IPFS document for contribution:', contribution.description); - - return ipfs.add(new ipfs.Buffer(JSON.stringify(contribution))) - .then(res => { - // robot.logger.debug('[kredits] created IPFS document', res[0].hash); - return res[0].hash; - }).catch(err => robot.logger.error('[kredits] couldn\'t create IPFS document', err)); - } - - function createProposal(recipient, amount, url, description, details) { - robot.logger.debug(`[kredits] 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 => { - // 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; } - robot.logger.debug('[kredits] proposal created:', util.inspect(d)); - resolve(); - }); - }); - }, () => { - 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`); - resolve(); - }); - }); - } - function handleGitHubIssueClosed(data) { return new Promise((resolve/*, reject*/) => { // fs.writeFileSync('tmp/github-issue.json', JSON.stringify(data, null, 4)); @@ -301,7 +169,7 @@ const tv4 = require('tv4'); let description = `${repoName}: ${issue.title}`; recipients.forEach(recipient => { - createProposal(recipient, amount, web_url, description, issue) + createProposal(recipient, amount, description, web_url, issue) .catch(err => robot.logger.error(err)); }); @@ -339,29 +207,18 @@ const tv4 = require('tv4'); let repoName = pull_request.base.repo.full_name; let description = `${repoName}: ${pull_request.title}`; + let proposalPromisses = []; recipients.forEach(recipient => { - createProposal(recipient, amount, web_url, description, pull_request) - .catch(err => robot.logger.error(err)); + proposalPromisses.push( + createProposal(recipient, amount, description, web_url, pull_request) + .catch(err => robot.logger.error(err)) + ); }); - - resolve(); + return Promise.all(proposalPromisses); }); }); } - robot.respond(/(got ETH)|(got gas)\?/i, res => { - getBalance().then(balance => { - if (balance <= 0) { - res.send(`HALP, I\'m totally broke! Not a single wei in my pocket.`); - } - else if (balance >= 1e+17) { - res.send(`my wallet contains ${web3.fromWei(balance, 'ether')} ETH`); - } - else { - res.send(`I\'m almost broke! Only have ${web3.fromWei(balance, 'ether')} ETH left in my pocket. :(`); - } - }); - }); robot.router.post('/incoming/kredits/github/'+process.env.KREDITS_WEBHOOK_TOKEN, (req, res) => { let evt = req.header('X-GitHub-Event'); @@ -383,45 +240,22 @@ const tv4 = require('tv4'); }); function watchContractEvents() { - web3.eth.getBlockNumber((err, blockNumber) => { - if (err) { - robot.logger.error('[kredits] couldn\t get current block number'); - return false; - } - + ethProvider.getBlockNumber().then((blockNumber) => { // current block is the last mined one, thus we check from the next // mined one onwards to prevent getting previous events let nextBlock = blockNumber + 1; robot.logger.debug(`[kredits] watching events from block ${nextBlock} onward`); + ethProvider.resetEventsBlock(nextBlock); - kredits.allEvents({fromBlock: nextBlock, toBlock: 'latest'}, (error, data) => { - robot.logger.debug('[kredits] received contract event', data.event); - if (data.blockNumber < nextBlock) { - // I don't know why, but the filter doesn't work as intended - robot.logger.debug('[kredits] dismissing old event from block', data.blockNumber); - return false; - } - switch (data.event) { - case 'ProposalCreated': - handleProposalCreated(data); - break; - // case 'ProposalExecuted': - // handleProposalExecuted(data); - // break; - // case 'ProposalVoted': - // handleProposalVoted(data); - // break; - // case 'Transfer': - // handleTransfer(data); - // break; - } - }); + Operator.on('ProposalCreated', handleProposalCreated); }); } - function handleProposalCreated(data) { - getContributorByAddress(data.args.recipient).then((contributor) => { - messageRoom(`Let's give ${contributor.name} some kredits for ${data.args.url}: https://kredits.kosmos.org`); + function handleProposalCreated(proposalId, creatorAccount, contributorId, amount) { + Contributor.getById(contributorId).then((contributor) => { + Operator.getBy(proposalId).then((proposal) => { + messageRoom(`Let's give ${contributor.name} some kredits for ${proposal.url} (${proposal.description}): https://kredits.kosmos.org`); + }); }); } diff --git a/kredits/contracts/base.js b/kredits/contracts/base.js new file mode 100644 index 0000000..c54ca22 --- /dev/null +++ b/kredits/contracts/base.js @@ -0,0 +1,17 @@ +export default class Base { + constructor(contract) { + this.contract = contract; + } + + get functions() { + return this.contract.functions; + } + + on(type, callback) { + let eventMethod = `on${type.toLowerCase()}`; + // Don't use this.contract.events here. Seems to be a bug in ethers.js + this.contract[eventMethod] = callback; + + return this; + } +} diff --git a/kredits/contracts/contributor.js b/kredits/contracts/contributor.js new file mode 100644 index 0000000..8b187a7 --- /dev/null +++ b/kredits/contracts/contributor.js @@ -0,0 +1,61 @@ +import ethers from 'ethers'; +import RSVP from 'rsvp'; + +import Kredits from '../kredits'; +import ContributorSerializer from '../serializers/contributor'; + +import Base from './base'; + +export default class Contributor extends Base { + all() { + return this.functions.contributorsCount() + .then((count) => { + count = count.toNumber(); + let contributors = []; + + for (let id = 1; id <= count; id++) { + contributors.push(this.getById(id)); + } + + return RSVP.all(contributors); + }); + } + + getById(id) { + id = ethers.utils.bigNumberify(id); + + return this.functions.getContributorById(id) + .then((data) => { + // TODO: remove as soon as the contract provides the id + data.id = id; + // TODO: rename address to account + data.address = data.account; + + return data; + }) + // Fetch IPFS data if available + .then((data) => { + return Kredits.ipfs.catAndMerge(data, ContributorSerializer.deserialize); + }); + } + + add(contributorAttr) { + let json = ContributorSerializer.serialize(contributorAttr); + // TODO: validate against schema + + return Kredits.ipfs + .add(json) + .then((ipfsHashAttr) => { + let contributor = [ + contributorAttr.address, + ipfsHashAttr.ipfsHash, + ipfsHashAttr.hashFunction, + ipfsHashAttr.hashSize, + contributorAttr.isCore, + ]; + + console.log('[kredits] addContributor', ...contributor); + return this.functions.addContributor(...contributor); + }); + } +} diff --git a/kredits/contracts/index.js b/kredits/contracts/index.js new file mode 100644 index 0000000..4ef4162 --- /dev/null +++ b/kredits/contracts/index.js @@ -0,0 +1,9 @@ +import Contributor from './contributor'; +import Operator from './operator'; +import Token from './token'; + +export default { + Contributors: Contributor, + Operator, + Token +}; diff --git a/kredits/contracts/operator.js b/kredits/contracts/operator.js new file mode 100644 index 0000000..5994826 --- /dev/null +++ b/kredits/contracts/operator.js @@ -0,0 +1,61 @@ +import ethers from 'ethers'; +import RSVP from 'rsvp'; + +import Kredits from '../kredits'; +import ContributionSerializer from '../serializers/contribution'; + +import Base from './base'; + +export default class Operator extends Base { + all() { + return this.functions.proposalsCount() + .then((count) => { + count = count.toNumber(); + let proposals = []; + + for (let id = 1; id <= count; id++) { + proposals.push(this.getById(id)); + } + + return RSVP.all(proposals); + }); + } + + getById(id) { + id = ethers.utils.bigNumberify(id); + + return this.functions.proposals(id) + .then((data) => { + // TODO: remove as soon as the contract provides the id + data.id = id; + // TODO: rename creatorAddress to creator + data.creatorAddress = data.creator; + + return data; + }) + // Fetch IPFS data if available + .then((data) => { + return Kredits.ipfs.catAndMerge(data, ContributionSerializer.deserialize); + }); + } + + addProposal(proposalAttr) { + let json = ContributionSerializer.serialize(proposalAttr); + // TODO: validate against schema + + return Kredits.ipfs + .add(json) + .then((ipfsHashAttr) => { + let proposal = [ + proposalAttr.contributorId, + proposalAttr.amount, + ipfsHashAttr.ipfsHash, + ipfsHashAttr.hashFunction, + ipfsHashAttr.hashSize, + ]; + + console.log('[kredits] addProposal', ...proposal); + return this.functions.addProposal(...proposal); + }); + } +} diff --git a/kredits/contracts/token.js b/kredits/contracts/token.js new file mode 100644 index 0000000..42c97ee --- /dev/null +++ b/kredits/contracts/token.js @@ -0,0 +1,4 @@ +import Base from './base'; + +export default class Token extends Base { +} diff --git a/kredits/index.js b/kredits/index.js new file mode 100644 index 0000000..cdd0cfe --- /dev/null +++ b/kredits/index.js @@ -0,0 +1,2 @@ +import kredits from './kredits'; +export default kredits; diff --git a/kredits/kredits.js b/kredits/kredits.js new file mode 100644 index 0000000..46a893f --- /dev/null +++ b/kredits/kredits.js @@ -0,0 +1,102 @@ +import ethers from 'npm:ethers'; +import RSVP from 'rsvp'; + +import abis from 'contracts/abis'; +import addresses from 'contracts/addresses'; + +import contracts from './contracts'; +import IPFS from './utils/ipfs'; + +// Helpers +function capitalize(word) { + let [first, ...rest] = word; + return `${first.toUpperCase()}${rest.join('')}`; +} + +export default class Kredits { + constructor(provider, signer, addresses) { + this.provider = provider; + this.signer = signer; + + // Initialize our registry contract + this.addresses = addresses; + this.contracts = {}; + } + + static setup(provider, signer, ipfsConfig) { + this.ipfsConfig = ipfsConfig; + this.ipfs = new IPFS(ipfsConfig); + + return this.ipfs._ipfsAPI.id().catch((error) => { + throw new Error(`IPFS node not available; config: ${JSON.stringify(ipfsConfig)} - ${error.message}`); + }).then(() => { + + let registryContract = this.initRegistryContract(provider); + + let addresses = Object.keys(contracts).reduce((mem, name) => { + let contractName = capitalize(name); + mem[contractName] = registryContract.functions.getProxyFor(contractName).catch((error) => { + throw new Error(`Failed to get address for ${contractName} from registry at ${registryContract.address} + - correct registry? does it have version entry? - ${error.message}` + ); + }); + return mem; + }, {}); + + return RSVP.hash(addresses) + .then((addresses) => { + return new Kredits(provider, signer, addresses); + }); + }); + } + + static initRegistryContract(provider) { + let address = addresses['Registry'][provider.chainId]; + if (!address) { + throw new Error(`Registry address not found; invalid network? + requested network: ${provider.chainId} + supported networks: ${Object.keys(addresses['Registry'])} + `); + } + provider.getCode(address).then((code) => { + // not sure if we always get the same return value of the code is not available + // that's why checking if it is < 5 long + if (code === '0x00' || code.length < 5) { + throw new Error(`Registry not found at ${address} on network ${provider.chainId}`); + } + }); + let abi = abis['Registry']; + console.log('Initialize registry contract:', address, abi, provider); + return new ethers.Contract(address, abi, provider); + } + + get Contributor() { + // TODO: rename to contributor + return this.contractFor('contributors'); + } + + get Operator() { + return this.contractFor('operator'); + } + + get Token() { + return this.contractFor('token'); + } + + // Should be private + contractFor(name) { + if (this.contracts[name]) { + return this.contracts[name]; + } + + let contractName = capitalize(name); + let address = this.addresses[contractName]; + if (!address || !abis[contractName]) { + throw new Error(`Address or ABI not found for ${contractName}`); + } + let contract = new ethers.Contract(address, abis[contractName], this.signer); + this.contracts[name] = new contracts[contractName](contract); + + return this.contracts[name]; + } +} diff --git a/kredits/serializers/contribution.js b/kredits/serializers/contribution.js new file mode 100644 index 0000000..2bed268 --- /dev/null +++ b/kredits/serializers/contribution.js @@ -0,0 +1,64 @@ +/** + * Handle serialization for JSON-LD object of the contribution, according to + * https://github.com/67P/kosmos-schemas/blob/master/schemas/contribution.json + * + * @class + * @public + */ +export default class Contributor { + /** + * Deserialize JSON to object + * + * @method + * @public + */ + static deserialize(serialized) { + let { + kind, + description, + details, + url, + } = JSON.parse(serialized.toString('utf8')); + + return { + kind, + description, + details, + url, + ipfsData: serialized, + }; + } + + /** + * Serialize object to JSON + * + * @method + * @public + */ + static serialize(deserialized) { + let { + contributorIpfsHash, + kind, + description, + url, + } = deserialized; + + let data = { + "@context": "https://schema.kosmos.org", + "@type": "Contribution", + "contributor": { + "ipfs": contributorIpfsHash + }, + kind, + description, + "details": {} + }; + + if (url) { + data["url"] = url; + } + + // Write it pretty to ipfs + return JSON.stringify(data, null, 2); + } +} diff --git a/kredits/serializers/contributor.js b/kredits/serializers/contributor.js new file mode 100644 index 0000000..5967903 --- /dev/null +++ b/kredits/serializers/contributor.js @@ -0,0 +1,93 @@ +/** + * Handle serialization for JSON-LD object of the contributor, according to + * https://github.com/67P/kosmos-schemas/blob/master/schemas/contributor.json + * + * @class + * @public + */ +export default class Contributor { + /** + * Deserialize JSON to object + * + * @method + * @public + */ + static deserialize(serialized) { + let { + name, + kind, + url, + accounts, + } = JSON.parse(serialized.toString('utf8')); + + let github_username, github_uid, wiki_username; + let github = accounts.find((a) => a.site === 'github.com'); + let wiki = accounts.find((a) => a.site === 'wiki.kosmos.org'); + + if (github) { + ({ username: github_username, uid: github_uid} = github); + } + if (wiki) { + ({ username: wiki_username } = wiki); + } + + return { + name, + kind, + url, + github_uid, + github_username, + wiki_username, + ipfsData: serialized, + }; + } + + /** + * Serialize object to JSON + * + * @method + * @public + */ + static serialize(deserialized) { + let { + name, + kind, + url, + github_uid, + github_username, + wiki_username, + } = deserialized; + + let data = { + "@context": "https://schema.kosmos.org", + "@type": "Contributor", + kind, + name, + "accounts": [] + }; + + if (url) { + data["url"] = url; + } + + if (github_uid) { + data.accounts.push({ + "site": "github.com", + "uid": github_uid, + "username": github_username, + "url": `https://github.com/${github_username}` + }); + } + + if (wiki_username) { + data.accounts.push({ + "site": "wiki.kosmos.org", + "username": wiki_username, + "url": `https://wiki.kosmos.org/User:${wiki_username}` + }); + } + + // Write it pretty to ipfs + return JSON.stringify(data, null, 2); + } +} diff --git a/kredits/utils/ipfs.js b/kredits/utils/ipfs.js new file mode 100644 index 0000000..42ff47c --- /dev/null +++ b/kredits/utils/ipfs.js @@ -0,0 +1,54 @@ +import ipfsAPI from 'ipfs-api'; +import multihashes from 'multihashes'; + +export default class IPFS { + + constructor(config) { + this._ipfsAPI = ipfsAPI(config); + this._config = config; + } + + catAndMerge(data, deserialize) { + // if no hash details are found simply return the data; nothing to merge + if (!data.hashSize || data.hashSize === 0) { + return data; + } + return this.cat(data) + .then(deserialize) + .then((attributes) => { + return Object.assign({}, data, attributes); + }); + } + + add(data) { + return this._ipfsAPI + .add(new this._ipfsAPI.Buffer(data)) + .then((res) => { + return this.decodeHash(res[0].hash); + }); + } + + cat(hashData) { + let ipfsHash = hashData; // default - if it is a string + if (hashData.hasOwnProperty('hashSize')) { + ipfsHash = this.encodeHash(hashData); + } + return this._ipfsAPI.cat(ipfsHash); + } + + decodeHash(ipfsHash) { + let multihash = multihashes.decode(multihashes.fromB58String(ipfsHash)); + return { + ipfsHash: '0x' + multihashes.toHexString(multihash.digest), + hashSize: multihash.length, + hashFunction: multihash.code, + sourceHash: ipfsHash + }; + } + + encodeHash(hashData) { + let digest = this._ipfsAPI.Buffer.from(hashData.ipfsHash.slice(2), 'hex'); + return multihashes.encode(digest, hashData.hashFunction, hashData.hashSize); + } + +} diff --git a/package.json b/package.json index a56468c..cae0937 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,10 @@ "kredits-wallet-create": "scripts/create-wallet.js" }, "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" + "node-fetch": "^2.1.1", + "prompt": "^1.0.0" }, "repository": { "type": "git",