Implement getDAppDataById to get a dapp with decoded metadata, add more examples for blockchainSDK usage, fix some validations, add support for converting from IPFS hash to bytes32 and vice versa, add error handling in getDAppById method, implement a singleton EmbarkJSService class, check for IPFS Storage availability before IPFS interaction, add instead of using

This commit is contained in:
Lyubomir Kiprov 2019-05-07 18:25:11 +03:00
parent c3920055bb
commit 2b55cc7955
16 changed files with 366 additions and 227 deletions

View File

@ -1,8 +1,8 @@
import utils from './utils'
import SNTService from './sdk/snt-services/snt-service'
import DiscoverService from './sdk/discover-services/discover-service'
import SNTService from './services/contracts-services/snt-service/snt-service'
import DiscoverService from './services/contracts-services/discover-service/discover-service'
import BlockchainConfig from './sdk/config'
import BlockchainConfig from './services/config'
const init = function() {
try {

View File

@ -1,5 +1,7 @@
import bs58 from 'bs58'
export const base64ToBlob = base64Text => {
const byteString = atob(base64Text.split(',')[1])
const byteString = atob(base64Text)
const arrayBuffer = new ArrayBuffer(byteString.length)
const uintArray = new Uint8Array(arrayBuffer)
@ -9,3 +11,18 @@ export const base64ToBlob = base64Text => {
return new Blob([arrayBuffer])
}
export const getBytes32FromIpfsHash = ipfsListing => {
const decodedHash = bs58
.decode(ipfsListing)
.slice(2)
.toString('hex')
return `0x${decodedHash}`
}
export const getIpfsHashFromBytes32 = bytes32Hex => {
const hashHex = `1220${bytes32Hex.slice(2)}`
const hashBytes = Buffer.from(hashHex, 'hex')
const hashStr = bs58.encode(hashBytes)
return hashStr
}

View File

@ -1,15 +1,20 @@
import { base64ToBlob } from './helpers'
import * as helpers from './helpers'
import EmbarkJSService from '../services/embark-service/embark-service'
// Todo: EmbarkJS -> setup it in init
// Todo: Should check for isAvailable
import EmbarkJS from '../../../embarkArtifacts/embarkjs'
EmbarkJS.Storage.setProvider('ipfs')
const checkIPFSAvailability = async () => {
const isAvailable = await EmbarkJSService.Storage.isAvailable()
if (!isAvailable) {
throw new Error('IPFS Storage is unavailable')
}
}
export const uploadMetadata = async metadata => {
try {
const hash = await EmbarkJS.Storage.saveText(metadata)
return hash
await checkIPFSAvailability()
const hash = await EmbarkJSService.Storage.saveText(metadata)
const metadataInBytes = helpers.getBytes32FromIpfsHash(hash)
return metadataInBytes
} catch (error) {
throw new Error(
`Uploading DApp metadata to IPFS failed. Details: ${error.message}`,
@ -17,15 +22,17 @@ export const uploadMetadata = async metadata => {
}
}
// Todo: should convert base64 image into binary data in order to upload it on IPFS
export const uploadImage = async base64Image => {
try {
await checkIPFSAvailability()
const imageFile = [
{
files: [base64ToBlob(base64Image)],
files: [helpers.base64ToBlob(base64Image)],
},
]
const hash = await EmbarkJS.Storage.uploadFile(imageFile)
const hash = await EmbarkJSService.Storage.uploadFile(imageFile)
return hash
} catch (error) {
throw new Error(
@ -34,9 +41,13 @@ export const uploadImage = async base64Image => {
}
}
export const retrieveMetadata = async metadataHash => {
export const retrieveMetadata = async metadataBytes32 => {
try {
const metadata = await EmbarkJS.Storage.get(metadataHash)
await checkIPFSAvailability()
const metadataHash = helpers.getIpfsHashFromBytes32(metadataBytes32)
const metadata = await EmbarkJSService.Storage.get(metadataHash)
return metadata
} catch (error) {
throw new Error(
@ -46,5 +57,6 @@ export const retrieveMetadata = async metadataHash => {
}
export const retrieveImageUrl = async imageHash => {
return EmbarkJS.Storage.getUrl(imageHash)
await checkIPFSAvailability()
return EmbarkJSService.Storage.getUrl(imageHash)
}

View File

@ -1,9 +0,0 @@
export default {
broadcastContractFn: (contractMethod, account) => {
return new Promise(resolve => {
contractMethod({ from: account }).on('transactionHash', hash => {
resolve(hash)
})
})
},
}

View File

@ -1,8 +1,9 @@
/* global web3 */
import Web3 from '../../../embarkArtifacts/modules/web3'
// Should be moved to .env
// Todo: Should be moved to .env
const RPC_URL = 'http://localhost:8545'
export default function() {
global.web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL))
web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL))
}

View File

@ -1,26 +1,28 @@
import EmbarkJS from '../../../embarkArtifacts/embarkjs'
/* global web3 */
import EmbarkJSService from '../embark-service/embark-service'
class BlockchainService {
constructor(sharedContext, contract, Validator) {
this.contract = contract.address
contract.setProvider(global.web3.currentProvider)
contract.setProvider(web3.currentProvider)
this.sharedContext = sharedContext
this.validator = new Validator(this)
}
async __unlockServiceAccount() {
const accounts = await EmbarkJS.Blockchain.Providers.web3.getAccounts()
// if (accounts.length > 0) {
this.sharedContext.account = accounts[0]
// const accounts = await EmbarkJS.Blockchain.Providers.web3.getAccounts()
// // if (accounts.length > 0) {
// this.sharedContext.account = accounts[0]
// } else {
// const provider = global.web3.currentProvider
// Check for undefined
// console.log(await global.web3.eth.getAccounts())
// const accounts = await EmbarkJS.enableEthereum()
// if (accounts) {
// this.sharedContext.account = accounts[0]
// }
const accounts = await EmbarkJSService.enableEthereum()
if (accounts) {
this.sharedContext.account = accounts[0]
}
// global.web3.setProvider(provider)
// }

View File

@ -1,32 +1,27 @@
import broadcastContractFn from '../helpers'
/* global web3 */
import { broadcastContractFn } from '../helpers'
import * as ipfsSDK from '../../ipfs'
import * as ipfsSDK from '../../../ipfs'
import BlockchainService from '../blockchain-service'
import DiscoverValidator from './discover-validator'
import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover'
import DiscoverContract from '../../../../../embarkArtifacts/contracts/Discover'
class DiscoverService extends BlockchainService {
constructor(sharedContext) {
super(sharedContext, DiscoverContract, DiscoverValidator)
}
// TODO: Amount -> string/bigInt/number ?
// TODO: formatBigNumberToNumber
// View methods
async upVoteEffect(id, amount) {
const dapp = await this.getDAppById(id)
await this.validator.validateUpVoteEffect(dapp, id, amount)
await this.validator.validateUpVoteEffect(id, amount)
return DiscoverContract.methods.upvoteEffect(id, amount).call()
}
async downVoteCost(id) {
const dapp = await this.getDAppById(id)
await this.validator.validateDownVoteCost(dapp, id)
return DiscoverContract.methods.upvoteEffect(id).call()
return DiscoverContract.methods.downvoteCost(dapp.id).call()
}
// Todo: Should be implemented
@ -40,13 +35,31 @@ class DiscoverService extends BlockchainService {
// }
async getDAppById(id) {
let dapp
try {
const dappId = await DiscoverContract.methods.id2index(id).call()
const dapp = await DiscoverContract.methods.dapps(dappId).call()
dapp = await DiscoverContract.methods.dapps(dappId).call()
} catch (error) {
throw new Error('Searching DApp does not exists')
}
if (dapp.id != id) {
throw new Error('Error fetching correct data from contract')
}
return dapp
}
async getDAppDataById(id) {
const dapp = await this.getDAppById(id)
try {
dapp.metadata = JSON.parse(await ipfsSDK.retrieveMetadata(dapp.metadata))
dapp.metadata.image = await ipfsSDK.retrieveImageUrl(dapp.metadata.image)
return dapp
} catch (error) {
throw new Error('Searching DApp does not exists')
throw new Error('Error fetching correct data from IPFS')
}
}
@ -61,24 +74,26 @@ class DiscoverService extends BlockchainService {
// Transaction methods
async createDApp(amount, metadata) {
const dappMetadata = JSON.parse(JSON.stringify(metadata))
const dappId = global.web3.keccak256(JSON.stringify(dappMetadata))
const dappId = web3.utils.keccak256(JSON.stringify(dappMetadata))
await this.validator.validateDAppCreation(dappId, amount)
dappMetadata.image = await ipfsSDK.uploadImage(dappMetadata.image)
const metadataHash = await ipfsSDK.uploadMetadata(
const uploadedMetadata = await ipfsSDK.uploadMetadata(
JSON.stringify(dappMetadata),
)
const callData = DiscoverContract.methods
.createDApp(dappId, amount, metadataHash)
.createDApp(dappId, amount, uploadedMetadata)
.encodeABI()
return this.sharedContext.SNTService.approveAndCall(
const createdTx = await this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData,
)
return { tx: createdTx, id: dappId }
}
async upVote(id, amount) {

View File

@ -1,9 +1,3 @@
const checkDappCorrectness = async function(dapp, id) {
if (dapp.id != id) {
throw new Error('Error fetching correct data')
}
}
class DiscoverValidator {
constructor(service) {
this.service = service
@ -11,20 +5,16 @@ class DiscoverValidator {
async validateUpVoteEffect(id, amount) {
const dapp = await this.service.getDAppById(id)
await checkDappCorrectness(dapp, id)
// TODO: should check if dapp.balance is a big number
const safeMax = await this.service.safeMax()
if (dapp.balance + amount > safeMax) {
throw new Error('You cannot upvote by this much, try with a lower amount')
if (Number(dapp.balance) + amount > safeMax) {
throw new Error(
`You cannot upvote by this much, try with a lower amount. Maximum upvote amount:
${Number(safeMax) - Number(dapp.balance)}`,
)
}
}
async validateDownVoteCost(id) {
const dapp = await this.service.getDAppById(id)
await checkDappCorrectness(dapp, id)
}
async validateDAppCreation(id, amount) {
const dappExists = await this.service.isDAppExists(id)
if (dappExists) {
@ -52,19 +42,18 @@ class DiscoverValidator {
}
async validateDownVoting(id, amount) {
await this.validateDownVoteCost(id)
const dapp = await this.service.getDAppById(id)
const downVoteCost = await this.service.downVoteCost(id)
if (downVoteCost != amount) {
throw new Error('Incorrect amount: valid iff effect on ranking is 1%')
const downVoteCost = await this.service.downVoteCost(dapp.id)
if (downVoteCost.c != amount) {
throw new Error('Incorrect amount: valid if effect on ranking is 1%')
}
}
async validateWithdrawing(id, amount) {
const dapp = await this.service.getDAppById(id)
await checkDappCorrectness(dapp, id)
if (dapp.developer != this.service.sharedContext.account) {
if (dapp.developer.toLowerCase() != this.service.sharedContext.account) {
throw new Error('Only the developer can withdraw SNT staked on this data')
}

View File

@ -0,0 +1,7 @@
export const broadcastContractFn = (contractMethod, account) => {
return new Promise(resolve => {
contractMethod({ from: account }).on('transactionHash', hash => {
resolve(hash)
})
})
}

View File

@ -1,9 +1,9 @@
import broadcastContractFn from '../helpers'
import { broadcastContractFn } from '../helpers'
import BlockchainService from '../blockchain-service'
import SNTValidator from './snt-validator'
import SNTToken from '../../../../embarkArtifacts/contracts/SNT'
import SNTToken from '../../../../../embarkArtifacts/contracts/SNT'
class SNTService extends BlockchainService {
constructor(sharedContext) {

View File

@ -0,0 +1,14 @@
import EmbarkJS from '../../../../embarkArtifacts/embarkjs'
class EmbarkService {
constructor() {
if (!EmbarkService.instance) {
EmbarkJS.Storage.setProvider('ipfs')
EmbarkService.instance = EmbarkJS
}
return EmbarkService.instance
}
}
export default new EmbarkService()

View File

@ -1,3 +1,5 @@
/* global web3 */
const TRANSACTION_STATUSES = {
Failed: 0,
Successful: 1,
@ -6,7 +8,7 @@ const TRANSACTION_STATUSES = {
export default {
getTxStatus: async txHash => {
const txReceipt = await global.web3.eth.getTransactionReceipt(txHash)
const txReceipt = await web3.eth.getTransactionReceipt(txHash)
if (txReceipt) {
return txReceipt.status
? TRANSACTION_STATUSES.Successful

View File

@ -1,23 +1,89 @@
import React from 'react'
import exampleImage from './dapp.image'
import BlockchainSDK from '../../common/blockchain'
const SERVICES = BlockchainSDK.init()
const DAPP_DATA = {
name: 'Test1',
url: 'https://www.test1.com/',
description: 'Decentralized Test DApp',
category: 'test',
dateCreated: Date.now(),
image: exampleImage.image,
}
// setTimeout is used in order to wait a transaction to be mined
const getResult = async function(method, params) {
return new Promise((resolve, reject) => {
setTimeout(async () => {
const result = await SERVICES.DiscoverService[method](...params)
resolve(result)
}, 2000)
})
}
/*
Each transaction-function return tx hash
createDApp returns tx hash + dapp id
*/
class Example extends React.Component {
async logDiscoverMethod() {
// const services = await BlockchainSDK.init()
// console.log(await services.SNTService.controller())
// await services.SNTService.generateTokens()
// await services.DiscoverService.createDApp('0x2', 10000, '0x2')
// console.log(await services.DiscoverService.getDAppById('0x2'))
async getFullDApp(id) {
return getResult('getDAppDataById', [id])
}
async createDApp() {
await SERVICES.SNTService.generateTokens()
return SERVICES.DiscoverService.createDApp(10000, DAPP_DATA)
}
async upvote(id) {
return getResult('upVote', [id, 1000])
}
async downvote(id, amount) {
return getResult('downVote', [id, amount])
}
async withdraw(id) {
return getResult('withdraw', [id, 500])
}
async upVoteEffect(id) {
return getResult('upVoteEffect', [id, 10000])
}
async downVoteCost(id) {
return getResult('downVoteCost', [id])
}
async logDiscoverMethods() {
const createdDApp = await this.createDApp()
const downVote = await this.downVoteCost(createdDApp.id)
console.log(
`Downvote TX Hash : ${await this.downvote(createdDApp.id, downVote.c)}`,
)
console.log(`Upvote TX Hash : ${await this.upvote(createdDApp.id)}`)
console.log(`Withdraw TX Hash : ${await this.withdraw(createdDApp.id)}`)
console.log(
`UpvoteEffect Result : ${await this.upVoteEffect(createdDApp.id)}`,
)
console.log(
`DownVoteCost Result : ${await this.downVoteCost(createdDApp.id)}`,
)
const dappData = await this.getFullDApp(createdDApp.id)
document.getElementById('testImage').src = dappData.metadata.image
}
render() {
return (
<div>
<h1 onLoad={this.logDiscoverMethod()} />
<h1 onLoad={this.logDiscoverMethods()} />
<img id="testImage" />
</div>
)
}
}
export default Example
// QmZGzoAEEZoFP9jYXoVfhkDqXHxVrFCSMxSU8eGQpcDNHw

File diff suppressed because one or more lines are too long

View File

@ -1,156 +1,176 @@
/*global assert, web3*/
const bs58 = require('bs58');
/*global assert, web3 */
const bs58 = require('bs58')
// This has been tested with the real Ethereum network and Testrpc.
// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9
exports.assertReverts = (contractMethodCall, maxGasAvailable) => {
return new Promise((resolve, reject) => {
try {
resolve(contractMethodCall());
} catch (error) {
reject(error);
return new Promise((resolve, reject) => {
try {
resolve(contractMethodCall())
} catch (error) {
reject(error)
}
})
.then(tx => {
assert.equal(
tx.receipt.gasUsed,
maxGasAvailable,
'tx successful, the max gas available was not consumed',
)
})
.catch(error => {
if (
String(error).indexOf('invalid opcode') < 0 &&
String(error).indexOf('out of gas') < 0
) {
// Checks if the error is from TestRpc. If it is then ignore it.
// Otherwise relay/throw the error produced by the above assertion.
// Note that no error is thrown when using a real Ethereum network AND the assertion above is true.
throw error
}
})
.then(tx => {
assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed");
})
.catch(error => {
if ((String(error)).indexOf("invalid opcode") < 0 && (String(error)).indexOf("out of gas") < 0) {
// Checks if the error is from TestRpc. If it is then ignore it.
// Otherwise relay/throw the error produced by the above assertion.
// Note that no error is thrown when using a real Ethereum network AND the assertion above is true.
throw error;
}
});
};
exports.listenForEvent = event => new Promise((resolve, reject) => {
}
exports.listenForEvent = event =>
new Promise((resolve, reject) => {
event({}, (error, response) => {
if (!error) {
resolve(response.args);
resolve(response.args)
} else {
reject(error);
reject(error)
}
event.stopWatching();
});
});
exports.eventValues = (receipt, eventName) => {
if (receipt.events[eventName]) return receipt.events[eventName].returnValues;
};
exports.addressToBytes32 = (address) => {
const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2);
return "0x" + stringed.substring(stringed.length - 64, stringed.length);
};
// OpenZeppelin's expectThrow helper -
// Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js
exports.expectThrow = async promise => {
try {
await promise;
} catch (error) {
// TODO: Check jump destination to destinguish between a throw
// and an actual invalid jump.
const invalidOpcode = error.message.search('invalid opcode') >= 0;
// TODO: When we contract A calls contract B, and B throws, instead
// of an 'invalid jump', we get an 'out of gas' error. How do
// we distinguish this from an actual out of gas event? (The
// testrpc log actually show an 'invalid jump' event.)
const outOfGas = error.message.search('out of gas') >= 0;
const revert = error.message.search('revert') >= 0;
assert(
invalidOpcode || outOfGas || revert,
'Expected throw, got \'' + error + '\' instead',
);
return;
}
assert.fail('Expected throw not received');
};
exports.assertJump = (error) => {
assert(error.message.search('VM Exception while processing transaction: revert') > -1, 'Revert should happen');
};
function callbackToResolve(resolve, reject) {
return function(error, value) {
if (error) {
reject(error);
} else {
resolve(value);
}
};
}
exports.promisify = (func) =>
(...args) => {
return new Promise((resolve, reject) => {
const callback = (err, data) => err ? reject(err) : resolve(data);
func.apply(this, [...args, callback]);
});
};
exports.zeroAddress = '0x0000000000000000000000000000000000000000';
exports.zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
exports.timeUnits = {
seconds: 1,
minutes: 60,
hours: 60 * 60,
days: 24 * 60 * 60,
weeks: 7 * 24 * 60 * 60,
years: 365 * 24 * 60 * 60
};
exports.ensureException = function(error) {
assert(isException(error), error.toString());
};
function isException(error) {
let strError = error.toString();
return strError.includes('invalid opcode') || strError.includes('invalid JUMP') || strError.includes('revert');
}
const evmMethod = (method, params = []) => {
return new Promise(function(resolve, reject) {
const sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider);
sendMethod(
{
jsonrpc: '2.0',
method,
params,
id: new Date().getSeconds()
},
(error, res) => {
if (error) {
return reject(error);
}
resolve(res.result);
}
);
});
};
exports.evmSnapshot = async () => {
const result = await evmMethod("evm_snapshot");
return web3.utils.hexToNumber(result);
};
exports.evmRevert = (id) => {
const params = [id];
return evmMethod("evm_revert", params);
};
exports.increaseTime = async (amount) => {
await evmMethod("evm_increaseTime", [Number(amount)]);
await evmMethod("evm_mine");
};
event.stopWatching()
})
})
exports.eventValues = (receipt, eventName) => {
if (receipt.events[eventName]) return receipt.events[eventName].returnValues
}
exports.addressToBytes32 = address => {
const stringed =
'0000000000000000000000000000000000000000000000000000000000000000' +
address.slice(2)
return `0x${ stringed.substring(stringed.length - 64, stringed.length)}`;
}
// OpenZeppelin's expectThrow helper -
// Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js
exports.expectThrow = async promise => {
try {
await promise
} catch (error) {
// TODO: Check jump destination to destinguish between a throw
// and an actual invalid jump.
const invalidOpcode = error.message.search('invalid opcode') >= 0
// TODO: When we contract A calls contract B, and B throws, instead
// of an 'invalid jump', we get an 'out of gas' error. How do
// we distinguish this from an actual out of gas event? (The
// testrpc log actually show an 'invalid jump' event.)
const outOfGas = error.message.search('out of gas') >= 0
const revert = error.message.search('revert') >= 0
assert(
invalidOpcode || outOfGas || revert,
`Expected throw, got '${ error }' instead`,
)
return
}
assert.fail('Expected throw not received')
}
exports.assertJump = error => {
assert(
error.message.search('VM Exception while processing transaction: revert') >
-1,
'Revert should happen',
)
}
function callbackToResolve(resolve, reject) {
return function(error, value) {
if (error) {
reject(error)
} else {
resolve(value)
}
}
}
exports.promisify = func => (...args) => {
return new Promise((resolve, reject) => {
const callback = (err, data) => (err ? reject(err) : resolve(data))
func.apply(this, [...args, callback])
})
}
exports.zeroAddress = '0x0000000000000000000000000000000000000000'
exports.zeroBytes32 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
exports.timeUnits = {
seconds: 1,
minutes: 60,
hours: 60 * 60,
days: 24 * 60 * 60,
weeks: 7 * 24 * 60 * 60,
years: 365 * 24 * 60 * 60,
}
exports.ensureException = function(error) {
assert(isException(error), error.toString())
}
function isException(error) {
const strError = error.toString()
return (
strError.includes('invalid opcode') ||
strError.includes('invalid JUMP') ||
strError.includes('revert')
)
}
const evmMethod = (method, params = []) => {
return new Promise(function(resolve, reject) {
const sendMethod = web3.currentProvider.sendAsync
? web3.currentProvider.sendAsync.bind(web3.currentProvider)
: web3.currentProvider.send.bind(web3.currentProvider)
sendMethod(
{
jsonrpc: '2.0',
method,
params,
id: new Date().getSeconds(),
},
(error, res) => {
if (error) {
return reject(error)
}
resolve(res.result)
},
)
})
}
exports.evmSnapshot = async () => {
const result = await evmMethod('evm_snapshot')
return web3.utils.hexToNumber(result)
}
exports.evmRevert = id => {
const params = [id]
return evmMethod('evm_revert', params)
}
exports.increaseTime = async amount => {
await evmMethod('evm_increaseTime', [Number(amount)])
await evmMethod('evm_mine')
}
exports.getBytes32FromIpfsHash = ipfsListing => {
const decodedHash = bs58.decode(ipfsListing).slice(2).toString('hex')
const decodedHash = bs58
.decode(ipfsListing)
.slice(2)
.toString('hex')
return `0x${decodedHash}`
}
@ -159,4 +179,4 @@ exports.getIpfsHashFromBytes32 = bytes32Hex => {
const hashBytes = Buffer.from(hashHex, 'hex')
const hashStr = bs58.encode(hashBytes)
return hashStr
}
}