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 utils from './utils'
import SNTService from './sdk/snt-services/snt-service' import SNTService from './services/contracts-services/snt-service/snt-service'
import DiscoverService from './sdk/discover-services/discover-service' import DiscoverService from './services/contracts-services/discover-service/discover-service'
import BlockchainConfig from './sdk/config' import BlockchainConfig from './services/config'
const init = function() { const init = function() {
try { try {

View File

@ -1,5 +1,7 @@
import bs58 from 'bs58'
export const base64ToBlob = base64Text => { export const base64ToBlob = base64Text => {
const byteString = atob(base64Text.split(',')[1]) const byteString = atob(base64Text)
const arrayBuffer = new ArrayBuffer(byteString.length) const arrayBuffer = new ArrayBuffer(byteString.length)
const uintArray = new Uint8Array(arrayBuffer) const uintArray = new Uint8Array(arrayBuffer)
@ -9,3 +11,18 @@ export const base64ToBlob = base64Text => {
return new Blob([arrayBuffer]) 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 const checkIPFSAvailability = async () => {
// Todo: Should check for isAvailable const isAvailable = await EmbarkJSService.Storage.isAvailable()
import EmbarkJS from '../../../embarkArtifacts/embarkjs' if (!isAvailable) {
throw new Error('IPFS Storage is unavailable')
EmbarkJS.Storage.setProvider('ipfs') }
}
export const uploadMetadata = async metadata => { export const uploadMetadata = async metadata => {
try { try {
const hash = await EmbarkJS.Storage.saveText(metadata) await checkIPFSAvailability()
return hash
const hash = await EmbarkJSService.Storage.saveText(metadata)
const metadataInBytes = helpers.getBytes32FromIpfsHash(hash)
return metadataInBytes
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Uploading DApp metadata to IPFS failed. Details: ${error.message}`, `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 => { export const uploadImage = async base64Image => {
try { try {
await checkIPFSAvailability()
const imageFile = [ 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 return hash
} catch (error) { } catch (error) {
throw new Error( throw new Error(
@ -34,9 +41,13 @@ export const uploadImage = async base64Image => {
} }
} }
export const retrieveMetadata = async metadataHash => { export const retrieveMetadata = async metadataBytes32 => {
try { try {
const metadata = await EmbarkJS.Storage.get(metadataHash) await checkIPFSAvailability()
const metadataHash = helpers.getIpfsHashFromBytes32(metadataBytes32)
const metadata = await EmbarkJSService.Storage.get(metadataHash)
return metadata return metadata
} catch (error) { } catch (error) {
throw new Error( throw new Error(
@ -46,5 +57,6 @@ export const retrieveMetadata = async metadataHash => {
} }
export const retrieveImageUrl = async imageHash => { 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' import Web3 from '../../../embarkArtifacts/modules/web3'
// Should be moved to .env // Todo: Should be moved to .env
const RPC_URL = 'http://localhost:8545' const RPC_URL = 'http://localhost:8545'
export default function() { 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 { class BlockchainService {
constructor(sharedContext, contract, Validator) { constructor(sharedContext, contract, Validator) {
this.contract = contract.address this.contract = contract.address
contract.setProvider(global.web3.currentProvider) contract.setProvider(web3.currentProvider)
this.sharedContext = sharedContext this.sharedContext = sharedContext
this.validator = new Validator(this) this.validator = new Validator(this)
} }
async __unlockServiceAccount() { async __unlockServiceAccount() {
const accounts = await EmbarkJS.Blockchain.Providers.web3.getAccounts() // const accounts = await EmbarkJS.Blockchain.Providers.web3.getAccounts()
// if (accounts.length > 0) { // // if (accounts.length > 0) {
this.sharedContext.account = accounts[0] // this.sharedContext.account = accounts[0]
// } else { // } else {
// const provider = global.web3.currentProvider // const provider = global.web3.currentProvider
// Check for undefined // Check for undefined
// console.log(await global.web3.eth.getAccounts()) // console.log(await global.web3.eth.getAccounts())
// const accounts = await EmbarkJS.enableEthereum() const accounts = await EmbarkJSService.enableEthereum()
// if (accounts) { if (accounts) {
// this.sharedContext.account = accounts[0] this.sharedContext.account = accounts[0]
// } }
// global.web3.setProvider(provider) // 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 BlockchainService from '../blockchain-service'
import DiscoverValidator from './discover-validator' import DiscoverValidator from './discover-validator'
import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' import DiscoverContract from '../../../../../embarkArtifacts/contracts/Discover'
class DiscoverService extends BlockchainService { class DiscoverService extends BlockchainService {
constructor(sharedContext) { constructor(sharedContext) {
super(sharedContext, DiscoverContract, DiscoverValidator) super(sharedContext, DiscoverContract, DiscoverValidator)
} }
// TODO: Amount -> string/bigInt/number ?
// TODO: formatBigNumberToNumber
// View methods // View methods
async upVoteEffect(id, amount) { async upVoteEffect(id, amount) {
const dapp = await this.getDAppById(id) await this.validator.validateUpVoteEffect(id, amount)
await this.validator.validateUpVoteEffect(dapp, id, amount)
return DiscoverContract.methods.upvoteEffect(id, amount).call() return DiscoverContract.methods.upvoteEffect(id, amount).call()
} }
async downVoteCost(id) { async downVoteCost(id) {
const dapp = await this.getDAppById(id) const dapp = await this.getDAppById(id)
await this.validator.validateDownVoteCost(dapp, id) return DiscoverContract.methods.downvoteCost(dapp.id).call()
return DiscoverContract.methods.upvoteEffect(id).call()
} }
// Todo: Should be implemented // Todo: Should be implemented
@ -40,13 +35,31 @@ class DiscoverService extends BlockchainService {
// } // }
async getDAppById(id) { async getDAppById(id) {
let dapp
try { try {
const dappId = await DiscoverContract.methods.id2index(id).call() 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 return dapp
} catch (error) { } 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 // Transaction methods
async createDApp(amount, metadata) { async createDApp(amount, metadata) {
const dappMetadata = JSON.parse(JSON.stringify(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) await this.validator.validateDAppCreation(dappId, amount)
dappMetadata.image = await ipfsSDK.uploadImage(dappMetadata.image) dappMetadata.image = await ipfsSDK.uploadImage(dappMetadata.image)
const metadataHash = await ipfsSDK.uploadMetadata( const uploadedMetadata = await ipfsSDK.uploadMetadata(
JSON.stringify(dappMetadata), JSON.stringify(dappMetadata),
) )
const callData = DiscoverContract.methods const callData = DiscoverContract.methods
.createDApp(dappId, amount, metadataHash) .createDApp(dappId, amount, uploadedMetadata)
.encodeABI() .encodeABI()
return this.sharedContext.SNTService.approveAndCall( const createdTx = await this.sharedContext.SNTService.approveAndCall(
this.contract, this.contract,
amount, amount,
callData, callData,
) )
return { tx: createdTx, id: dappId }
} }
async upVote(id, amount) { 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 { class DiscoverValidator {
constructor(service) { constructor(service) {
this.service = service this.service = service
@ -11,20 +5,16 @@ class DiscoverValidator {
async validateUpVoteEffect(id, amount) { async validateUpVoteEffect(id, amount) {
const dapp = await this.service.getDAppById(id) 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() const safeMax = await this.service.safeMax()
if (dapp.balance + amount > safeMax) { if (Number(dapp.balance) + amount > safeMax) {
throw new Error('You cannot upvote by this much, try with a lower amount') 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) { async validateDAppCreation(id, amount) {
const dappExists = await this.service.isDAppExists(id) const dappExists = await this.service.isDAppExists(id)
if (dappExists) { if (dappExists) {
@ -52,19 +42,18 @@ class DiscoverValidator {
} }
async validateDownVoting(id, amount) { async validateDownVoting(id, amount) {
await this.validateDownVoteCost(id) const dapp = await this.service.getDAppById(id)
const downVoteCost = await this.service.downVoteCost(id) const downVoteCost = await this.service.downVoteCost(dapp.id)
if (downVoteCost != amount) { if (downVoteCost.c != amount) {
throw new Error('Incorrect amount: valid iff effect on ranking is 1%') throw new Error('Incorrect amount: valid if effect on ranking is 1%')
} }
} }
async validateWithdrawing(id, amount) { async validateWithdrawing(id, amount) {
const dapp = await this.service.getDAppById(id) 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') 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 BlockchainService from '../blockchain-service'
import SNTValidator from './snt-validator' import SNTValidator from './snt-validator'
import SNTToken from '../../../../embarkArtifacts/contracts/SNT' import SNTToken from '../../../../../embarkArtifacts/contracts/SNT'
class SNTService extends BlockchainService { class SNTService extends BlockchainService {
constructor(sharedContext) { 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 = { const TRANSACTION_STATUSES = {
Failed: 0, Failed: 0,
Successful: 1, Successful: 1,
@ -6,7 +8,7 @@ const TRANSACTION_STATUSES = {
export default { export default {
getTxStatus: async txHash => { getTxStatus: async txHash => {
const txReceipt = await global.web3.eth.getTransactionReceipt(txHash) const txReceipt = await web3.eth.getTransactionReceipt(txHash)
if (txReceipt) { if (txReceipt) {
return txReceipt.status return txReceipt.status
? TRANSACTION_STATUSES.Successful ? TRANSACTION_STATUSES.Successful

View File

@ -1,23 +1,89 @@
import React from 'react' import React from 'react'
import exampleImage from './dapp.image'
import BlockchainSDK from '../../common/blockchain' 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 { class Example extends React.Component {
async logDiscoverMethod() { async getFullDApp(id) {
// const services = await BlockchainSDK.init() return getResult('getDAppDataById', [id])
// console.log(await services.SNTService.controller()) }
// await services.SNTService.generateTokens()
// await services.DiscoverService.createDApp('0x2', 10000, '0x2') async createDApp() {
// console.log(await services.DiscoverService.getDAppById('0x2')) 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() { render() {
return ( return (
<div> <div>
<h1 onLoad={this.logDiscoverMethod()} /> <h1 onLoad={this.logDiscoverMethods()} />
<img id="testImage" />
</div> </div>
) )
} }
} }
export default Example export default Example
// QmZGzoAEEZoFP9jYXoVfhkDqXHxVrFCSMxSU8eGQpcDNHw

File diff suppressed because one or more lines are too long

View File

@ -1,156 +1,176 @@
/*global assert, web3*/ /*global assert, web3 */
const bs58 = require('bs58'); const bs58 = require('bs58')
// This has been tested with the real Ethereum network and Testrpc. // This has been tested with the real Ethereum network and Testrpc.
// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9 // Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9
exports.assertReverts = (contractMethodCall, maxGasAvailable) => { exports.assertReverts = (contractMethodCall, maxGasAvailable) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
resolve(contractMethodCall()); resolve(contractMethodCall())
} catch (error) { } catch (error) {
reject(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");
}) exports.listenForEvent = event =>
.catch(error => { new Promise((resolve, reject) => {
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) => {
event({}, (error, response) => { event({}, (error, response) => {
if (!error) { if (!error) {
resolve(response.args); resolve(response.args)
} else { } else {
reject(error); reject(error)
} }
event.stopWatching(); 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");
};
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 => { exports.getBytes32FromIpfsHash = ipfsListing => {
const decodedHash = bs58.decode(ipfsListing).slice(2).toString('hex') const decodedHash = bs58
.decode(ipfsListing)
.slice(2)
.toString('hex')
return `0x${decodedHash}` return `0x${decodedHash}`
} }
@ -159,4 +179,4 @@ exports.getIpfsHashFromBytes32 = bytes32Hex => {
const hashBytes = Buffer.from(hashHex, 'hex') const hashBytes = Buffer.from(hashHex, 'hex')
const hashStr = bs58.encode(hashBytes) const hashStr = bs58.encode(hashBytes)
return hashStr return hashStr
} }