Merge branch 'develop' of https://github.com/bakasura980/discover-dapps into develop

This commit is contained in:
Lyubomir Kiprov 2019-05-09 12:22:38 +03:00
commit b566c248bf
27 changed files with 972 additions and 242 deletions

View File

@ -1,10 +1,21 @@
{
"extends": ["airbnb", "plugin:prettier/recommended"],
"plugins": ["prettier"],
"extends": [
"airbnb",
"plugin:prettier/recommended"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": ["error", {
"endOfLine":"auto"
}]
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"func-names": "off",
"eqeqeq": "off",
"class-methods-use-this": "off"
},
"env": {
"browser": true,
@ -14,4 +25,4 @@
"parserOptions": {
"ecmaVersion": 9
}
}
}

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.embark
chains.json
config/development/mnemonic
config/livenet/password
config/production/password
coverage

View File

@ -2,105 +2,104 @@ module.exports = {
// applies to all environments
default: {
enabled: true,
rpcHost: "localhost", // HTTP-RPC server listening interface (default: "localhost")
rpcHost: 'localhost', // HTTP-RPC server listening interface (default: "localhost")
rpcPort: 8545, // HTTP-RPC server listening port (default: 8545)
rpcCorsDomain: { // Domains from which to accept cross origin requests (browser enforced). This can also be a comma separated list
rpcCorsDomain: {
// Domains from which to accept cross origin requests (browser enforced). This can also be a comma separated list
auto: true, // When "auto" is true, Embark will automatically set the cors to the address of the webserver
additionalCors: [] // Additional CORS domains to add to the list. If "auto" is false, only those will be added
additionalCors: [], // Additional CORS domains to add to the list. If "auto" is false, only those will be added
},
wsRPC: true, // Enable the WS-RPC server
wsOrigins: { // Same thing as "rpcCorsDomain", but for WS origins
wsOrigins: {
// Same thing as "rpcCorsDomain", but for WS origins
auto: true,
additionalCors: []
additionalCors: [],
},
wsHost: "localhost", // WS-RPC server listening interface (default: "localhost")
wsPort: 8546 // WS-RPC server listening port (default: 8546)
wsHost: 'localhost', // WS-RPC server listening interface (default: "localhost")
wsPort: 8546, // WS-RPC server listening port (default: 8546)
// Accounts to use as node accounts
// The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount`
/*,accounts: [
{
nodeAccounts: true, // Accounts use for the node
numAddresses: "1", // Number of addresses/accounts (defaults to 1)
password: "config/development/devpassword" // Password file for the accounts
},
// Below are additional accounts that will count as `nodeAccounts` in the `deployment` section of your contract config
// Those will not be unlocked in the node itself
{
privateKey: "your_private_key"
},
{
privateKeyFile: "path/to/file", // Either a keystore or a list of keys, separated by , or ;
password: "passwordForTheKeystore" // Needed to decrypt the keystore file
},
{
mnemonic: "12 word mnemonic",
addressIndex: "0", // Optional. The index to start getting the address
numAddresses: "1", // Optional. The number of addresses to get
hdpath: "m/44'/60'/0'/0/" // Optional. HD derivation path
}
]*/
// accounts: [
// {
// nodeAccounts: true, // Accounts use for the node
// numAddresses: '1', // Number of addresses/accounts (defaults to 1)
// password: 'config/development/password', // Password file for the accounts
// },
// Below are additional accounts that will count as `nodeAccounts` in the `deployment` section of your contract config
// Those will not be unlocked in the node itself
// {
// privateKeyFile: 'path/to/file', // Either a keystore or a list of keys, separated by , or ;
// password: 'passwordForTheKeystore', // Needed to decrypt the keystore file
// },
// {
// mnemonic: '12 word mnemonic',
// addressIndex: '0', // Optional. The index to start getting the address
// numAddresses: '1', // Optional. The number of addresses to get
// hdpath: "m/44'/60'/0'/0/", // Optional. HD derivation path
// },
// ],
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run` and `embark blockchain`
development: {
ethereumClientName: "geth", // Can be geth or parity (default:geth)
//ethereumClientBin: "geth", // path to the client binary. Useful if it is not in the global PATH
networkType: "custom", // Can be: testnet, rinkeby, livenet or custom, in which case, it will use the specified networkId
ethereumClientName: 'geth', // Can be geth or parity (default:geth)
// ethereumClientBin: "geth", // path to the client binary. Useful if it is not in the global PATH
networkType: 'custom', // Can be: testnet, rinkeby, livenet or custom, in which case, it will use the specified networkId
networkId: 1337, // Network id used when networkType is custom
isDev: true, // Uses and ephemeral proof-of-authority network with a pre-funded developer account, mining enabled
datadir: ".embark/development/datadir", // Data directory for the databases and keystore (Geth 1.8.15 and Parity 2.0.4 can use the same base folder, till now they does not conflict with each other)
datadir: '.embark/development/datadir', // Data directory for the databases and keystore (Geth 1.8.15 and Parity 2.0.4 can use the same base folder, till now they does not conflict with each other)
mineWhenNeeded: true, // Uses our custom script (if isDev is false) to mine only when needed
nodiscover: true, // Disables the peer discovery mechanism (manual peer addition)
maxpeers: 0, // Maximum number of network peers (network disabled if set to 0) (default: 25)
proxy: true, // Proxy is used to present meaningful information about transactions
targetGasLimit: 9000000, // Target gas limit sets the artificial target gas floor for the blocks to mine
simulatorBlocktime: 0 // Specify blockTime in seconds for automatic mining. Default is 0 and no auto-mining.
simulatorBlocktime: 0, // Specify blockTime in seconds for automatic mining. Default is 0 and no auto-mining.
},
// merges with the settings in default
// used with "embark run privatenet" and/or "embark blockchain privatenet"
privatenet: {
networkType: "custom",
networkType: 'custom',
networkId: 1337,
isDev: false,
datadir: ".embark/privatenet/datadir",
datadir: '.embark/privatenet/datadir',
// -- mineWhenNeeded --
// This options is only valid when isDev is false.
// This options is only valid when isDev is false.
// Enabling this option uses our custom script to mine only when needed.
// Embark creates a development account for you (using `geth account new`) and funds the account. This account can be used for
// development (and even imported in to MetaMask). To enable correct usage, a password for this account must be specified
// in the `account > password` setting below.
// NOTE: once `mineWhenNeeded` is enabled, you must run an `embark reset` on your dApp before running
// `embark blockchain` or `embark run` for the first time.
mineWhenNeeded: true,
mineWhenNeeded: true,
// -- genesisBlock --
// This option is only valid when mineWhenNeeded is true (which is only valid if isDev is false).
// When enabled, geth uses POW to mine transactions as it would normally, instead of using POA as it does in --dev mode.
// On the first `embark blockchain or embark run` after this option is enabled, geth will create a new chain with a
// On the first `embark blockchain or embark run` after this option is enabled, geth will create a new chain with a
// genesis block, which can be configured using the `genesisBlock` configuration option below.
genesisBlock: "config/privatenet/genesis.json", // Genesis block to initiate on first creation of a development node
genesisBlock: 'config/privatenet/genesis.json', // Genesis block to initiate on first creation of a development node
nodiscover: true,
maxpeers: 0,
proxy: true,
accounts: [
{
nodeAccounts: true,
password: "config/privatenet/password" // Password to unlock the account
}
password: 'config/privatenet/password', // Password to unlock the account
},
],
targetGasLimit: 8000000,
simulatorBlocktime: 0
simulatorBlocktime: 0,
},
privateparitynet: {
ethereumClientName: "parity",
networkType: "custom",
ethereumClientName: 'parity',
networkType: 'custom',
networkId: 1337,
isDev: false,
genesisBlock: "config/privatenet/genesis-parity.json", // Genesis block to initiate on first creation of a development node
datadir: ".embark/privatenet/datadir",
genesisBlock: 'config/privatenet/genesis-parity.json', // Genesis block to initiate on first creation of a development node
datadir: '.embark/privatenet/datadir',
mineWhenNeeded: false,
nodiscover: true,
maxpeers: 0,
@ -108,43 +107,43 @@ module.exports = {
accounts: [
{
nodeAccounts: true,
password: "config/privatenet/password"
}
password: 'config/privatenet/password',
},
],
targetGasLimit: 8000000,
simulatorBlocktime: 0
simulatorBlocktime: 0,
},
// merges with the settings in default
// used with "embark run testnet" and/or "embark blockchain testnet"
testnet: {
networkType: "testnet",
syncMode: "light",
networkType: 'testnet',
syncMode: 'light',
accounts: [
{
nodeAccounts: true,
password: "config/testnet/password"
}
]
password: 'config/testnet/password',
},
],
},
// merges with the settings in default
// used with "embark run livenet" and/or "embark blockchain livenet"
livenet: {
networkType: "livenet",
syncMode: "light",
rpcCorsDomain: "http://localhost:8000",
wsOrigins: "http://localhost:8000",
networkType: 'livenet',
syncMode: 'light',
rpcCorsDomain: 'http://localhost:8000',
wsOrigins: 'http://localhost:8000',
accounts: [
{
nodeAccounts: true,
password: "config/livenet/password"
}
]
}
password: 'config/livenet/password',
},
],
},
// you can name an environment with specific settings and then specify with
// "embark run custom_name" or "embark blockchain custom_name"
//custom_name: {
//}
};
// custom_name: {
// }
}

View File

@ -1,14 +1,16 @@
const wallet = require('./development/mnemonic')
module.exports = {
// default applies to all environments
default: {
// Blockchain node to deploy the contracts
deployment: {
host: "localhost", // Host of the blockchain node
port: 8546, // Port of the blockchain node
type: "ws" // Type of connection (ws or rpc),
host: 'localhost', // Host of the blockchain node
port: 8545, // Port of the blockchain node
type: 'rpc', // Type of connection (ws or rpc),
// Accounts to use instead of the default account to populate your wallet
// The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount`
/*,accounts: [
/* ,accounts: [
{
privateKey: "your_private_key",
balance: "5 ether" // You can set the balance of the account in the dev environment
@ -27,13 +29,20 @@ module.exports = {
{
"nodeAccounts": true // Uses the Ethereum node's accounts
}
]*/
] */
accounts: [
{
mnemonic: wallet.mnemonic,
balance: '1534983463450 ether',
},
],
},
// order of connections the dapp should connect to
dappConnection: [
"$WEB3", // uses pre existing web3 object if available (e.g in Mist)
"ws://localhost:8546",
"http://localhost:8545"
'$WEB3', // uses pre existing web3 object if available (e.g in Mist)
'ws://localhost:8546',
'http://localhost:8545',
],
// Automatically call `ethereum.enable` if true.
@ -41,7 +50,7 @@ module.exports = {
// Default value is true.
// dappAutoEnable: true,
gas: "auto",
gas: 'auto',
// Strategy for the deployment of the contracts:
// - implicit will try to deploy all the contracts located inside the contracts directory
@ -49,44 +58,62 @@ module.exports = {
// when not specified
// - explicit will only attempt to deploy the contracts that are explicitly specified inside the
// contracts section.
//strategy: 'implicit',
// strategy: 'implicit',
// contracts: {
// Discover: {
// args: { _SNT: '0x744d70fdbe2ba4cf95131626614a1763df805b9e' },
// },
// MiniMeToken: { deploy: false },
// TestBancorFormula: { deploy: false },
// },
contracts: {
Discover: {
args: { _SNT: "0x744d70fdbe2ba4cf95131626614a1763df805b9e" }
MiniMeToken: { deploy: false },
TestBancorFormula: { deploy: false },
MiniMeTokenFactory: {},
SNT: {
instanceOf: 'MiniMeToken',
args: [
'$MiniMeTokenFactory',
'0x0000000000000000000000000000000000000000',
0,
'TestMiniMeToken',
18,
'SNT',
true,
],
},
MiniMeToken: { "deploy": false },
TestBancorFormula: { "deploy": false }
}
Discover: {
args: ['$SNT'],
},
},
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run`
development: {
dappConnection: [
"ws://localhost:8546",
"http://localhost:8545",
"$WEB3" // uses pre existing web3 object if available (e.g in Mist)
]
'ws://localhost:8546',
'http://localhost:8545',
'$WEB3', // uses pre existing web3 object if available (e.g in Mist)
],
},
// merges with the settings in default
// used with "embark run privatenet"
privatenet: {
},
privatenet: {},
// merges with the settings in default
// used with "embark run testnet"
testnet: {
},
testnet: {},
// merges with the settings in default
// used with "embark run livenet"
livenet: {
},
livenet: {},
// you can name an environment with specific settings and then specify with
// "embark run custom_name" or "embark blockchain custom_name"
//custom_name: {
//}
};
// custom_name: {
// }
}

View File

@ -0,0 +1,2 @@
module.exports.mnemonic =
'artefact rebuild liquid honey sport clean candy motor cereal job gap series'

View File

@ -42,7 +42,7 @@ contract Discover is ApproveAndCallFallBack, BancorFormula {
Data[] public dapps;
mapping(bytes32 => uint) public id2index;
mapping(bytes32 => bool) existingIDs;
mapping(bytes32 => bool) public existingIDs;
event DAppCreated(bytes32 indexed id, uint newEffectiveBalance);
event Upvote(bytes32 indexed id, uint newEffectiveBalance);
@ -237,6 +237,15 @@ contract Discover is ApproveAndCallFallBack, BancorFormula {
return (mEBalance.sub(d.effectiveBalance));
}
/**
* @dev Used in UI in order to fetch all dapps
* @return dapps count
*/
function getDAppsCount() external view returns(uint) {
return dapps.length;
}
/**
* @dev Downvotes always remove 1% of the current ranking.
* @param _id bytes32 unique identifier.

View File

@ -22,5 +22,5 @@
"optimize-runs": 200
}
},
"generationDir": "embarkArtifacts"
"generationDir": "src/embarkArtifacts"
}

View File

@ -4,12 +4,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/runtime-corejs2": "^7.4.3",
"@trailofbits/embark-contract-info": "^1.0.0",
"bignumber.js": "^8.1.1",
"bs58": "^4.0.1",
"connected-react-router": "^6.3.2",
"debounce": "^1.2.0",
"decimal.js": "^10.0.2",
"embark": "^4.0.2",
"embark-solium": "0.0.1",
"history": "^4.7.2",
"moment": "^2.24.0",

View File

@ -0,0 +1,28 @@
import utils from './utils'
import SNTService from './services/contracts-services/snt-service/snt-service'
import DiscoverService from './services/contracts-services/discover-service/discover-service'
import BlockchainConfig from './services/config'
const init = function() {
try {
BlockchainConfig()
const sharedContext = {
account: '',
}
sharedContext.SNTService = new SNTService(sharedContext)
sharedContext.DiscoverService = new DiscoverService(sharedContext)
return {
SNTService: sharedContext.SNTService,
DiscoverService: sharedContext.DiscoverService,
utils,
}
} catch (error) {
throw new Error(error.message)
}
}
export default { init, utils }

View File

@ -0,0 +1,28 @@
import bs58 from 'bs58'
export const base64ToBlob = base64Text => {
const byteString = atob(base64Text)
const arrayBuffer = new ArrayBuffer(byteString.length)
const uintArray = new Uint8Array(arrayBuffer)
for (let i = 0; i < byteString.length; i++) {
uintArray[i] = byteString.charCodeAt(i)
}
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

@ -0,0 +1,63 @@
import * as helpers from './helpers'
import EmbarkJSService from '../services/embark-service/embark-service'
const checkIPFSAvailability = async () => {
const isAvailable = await EmbarkJSService.Storage.isAvailable()
if (!isAvailable) {
throw new Error('IPFS Storage is unavailable')
}
}
const uploadImage = async base64Image => {
const imageFile = [
{
files: [helpers.base64ToBlob(base64Image)],
},
]
return EmbarkJSService.Storage.uploadFile(imageFile)
}
const uploadMetadata = async metadata => {
const hash = await EmbarkJSService.Storage.saveText(metadata)
return helpers.getBytes32FromIpfsHash(hash)
}
export const uploadDAppMetadata = async metadata => {
try {
await checkIPFSAvailability()
metadata.image = await uploadImage(metadata.image)
const uploadedMetadataHash = await uploadMetadata(JSON.stringify(metadata))
return uploadedMetadataHash
} catch (error) {
throw new Error(
`Uploading DApp metadata to IPFS failed. Details: ${error.message}`,
)
}
}
const retrieveMetadata = async metadataBytes32 => {
const metadataHash = helpers.getIpfsHashFromBytes32(metadataBytes32)
return EmbarkJSService.Storage.get(metadataHash)
}
const retrieveImageUrl = async imageHash => {
return EmbarkJSService.Storage.getUrl(imageHash)
}
export const retrieveDAppMetadataByHash = async metadataBytes32 => {
try {
await checkIPFSAvailability()
const metadata = JSON.parse(await retrieveMetadata(metadataBytes32))
metadata.image = await retrieveImageUrl(metadata.image)
return metadata
} catch (error) {
throw new Error(
`Fetching metadata from IPFS failed. Details: ${error.message}`,
)
}
}

View File

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

View File

@ -0,0 +1,33 @@
/* global web3 */
import EmbarkJSService from '../embark-service/embark-service'
class BlockchainService {
constructor(sharedContext, contract, Validator) {
this.contract = contract.address
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]
// } else {
// const provider = global.web3.currentProvider
// Check for undefined
// console.log(await global.web3.eth.getAccounts())
const accounts = await EmbarkJSService.enableEthereum()
if (accounts) {
this.sharedContext.account = accounts[0]
}
// global.web3.setProvider(provider)
// }
// throw new Error('Could not unlock an account or web3 is missing')
}
}
export default BlockchainService

View File

@ -0,0 +1,148 @@
/* global web3 */
import { broadcastContractFn } from '../helpers'
import * as ipfsSDK from '../../../ipfs'
import BlockchainService from '../blockchain-service'
import DiscoverValidator from './discover-validator'
import DiscoverContract from '../../../../../embarkArtifacts/contracts/Discover'
class DiscoverService extends BlockchainService {
constructor(sharedContext) {
super(sharedContext, DiscoverContract, DiscoverValidator)
}
// View methods
async upVoteEffect(id, amount) {
await this.validator.validateUpVoteEffect(id, amount)
return DiscoverContract.methods.upvoteEffect(id, amount).call()
}
async downVoteCost(id) {
const dapp = await this.getDAppById(id)
return DiscoverContract.methods.downvoteCost(dapp.id).call()
}
// Todo: Should be implemented
// async getDApps() {
// const dapps = []
// const dappsCount = await DiscoverContract.methods.getDAppsCount().call()
// for (let i = 0; i < dappsCount; i++) {
// const dapp = await DiscoverContract.methods.dapps(i).call()
// }
// }
async getDAppById(id) {
let dapp
try {
const dappId = await DiscoverContract.methods.id2index(id).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 = await ipfsSDK.retrieveDAppMetadataByHash(dapp.metadata)
return dapp
} catch (error) {
throw new Error('Error fetching correct data from IPFS')
}
}
async safeMax() {
return DiscoverContract.methods.safeMax().call()
}
async isDAppExists(id) {
return DiscoverContract.methods.existingIDs(id).call()
}
// Transaction methods
async createDApp(amount, metadata) {
const dappMetadata = JSON.parse(JSON.stringify(metadata))
const dappId = web3.utils.keccak256(JSON.stringify(dappMetadata))
await this.validator.validateDAppCreation(dappId, amount)
const uploadedMetadata = await ipfsSDK.uploadDAppMetadata(dappMetadata)
const callData = DiscoverContract.methods
.createDApp(dappId, amount, uploadedMetadata)
.encodeABI()
const createdTx = await this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData,
)
return { tx: createdTx, id: dappId }
}
async upVote(id, amount) {
await this.validator.validateUpVoting(id, amount)
const callData = DiscoverContract.methods.upvote(id, amount).encodeABI()
return this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData,
)
}
async downVote(id, amount) {
await this.validator.validateDownVoting(id, amount)
const callData = DiscoverContract.methods.downvote(id, amount).encodeABI()
return this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData,
)
}
async withdraw(id, amount) {
await super.__unlockServiceAccount()
await this.validator.validateWithdrawing(id, amount)
try {
return broadcastContractFn(
DiscoverContract.methods.withdraw(id, amount).send,
this.sharedContext.account,
)
} catch (error) {
throw new Error(`Transfer on withdraw failed. Details: ${error.message}`)
}
}
async setMetadata(id, metadata) {
await super.__unlockServiceAccount()
await this.validator.validateMetadataSet(id)
const dappMetadata = JSON.parse(JSON.stringify(metadata))
const uploadedMetadata = await ipfsSDK.uploadDAppMetadata(dappMetadata)
try {
return broadcastContractFn(
DiscoverContract.methods.setMetadata(id, uploadedMetadata).send,
this.sharedContext.account,
)
} catch (error) {
throw new Error(`Uploading metadata failed. Details: ${error.message}`)
}
}
}
export default DiscoverService

View File

@ -0,0 +1,76 @@
class DiscoverValidator {
constructor(service) {
this.service = service
}
async validateUpVoteEffect(id, amount) {
const dapp = await this.service.getDAppById(id)
const safeMax = await this.service.safeMax()
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 validateDAppCreation(id, amount) {
const dappExists = await this.service.isDAppExists(id)
if (dappExists) {
throw new Error('You must submit a unique ID')
}
if (amount <= 0) {
throw new Error(
'You must spend some SNT to submit a ranking in order to avoid spam',
)
}
const safeMax = await this.service.safeMax()
if (amount > safeMax) {
throw new Error('You cannot stake more SNT than the ceiling dictates')
}
}
async validateUpVoting(id, amount) {
await this.validateUpVoteEffect(id, amount)
if (amount <= 0) {
throw new Error('You must send some SNT in order to upvote')
}
}
async validateDownVoting(id, amount) {
const dapp = await this.service.getDAppById(id)
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)
if (dapp.developer.toLowerCase() != this.service.sharedContext.account) {
throw new Error('Only the developer can withdraw SNT staked on this data')
}
if (amount > dapp.available) {
throw new Error(
'You can only withdraw a percentage of the SNT staked, less what you have already received',
)
}
}
async validateMetadataSet(id) {
const dapp = await this.service.getDAppById(id)
if (dapp.developer.toLowerCase() != this.service.sharedContext.account) {
throw new Error('Only the developer can update the metadata')
}
}
}
export default DiscoverValidator

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

@ -0,0 +1,49 @@
import { broadcastContractFn } from '../helpers'
import BlockchainService from '../blockchain-service'
import SNTValidator from './snt-validator'
import SNTToken from '../../../../../embarkArtifacts/contracts/SNT'
class SNTService extends BlockchainService {
constructor(sharedContext) {
super(sharedContext, SNTToken, SNTValidator)
}
async allowance(from, to) {
return SNTToken.methods.allowance(from, to).call()
}
async balanceOf(account) {
return SNTToken.methods.balanceOf(account).call()
}
async controller() {
return SNTToken.methods.controller().call()
}
async transferable() {
return SNTToken.methods.transfersEnabled().call()
}
async approveAndCall(spender, amount, callData) {
await super.__unlockServiceAccount()
await this.validator.validateApproveAndCall(spender, amount)
return broadcastContractFn(
SNTToken.methods.approveAndCall(spender, amount, callData).send,
this.sharedContext.account,
)
}
// This is for testing purpose only
async generateTokens() {
await super.__unlockServiceAccount()
await SNTToken.methods
.generateTokens(this.sharedContext.account, 10000)
.send({ from: this.sharedContext.account })
}
}
export default SNTService

View File

@ -0,0 +1,35 @@
class SNTValidator {
constructor(service) {
this.service = service
}
async validateSNTTransferFrom(amount) {
const toBalance = await this.service.balanceOf(
this.service.sharedContext.account,
)
if (toBalance < amount) {
throw new Error('Not enough SNT balance')
}
}
async validateApproveAndCall(spender, amount) {
const isTransferableToken = await this.service.transferable()
if (!isTransferableToken) {
throw new Error('Token is not transferable')
}
await this.validateSNTTransferFrom(amount)
const allowance = await this.service.allowance(
this.service.sharedContext.account,
spender,
)
if (amount != 0 && allowance != 0) {
throw new Error('You have allowance already')
}
}
}
export default SNTValidator

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

@ -0,0 +1,20 @@
/* global web3 */
const TRANSACTION_STATUSES = {
Failed: 0,
Successful: 1,
Pending: 2,
}
export default {
getTxStatus: async txHash => {
const txReceipt = await web3.eth.getTransactionReceipt(txHash)
if (txReceipt) {
return txReceipt.status
? TRANSACTION_STATUSES.Successful
: TRANSACTION_STATUSES.Failed
}
return TRANSACTION_STATUSES.Pending
},
}

View File

@ -0,0 +1,29 @@
const ONE = '1000000000000000000'
const formatBigNumberToNumber = function(bigNumber) {
let stringifyedNumber = bigNumber.toString(10)
if (stringifyedNumber == '0') {
return stringifyedNumber
}
let numberWholePartLength = 0
if (bigNumber.lt(ONE)) {
stringifyedNumber = stringifyedNumber.padStart(19, 0)
numberWholePartLength = 1
} else {
numberWholePartLength = bigNumber.div('1000000000000000000').toString(10)
.length
}
return `${stringifyedNumber.substr(
0,
numberWholePartLength,
)}.${stringifyedNumber.substr(
numberWholePartLength,
stringifyedNumber.length,
)}`
}
export default formatBigNumberToNumber

View File

@ -10,6 +10,8 @@ import Submit from '../Submit'
import Terms from '../Terms/Terms'
import TransactionStatus from '../TransactionStatus'
import Example from '../BlockchainExample'
class Router extends React.Component {
componentDidMount() {
const { fetchHighestRanked, fetchRecentlyAdded } = this.props
@ -25,6 +27,7 @@ class Router extends React.Component {
<Route path="/all" component={Dapps} />
<Route path="/recently-added" component={RecentlyAdded} />
<Route path="/terms" component={Terms} />
<Route path="/example" component={Example} />
</Switch>,
<Vote key={2} />,
<Submit key={3} />,

View File

@ -0,0 +1,4 @@
import { connect } from 'react-redux'
import BlockchainExample from './BlockchainExample'
export default connect()(BlockchainExample)

View File

@ -0,0 +1,107 @@
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 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 setMetadata(id) {
DAPP_DATA.category = 'updated'
return getResult('setMetadata', [id, DAPP_DATA])
}
async logDiscoverMethods() {
const createdDApp = await this.createDApp()
const dappData = await this.getFullDApp(createdDApp.id)
console.log(`Created DApp : ${JSON.stringify(dappData)}`)
document.getElementById('testImage').src = dappData.metadata.image
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)}`,
)
console.log(
`Set metadata TX Hash : ${await this.setMetadata(createdDApp.id)}`,
)
console.log(
`Updated DApp : ${JSON.stringify(
await this.getFullDApp(createdDApp.id),
)}`,
)
}
render() {
return (
<div>
<h1 onLoad={this.logDiscoverMethods()} />
<img id="testImage" />
</div>
)
}
}
export default Example

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
import BlockchainExample from './BlockchainExample.container'
export default BlockchainExample

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
}
}