From 27fb4f339ef5b9e97f5e173729e644fe16404713 Mon Sep 17 00:00:00 2001 From: Lyubomir Kiprov Date: Wed, 24 Apr 2019 13:56:18 +0300 Subject: [PATCH 1/6] Add Discover service implementation + some validators --- .eslintrc.json | 16 +++- config/blockchain.js | 86 +++++++++---------- package.json | 2 + .../discover-validator-utils.js | 14 +++ .../read-service/discover-r-service.js | 43 ++++++++++ .../read-service/validator.js | 26 ++++++ .../write-service/discover-w-service.js | 84 ++++++++++++++++++ .../write-service/validator.js | 71 +++++++++++++++ src/common/blockchain/index.js | 25 ++++++ src/common/utils/number-formatter.js | 29 +++++++ src/modules/App/Router.jsx | 2 + .../BlockchainExample.container.js | 4 + .../BlockchainExample/BlockchainExample.jsx | 17 ++++ src/modules/BlockchainExample/index.js | 3 + 14 files changed, 375 insertions(+), 47 deletions(-) create mode 100644 src/common/blockchain/discover-services/discover-validator-utils.js create mode 100644 src/common/blockchain/discover-services/read-service/discover-r-service.js create mode 100644 src/common/blockchain/discover-services/read-service/validator.js create mode 100644 src/common/blockchain/discover-services/write-service/discover-w-service.js create mode 100644 src/common/blockchain/discover-services/write-service/validator.js create mode 100644 src/common/blockchain/index.js create mode 100644 src/common/utils/number-formatter.js create mode 100644 src/modules/BlockchainExample/BlockchainExample.container.js create mode 100644 src/modules/BlockchainExample/BlockchainExample.jsx create mode 100644 src/modules/BlockchainExample/index.js diff --git a/.eslintrc.json b/.eslintrc.json index 402a1dc..b68da19 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,16 @@ { - "extends": ["airbnb", "plugin:prettier/recommended"], - "plugins": ["prettier"], + "extends": [ + "airbnb", + "plugin:prettier/recommended" + ], + "plugins": [ + "prettier" + ], "rules": { - "prettier/prettier": "error" + "prettier/prettier": "error", + "func-names": "off", + "eqeqeq": "off", + "class-methods-use-this": "off" }, "env": { "browser": true, @@ -12,4 +20,4 @@ "parserOptions": { "ecmaVersion": 9 } -} +} \ No newline at end of file diff --git a/config/blockchain.js b/config/blockchain.js index a474d41..650bfaa 100644 --- a/config/blockchain.js +++ b/config/blockchain.js @@ -2,23 +2,23 @@ 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 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 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: [ + /* ,accounts: [ { nodeAccounts: true, // Accounts use for the node numAddresses: "1", // Number of addresses/accounts (defaults to 1) @@ -39,68 +39,68 @@ module.exports = { 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 +108,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: { + // } }; diff --git a/package.json b/package.json index 7d21b62..5ec895b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/blockchain/discover-services/discover-validator-utils.js b/src/common/blockchain/discover-services/discover-validator-utils.js new file mode 100644 index 0000000..0274a40 --- /dev/null +++ b/src/common/blockchain/discover-services/discover-validator-utils.js @@ -0,0 +1,14 @@ +const discoverValidatorUtils = { + async checkDappCorrectness(dapp, id) { + if (dapp.id != id) { + throw new Error('Error fetching correct data') + } + }, + async checkUpVotingAmount(amount, limit) { + if (amount > limit) { + throw new Error('You cannot upvote by this much, try with a lower amount') + } + }, +} + +export default discoverValidatorUtils diff --git a/src/common/blockchain/discover-services/read-service/discover-r-service.js b/src/common/blockchain/discover-services/read-service/discover-r-service.js new file mode 100644 index 0000000..eedef19 --- /dev/null +++ b/src/common/blockchain/discover-services/read-service/discover-r-service.js @@ -0,0 +1,43 @@ +import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' +import DiscoverRServiceValidator from './validator' + +class DiscoverReadService { + constructor() { + this.validator = new DiscoverRServiceValidator(this) + } + + // TODO: Amount -> string/bigInt/number ? + // TODO: Maybe we can get id from a DApp name ? + // TODO: formatBigNumberToNumber + // TODO: validators - YUP + async upVoteEffect(id, amount) { + const dapp = await this.getDAppById(id) + await this.validator.validateUpVoteEffect(dapp, 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() + } + + async getDAppById(id) { + const dappId = await DiscoverContract.methods.id2index(id).call() + return DiscoverContract.methods.dapps(dappId).call() + } + + async safeMax() { + console.log(DiscoverContract) + debugger + return DiscoverContract.safeMax() + } + + async isDAppExists(id) { + return DiscoverContract.methods.existingIDs(id).call() + } +} + +export default DiscoverReadService diff --git a/src/common/blockchain/discover-services/read-service/validator.js b/src/common/blockchain/discover-services/read-service/validator.js new file mode 100644 index 0000000..bf04114 --- /dev/null +++ b/src/common/blockchain/discover-services/read-service/validator.js @@ -0,0 +1,26 @@ +import DiscoverValidatorUtils from '../discover-validator-utils' + +class DiscoverReadServiceValidator { + constructor(service) { + this.service = service + } + + async validateUpVoteEffect(id, amount) { + const dapp = await this.service.getDAppById() + await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) + + // TODO: should check if dapp.balance is a big number + const safeMax = await this.service.safeMax() + await DiscoverValidatorUtils.checkUpVotingAmount( + dapp.balance + amount, + safeMax, + ) + } + + async validateDownVoteCost(id) { + const dapp = await this.service.getDAppById() + await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) + } +} + +export default DiscoverReadServiceValidator diff --git a/src/common/blockchain/discover-services/write-service/discover-w-service.js b/src/common/blockchain/discover-services/write-service/discover-w-service.js new file mode 100644 index 0000000..b1d49ce --- /dev/null +++ b/src/common/blockchain/discover-services/write-service/discover-w-service.js @@ -0,0 +1,84 @@ +import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' +import DiscoverRService from '../read-service/discover-r-service' + +import DiscoverWServiceValidator from './validator' + +// TODO: Validators ? +// TODO: check for unlocked account: If it is not -> request unlocking +// TODO: preOperation -> inherited method ? +const unlockAccount = async function(account) { + return account +} + +class DiscoverWriteService extends DiscoverRService { + constructor(unlockedAccount) { + this.account = unlockedAccount + this.validator = new DiscoverWServiceValidator(this) + } + + async createDApp(id, amount, metadata) { + await unlockAccount(this.account) + await this.validator.validateDAppCreation(id, amount) + + try { + await DiscoverContract.methods.createDApp(id, amount, metadata, { + from: this.account, + }) + } catch (error) { + throw new Error('Transfer failed') + } + } + + async upVote(id, amount) { + await unlockAccount(this.account) + await this.validator.validateUpVoting(id, amount) + + try { + await DiscoverContract.methods.upVote(id, amount, { from: this.account }) + } catch (error) { + throw new Error('Transfer failed') + } + } + + async downVote(id, amount) { + await unlockAccount(this.account) + await this.validator.validateDownVoting(id, amount) + + try { + await DiscoverContract.methods.downVote(id, amount, { + from: this.account, + }) + } catch (error) { + throw new Error('Transfer failed') + } + } + + async withdraw(id, amount) { + await unlockAccount(this.account) + await this.validator.validateWithdrawing(id, amount) + + try { + await DiscoverContract.methods.withdraw(id, amount, this.account) + } catch (error) { + throw new Error('Transfer failed') + } + } + + async setMetadata(id, metadata) { + await unlockAccount(this.account) + await this.validator.validateMetadataSet(id) + + await DiscoverContract.methods.setMetadata(id, metadata, { + from: this.account, + }) + } + + // async receiveApproval(from, amount, token, data) { + // await unlockAccount(this.account); + // await this.validator.validateReceiveApproval(); + + // await DiscoverContract.methods.receiveApproval(from, amount, token, data, { from: this.account }); + // } +} + +export default DiscoverWriteService diff --git a/src/common/blockchain/discover-services/write-service/validator.js b/src/common/blockchain/discover-services/write-service/validator.js new file mode 100644 index 0000000..9587a27 --- /dev/null +++ b/src/common/blockchain/discover-services/write-service/validator.js @@ -0,0 +1,71 @@ +import DiscoverValidatorUtils from '../discover-validator-utils' +import DiscoverRServiceValidator from '../read-service/validator' + +class DiscoverWriteServiceValidator extends DiscoverRServiceValidator { + // TODO: Add SNT allowance checks + 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') + } + } + + // TODO: Add SNT allowance checks + async validateUpVoting(id, amount) { + await super.validateUpVoteEffect(id, amount) + + if (amount <= 0) { + throw new Error('You must send some SNT in order to upvote') + } + } + + // TODO: Add SNT allowance checks + async validateDownVoting(id, amount) { + await super.validateDownVoteCost(id) + + const downVoteCost = await this.service.downVoteCost(id) + if (downVoteCost != amount) { + throw new Error('Incorrect amount: valid iff effect on ranking is 1%') + } + } + + async validateWithdrawing(id, amount) { + const dapp = await this.service.getDAppById(id) + await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) + + if (dapp.developer != this.service.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 != this.service.account) { + throw new Error('Only the developer can update the metadata') + } + } + + // async validateReceiveApproval() { + + // } +} + +export default DiscoverWriteServiceValidator diff --git a/src/common/blockchain/index.js b/src/common/blockchain/index.js new file mode 100644 index 0000000..71fead4 --- /dev/null +++ b/src/common/blockchain/index.js @@ -0,0 +1,25 @@ +// import DiscoverContract from '../../../embarkArtifacts/contracts/Discover'; +import EmbarkJS from '../../embarkArtifacts/embarkjs' + +import DiscoverReadService from './discover-services/read-service/discover-r-service' +import DiscoverWriteService from './discover-services/write-service/discover-w-service' + +const ReadOnlyServices = { + DiscoverService: new DiscoverReadService(), +} + +// TODO: ask Andy what kind of wallets is going to be used +const init = async function() { + try { + const account = await EmbarkJS.enableEthereum() + + const discoverService = new DiscoverWriteService(account) + + return { DiscoverService: discoverService, ...ReadOnlyServices } + } catch (error) { + // TODO: Should handle it in an elegant way + throw new Error(error.message) + } +} + +export default { init, ...ReadOnlyServices } diff --git a/src/common/utils/number-formatter.js b/src/common/utils/number-formatter.js new file mode 100644 index 0000000..043dabc --- /dev/null +++ b/src/common/utils/number-formatter.js @@ -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 diff --git a/src/modules/App/Router.jsx b/src/modules/App/Router.jsx index d1723bb..4480f03 100644 --- a/src/modules/App/Router.jsx +++ b/src/modules/App/Router.jsx @@ -5,6 +5,7 @@ import Filtered from '../Filtered' import RecentlyAdded from '../RecentlyAdded' import Vote from '../Vote' import Dapps from '../Dapps' +import Example from '../BlockchainExample' export default () => ( @@ -13,5 +14,6 @@ export default () => ( + ) diff --git a/src/modules/BlockchainExample/BlockchainExample.container.js b/src/modules/BlockchainExample/BlockchainExample.container.js new file mode 100644 index 0000000..bbae1ca --- /dev/null +++ b/src/modules/BlockchainExample/BlockchainExample.container.js @@ -0,0 +1,4 @@ +import { connect } from 'react-redux' +import BlockchainExample from './BlockchainExample' + +export default connect()(BlockchainExample) diff --git a/src/modules/BlockchainExample/BlockchainExample.jsx b/src/modules/BlockchainExample/BlockchainExample.jsx new file mode 100644 index 0000000..d4a61de --- /dev/null +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import BlockchainSDK from '../../common/blockchain' +// import EmbarkJS from '../../embarkArtifacts/embarkjs'; + +class Example extends React.Component { + async logDiscoverMethod() { + console.log('here') + debugger + console.log(await BlockchainSDK.DiscoverService.safeMax()) + } + + render() { + return

+ } +} + +export default Example diff --git a/src/modules/BlockchainExample/index.js b/src/modules/BlockchainExample/index.js new file mode 100644 index 0000000..1b16b3f --- /dev/null +++ b/src/modules/BlockchainExample/index.js @@ -0,0 +1,3 @@ +import BlockchainExample from './BlockchainExample.container' + +export default BlockchainExample From b6e01e690baea4a32a5dc6444d122180877f6c50 Mon Sep 17 00:00:00 2001 From: Lyubomir Kiprov Date: Wed, 24 Apr 2019 18:03:54 +0300 Subject: [PATCH 2/6] Abstract validators and serverices --- config/blockchain.js | 40 ++++++++------- config/contracts.js | 49 +++++++++---------- embark.json | 2 +- .../discover-services/discover-service.js | 47 ++++++++++++++++++ .../discover-services/discover-validator.js | 11 +++++ .../read-service/discover-r-service.js | 41 ++-------------- .../read-service/validator.js | 7 +-- .../write-service/discover-w-service.js | 34 +++++++------ .../write-service/validator.js | 8 +-- src/common/blockchain/index.js | 8 +-- .../BlockchainExample/BlockchainExample.jsx | 12 +++-- 11 files changed, 146 insertions(+), 113 deletions(-) create mode 100644 src/common/blockchain/discover-services/discover-service.js create mode 100644 src/common/blockchain/discover-services/discover-validator.js diff --git a/config/blockchain.js b/config/blockchain.js index 650bfaa..8378040 100644 --- a/config/blockchain.js +++ b/config/blockchain.js @@ -4,12 +4,14 @@ module.exports = { enabled: true, 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 }, 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: [], }, @@ -18,28 +20,30 @@ module.exports = { // 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: [ + 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 + 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 { - privateKey: "your_private_key" + privateKey: + '0xEFA9DB87A755C9D2B96F77BBCB9EF06CBDDFC01DB1A5129CE2649F73E9C2739C', + balance: '100 ether', }, - { - 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 - } - ] */ + // { + // 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 @@ -147,4 +151,4 @@ module.exports = { // "embark run custom_name" or "embark blockchain custom_name" // custom_name: { // } -}; +} diff --git a/config/contracts.js b/config/contracts.js index d723a18..979179f 100644 --- a/config/contracts.js +++ b/config/contracts.js @@ -3,12 +3,12 @@ module.exports = { default: { // Blockchain node to deploy the contracts deployment: { - host: "localhost", // Host of the blockchain node + host: 'localhost', // Host of the blockchain node port: 8546, // Port of the blockchain node - type: "ws" // Type of connection (ws or rpc), + type: 'ws', // 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 +27,13 @@ module.exports = { { "nodeAccounts": true // Uses the Ethereum node's accounts } - ]*/ + ] */ }, // 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 +41,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 +49,41 @@ 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" } + args: { _SNT: '0x744d70fdbe2ba4cf95131626614a1763df805b9e' }, }, - MiniMeToken: { "deploy": false }, - TestBancorFormula: { "deploy": false } - } + MiniMeToken: { deploy: false }, + TestBancorFormula: { deploy: false }, + }, }, // 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: { + // } +} diff --git a/embark.json b/embark.json index 870cde6..f2ddad9 100644 --- a/embark.json +++ b/embark.json @@ -22,5 +22,5 @@ "optimize-runs": 200 } }, - "generationDir": "embarkArtifacts" + "generationDir": "src/embarkArtifacts" } \ No newline at end of file diff --git a/src/common/blockchain/discover-services/discover-service.js b/src/common/blockchain/discover-services/discover-service.js new file mode 100644 index 0000000..a5a9347 --- /dev/null +++ b/src/common/blockchain/discover-services/discover-service.js @@ -0,0 +1,47 @@ +import DiscoverContract from '../../../embarkArtifacts/contracts/Discover' +import DiscoverServiceValidator from './discover-validator' + +class DiscoverService { + constructor(Validator) { + this.validator = new Validator(this) + + if (!(this.validator instanceof DiscoverServiceValidator)) { + throw new Error( + 'Discover Service Validator should be an instance of DiscoverValidator', + ) + } + } + + // TODO: Amount -> string/bigInt/number ? + // TODO: Maybe we can get id from a DApp name ? + // TODO: formatBigNumberToNumber + // TODO: validators - YUP + async upVoteEffect(id, amount) { + const dapp = await this.getDAppById(id) + await this.validator.validateUpVoteEffect(dapp, 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() + } + + async getDAppById(id) { + const dappId = await DiscoverContract.methods.id2index(id).call() + return DiscoverContract.methods.dapps(dappId).call() + } + + async safeMax() { + return DiscoverContract.methods.safeMax().call() + } + + // async isDAppExists(id) { + // return DiscoverContract.methods.existingIDs(id).call() + // } +} + +export default DiscoverService diff --git a/src/common/blockchain/discover-services/discover-validator.js b/src/common/blockchain/discover-services/discover-validator.js new file mode 100644 index 0000000..cb4ef4a --- /dev/null +++ b/src/common/blockchain/discover-services/discover-validator.js @@ -0,0 +1,11 @@ +class DiscoverValidator { + constructor(service) { + this.service = service + } + + async validateUpVoteEffect(id, amount) {} + + async validateDownVoteCost(id) {} +} + +export default DiscoverValidator diff --git a/src/common/blockchain/discover-services/read-service/discover-r-service.js b/src/common/blockchain/discover-services/read-service/discover-r-service.js index eedef19..62795ef 100644 --- a/src/common/blockchain/discover-services/read-service/discover-r-service.js +++ b/src/common/blockchain/discover-services/read-service/discover-r-service.js @@ -1,42 +1,9 @@ -import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' -import DiscoverRServiceValidator from './validator' +import DiscoverService from '../discover-service' +import DiscoverReadServiceValidator from './validator' -class DiscoverReadService { +class DiscoverReadService extends DiscoverService { constructor() { - this.validator = new DiscoverRServiceValidator(this) - } - - // TODO: Amount -> string/bigInt/number ? - // TODO: Maybe we can get id from a DApp name ? - // TODO: formatBigNumberToNumber - // TODO: validators - YUP - async upVoteEffect(id, amount) { - const dapp = await this.getDAppById(id) - await this.validator.validateUpVoteEffect(dapp, 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() - } - - async getDAppById(id) { - const dappId = await DiscoverContract.methods.id2index(id).call() - return DiscoverContract.methods.dapps(dappId).call() - } - - async safeMax() { - console.log(DiscoverContract) - debugger - return DiscoverContract.safeMax() - } - - async isDAppExists(id) { - return DiscoverContract.methods.existingIDs(id).call() + super(DiscoverReadServiceValidator) } } diff --git a/src/common/blockchain/discover-services/read-service/validator.js b/src/common/blockchain/discover-services/read-service/validator.js index bf04114..fb2d75c 100644 --- a/src/common/blockchain/discover-services/read-service/validator.js +++ b/src/common/blockchain/discover-services/read-service/validator.js @@ -1,10 +1,7 @@ +import DiscoverValidator from '../discover-validator' import DiscoverValidatorUtils from '../discover-validator-utils' -class DiscoverReadServiceValidator { - constructor(service) { - this.service = service - } - +class DiscoverReadServiceValidator extends DiscoverValidator { async validateUpVoteEffect(id, amount) { const dapp = await this.service.getDAppById() await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) diff --git a/src/common/blockchain/discover-services/write-service/discover-w-service.js b/src/common/blockchain/discover-services/write-service/discover-w-service.js index b1d49ce..3811146 100644 --- a/src/common/blockchain/discover-services/write-service/discover-w-service.js +++ b/src/common/blockchain/discover-services/write-service/discover-w-service.js @@ -1,6 +1,6 @@ import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' -import DiscoverRService from '../read-service/discover-r-service' +import DiscoverService from '../discover-service' import DiscoverWServiceValidator from './validator' // TODO: Validators ? @@ -10,21 +10,21 @@ const unlockAccount = async function(account) { return account } -class DiscoverWriteService extends DiscoverRService { +class DiscoverWriteService extends DiscoverService { constructor(unlockedAccount) { + super(DiscoverWServiceValidator) this.account = unlockedAccount - this.validator = new DiscoverWServiceValidator(this) } async createDApp(id, amount, metadata) { await unlockAccount(this.account) await this.validator.validateDAppCreation(id, amount) - try { - await DiscoverContract.methods.createDApp(id, amount, metadata, { - from: this.account, - }) + await DiscoverContract.methods + .createDApp(id, amount, metadata) + .send({ from: this.account }) } catch (error) { + console.log(error) throw new Error('Transfer failed') } } @@ -34,7 +34,9 @@ class DiscoverWriteService extends DiscoverRService { await this.validator.validateUpVoting(id, amount) try { - await DiscoverContract.methods.upVote(id, amount, { from: this.account }) + await DiscoverContract.methods + .upVote(id, amount) + .send({ from: this.account }) } catch (error) { throw new Error('Transfer failed') } @@ -45,9 +47,9 @@ class DiscoverWriteService extends DiscoverRService { await this.validator.validateDownVoting(id, amount) try { - await DiscoverContract.methods.downVote(id, amount, { - from: this.account, - }) + await DiscoverContract.methods + .downVote(id, amount) + .send({ from: this.account }) } catch (error) { throw new Error('Transfer failed') } @@ -58,7 +60,9 @@ class DiscoverWriteService extends DiscoverRService { await this.validator.validateWithdrawing(id, amount) try { - await DiscoverContract.methods.withdraw(id, amount, this.account) + await DiscoverContract.methods + .withdraw(id, amount) + .send({ from: this.account }) } catch (error) { throw new Error('Transfer failed') } @@ -68,9 +72,9 @@ class DiscoverWriteService extends DiscoverRService { await unlockAccount(this.account) await this.validator.validateMetadataSet(id) - await DiscoverContract.methods.setMetadata(id, metadata, { - from: this.account, - }) + await DiscoverContract.methods + .setMetadata(id, metadata) + .send({ from: this.account }) } // async receiveApproval(from, amount, token, data) { diff --git a/src/common/blockchain/discover-services/write-service/validator.js b/src/common/blockchain/discover-services/write-service/validator.js index 9587a27..cc20799 100644 --- a/src/common/blockchain/discover-services/write-service/validator.js +++ b/src/common/blockchain/discover-services/write-service/validator.js @@ -4,10 +4,10 @@ import DiscoverRServiceValidator from '../read-service/validator' class DiscoverWriteServiceValidator extends DiscoverRServiceValidator { // TODO: Add SNT allowance checks async validateDAppCreation(id, amount) { - const dappExists = await this.service.isDAppExists(id) - if (dappExists) { - throw new Error('You must submit a unique ID') - } + // const dappExists = await this.service.isDAppExists(id) + // if (dappExists) { + // throw new Error('You must submit a unique ID') + // } if (amount <= 0) { throw new Error( diff --git a/src/common/blockchain/index.js b/src/common/blockchain/index.js index 71fead4..de5d59a 100644 --- a/src/common/blockchain/index.js +++ b/src/common/blockchain/index.js @@ -11,11 +11,13 @@ const ReadOnlyServices = { // TODO: ask Andy what kind of wallets is going to be used const init = async function() { try { - const account = await EmbarkJS.enableEthereum() + const account = (await EmbarkJS.enableEthereum())[0] - const discoverService = new DiscoverWriteService(account) + const DiscoverService = new DiscoverWriteService(account) - return { DiscoverService: discoverService, ...ReadOnlyServices } + return { + DiscoverService, + } } catch (error) { // TODO: Should handle it in an elegant way throw new Error(error.message) diff --git a/src/modules/BlockchainExample/BlockchainExample.jsx b/src/modules/BlockchainExample/BlockchainExample.jsx index d4a61de..b0df235 100644 --- a/src/modules/BlockchainExample/BlockchainExample.jsx +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -1,12 +1,16 @@ import React from 'react' import BlockchainSDK from '../../common/blockchain' -// import EmbarkJS from '../../embarkArtifacts/embarkjs'; class Example extends React.Component { async logDiscoverMethod() { - console.log('here') - debugger - console.log(await BlockchainSDK.DiscoverService.safeMax()) + const services = await BlockchainSDK.init() + console.log( + await services.DiscoverService.createDApp( + '0x123', + '100000000000000000', + '0x123', + ), + ) } render() { From a26091f59ee213b4f3c8cff47e1c8fbb6374d6d4 Mon Sep 17 00:00:00 2001 From: Lyubomir Kiprov Date: Tue, 30 Apr 2019 18:15:27 +0300 Subject: [PATCH 3/6] Refactor some implementation --- config/blockchain.js | 48 +++++----- config/contracts.js | 41 +++++++- contracts/Discover.sol | 2 +- src/common/blockchain/blockchain-service.js | 24 +++++ .../discover-services/discover-service.js | 96 ++++++++++++++++--- .../discover-validator-utils.js | 14 --- .../discover-services/discover-validator.js | 80 +++++++++++++++- .../read-service/discover-r-service.js | 10 -- .../read-service/validator.js | 23 ----- .../write-service/discover-w-service.js | 88 ----------------- .../write-service/validator.js | 71 -------------- src/common/blockchain/index.js | 25 ++--- .../blockchain/snt-services/snt-service.js | 46 +++++++++ .../blockchain/snt-services/snt-validator.js | 35 +++++++ .../BlockchainExample/BlockchainExample.jsx | 12 +-- 15 files changed, 340 insertions(+), 275 deletions(-) create mode 100644 src/common/blockchain/blockchain-service.js delete mode 100644 src/common/blockchain/discover-services/discover-validator-utils.js delete mode 100644 src/common/blockchain/discover-services/read-service/discover-r-service.js delete mode 100644 src/common/blockchain/discover-services/read-service/validator.js delete mode 100644 src/common/blockchain/discover-services/write-service/discover-w-service.js delete mode 100644 src/common/blockchain/discover-services/write-service/validator.js create mode 100644 src/common/blockchain/snt-services/snt-service.js create mode 100644 src/common/blockchain/snt-services/snt-validator.js diff --git a/config/blockchain.js b/config/blockchain.js index 8378040..d1bce0d 100644 --- a/config/blockchain.js +++ b/config/blockchain.js @@ -20,30 +20,30 @@ module.exports = { // 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/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 - { - privateKey: - '0xEFA9DB87A755C9D2B96F77BBCB9EF06CBDDFC01DB1A5129CE2649F73E9C2739C', - balance: '100 ether', - }, - // { - // 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 + // { + // privateKey: + // '0xEFA9DB87A755C9D2B96F77BBCB9EF06CBDDFC01DB1A5129CE2649F73E9C2739C', + // balance: '100 ether', + // }, + // { + // 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 diff --git a/config/contracts.js b/config/contracts.js index 979179f..c2d1c59 100644 --- a/config/contracts.js +++ b/config/contracts.js @@ -4,8 +4,8 @@ module.exports = { // 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), + 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: [ @@ -28,6 +28,14 @@ module.exports = { "nodeAccounts": true // Uses the Ethereum node's accounts } ] */ + + accounts: [ + { + privateKey: + '0xEFA9DB87A755C9D2B96F77BBCB9EF06CBDDFC01DB1A5129CE2649F73E9C2739C', + balance: '100 ether', + }, + ], }, // order of connections the dapp should connect to dappConnection: [ @@ -51,12 +59,35 @@ module.exports = { // contracts section. // 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: { + from: '0x68C864373C6631984B646453138557A81224ACf6', + instanceOf: 'MiniMeToken', + args: [ + '$MiniMeTokenFactory', + '0x0000000000000000000000000000000000000000', + 0, + 'TestMiniMeToken', + 18, + 'SNT', + true, + ], + }, + Discover: { + from: '0x68C864373C6631984B646453138557A81224ACf6', + args: ['$SNT'], + }, }, }, diff --git a/contracts/Discover.sol b/contracts/Discover.sol index 61b2048..1f57aeb 100644 --- a/contracts/Discover.sol +++ b/contracts/Discover.sol @@ -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); diff --git a/src/common/blockchain/blockchain-service.js b/src/common/blockchain/blockchain-service.js new file mode 100644 index 0000000..8e05ac7 --- /dev/null +++ b/src/common/blockchain/blockchain-service.js @@ -0,0 +1,24 @@ +import EmbarkJS from '../../embarkArtifacts/embarkjs' + +class BlockchainService { + constructor(sharedContext, contractAddress, Validator) { + this.contract = contractAddress + this.sharedContext = sharedContext + this.validator = new Validator(this) + } + + async __unlockServiceAccount() { + try { + const accounts = await EmbarkJS.Blockchain.Providers.web3.getAccounts() + if (accounts.length > 0) { + this.sharedContext.account = accounts[0] + } + + this.sharedContext.account = (await EmbarkJS.enableEthereum())[0] + } catch (error) { + throw new Error('Could not unlock an account or web3 is missing') + } + } +} + +export default BlockchainService diff --git a/src/common/blockchain/discover-services/discover-service.js b/src/common/blockchain/discover-services/discover-service.js index a5a9347..3953e25 100644 --- a/src/common/blockchain/discover-services/discover-service.js +++ b/src/common/blockchain/discover-services/discover-service.js @@ -1,21 +1,22 @@ +import BlockchainService from '../blockchain-service' + +import DiscoverValidator from './discover-validator' import DiscoverContract from '../../../embarkArtifacts/contracts/Discover' -import DiscoverServiceValidator from './discover-validator' -class DiscoverService { - constructor(Validator) { - this.validator = new Validator(this) +// TODO: Validators ? - YUP +// TODO: check for unlocked account: If it is not -> request unlocking - YUP +// TODO: Make transfer failed an Error object ? - if (!(this.validator instanceof DiscoverServiceValidator)) { - throw new Error( - 'Discover Service Validator should be an instance of DiscoverValidator', - ) - } +class DiscoverService extends BlockchainService { + constructor(sharedContext) { + super(sharedContext, DiscoverContract.address, DiscoverValidator) } // TODO: Amount -> string/bigInt/number ? // TODO: Maybe we can get id from a DApp name ? // TODO: formatBigNumberToNumber - // TODO: validators - YUP + + // View methods async upVoteEffect(id, amount) { const dapp = await this.getDAppById(id) await this.validator.validateUpVoteEffect(dapp, id, amount) @@ -31,17 +32,82 @@ class DiscoverService { } async getDAppById(id) { - const dappId = await DiscoverContract.methods.id2index(id).call() - return DiscoverContract.methods.dapps(dappId).call() + try { + const dappId = await DiscoverContract.methods.id2index(id).call() + const dapp = await DiscoverContract.methods.dapps(dappId).call() + + return dapp + } catch (error) { + throw new Error('Searching DApp does not exists') + } } async safeMax() { return DiscoverContract.methods.safeMax().call() } - // async isDAppExists(id) { - // return DiscoverContract.methods.existingIDs(id).call() - // } + async isDAppExists(id) { + return DiscoverContract.methods.existingIDs(id).call() + } + + // Transaction methods + async createDApp(id, amount, metadata) { + await this.validator.validateDAppCreation(id, amount) + + const callData = DiscoverContract.methods + .createDApp(id, amount, metadata) + .encodeABI() + + await this.sharedContext.SNTService.approveAndCall( + this.contract, + amount, + callData, + ) + } + + async upVote(id, amount) { + await this.validator.validateUpVoting(id, amount) + + const callData = DiscoverContract.methods.upvote(id, amount).encodeABI() + await 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() + await this.sharedContext.SNTService.approveAndCall( + this.contract, + amount, + callData, + ) + } + + async withdraw(id, amount) { + await super.__unlockServiceAccount(this.service) + await this.validator.validateWithdrawing(id, amount) + + try { + await DiscoverContract.methods + .withdraw(id, amount) + .send({ from: this.sharedContext.account }) + } catch (error) { + throw new Error('Transfer on withdraw failed') + } + } + + async setMetadata(id, metadata) { + await super.__unlockServiceAccount(this.service) + await this.validator.validateMetadataSet(id) + + await DiscoverContract.methods + .setMetadata(id, metadata) + .send({ from: this.sharedContext.account }) + } } export default DiscoverService diff --git a/src/common/blockchain/discover-services/discover-validator-utils.js b/src/common/blockchain/discover-services/discover-validator-utils.js deleted file mode 100644 index 0274a40..0000000 --- a/src/common/blockchain/discover-services/discover-validator-utils.js +++ /dev/null @@ -1,14 +0,0 @@ -const discoverValidatorUtils = { - async checkDappCorrectness(dapp, id) { - if (dapp.id != id) { - throw new Error('Error fetching correct data') - } - }, - async checkUpVotingAmount(amount, limit) { - if (amount > limit) { - throw new Error('You cannot upvote by this much, try with a lower amount') - } - }, -} - -export default discoverValidatorUtils diff --git a/src/common/blockchain/discover-services/discover-validator.js b/src/common/blockchain/discover-services/discover-validator.js index cb4ef4a..a67da95 100644 --- a/src/common/blockchain/discover-services/discover-validator.js +++ b/src/common/blockchain/discover-services/discover-validator.js @@ -1,11 +1,87 @@ +const checkDappCorrectness = async function(dapp, id) { + if (dapp.id != id) { + throw new Error('Error fetching correct data') + } +} + class DiscoverValidator { constructor(service) { this.service = service } - async validateUpVoteEffect(id, amount) {} + async validateUpVoteEffect(id, amount) { + const dapp = await this.service.getDAppById(id) + await checkDappCorrectness(dapp, id) - async validateDownVoteCost(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') + } + } + + 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) { + 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) { + await this.validateDownVoteCost(id) + + const downVoteCost = await this.service.downVoteCost(id) + if (downVoteCost != amount) { + throw new Error('Incorrect amount: valid iff 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) { + 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 != this.service.sharedContext.account) { + throw new Error('Only the developer can update the metadata') + } + } } export default DiscoverValidator diff --git a/src/common/blockchain/discover-services/read-service/discover-r-service.js b/src/common/blockchain/discover-services/read-service/discover-r-service.js deleted file mode 100644 index 62795ef..0000000 --- a/src/common/blockchain/discover-services/read-service/discover-r-service.js +++ /dev/null @@ -1,10 +0,0 @@ -import DiscoverService from '../discover-service' -import DiscoverReadServiceValidator from './validator' - -class DiscoverReadService extends DiscoverService { - constructor() { - super(DiscoverReadServiceValidator) - } -} - -export default DiscoverReadService diff --git a/src/common/blockchain/discover-services/read-service/validator.js b/src/common/blockchain/discover-services/read-service/validator.js deleted file mode 100644 index fb2d75c..0000000 --- a/src/common/blockchain/discover-services/read-service/validator.js +++ /dev/null @@ -1,23 +0,0 @@ -import DiscoverValidator from '../discover-validator' -import DiscoverValidatorUtils from '../discover-validator-utils' - -class DiscoverReadServiceValidator extends DiscoverValidator { - async validateUpVoteEffect(id, amount) { - const dapp = await this.service.getDAppById() - await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) - - // TODO: should check if dapp.balance is a big number - const safeMax = await this.service.safeMax() - await DiscoverValidatorUtils.checkUpVotingAmount( - dapp.balance + amount, - safeMax, - ) - } - - async validateDownVoteCost(id) { - const dapp = await this.service.getDAppById() - await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) - } -} - -export default DiscoverReadServiceValidator diff --git a/src/common/blockchain/discover-services/write-service/discover-w-service.js b/src/common/blockchain/discover-services/write-service/discover-w-service.js deleted file mode 100644 index 3811146..0000000 --- a/src/common/blockchain/discover-services/write-service/discover-w-service.js +++ /dev/null @@ -1,88 +0,0 @@ -import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' - -import DiscoverService from '../discover-service' -import DiscoverWServiceValidator from './validator' - -// TODO: Validators ? -// TODO: check for unlocked account: If it is not -> request unlocking -// TODO: preOperation -> inherited method ? -const unlockAccount = async function(account) { - return account -} - -class DiscoverWriteService extends DiscoverService { - constructor(unlockedAccount) { - super(DiscoverWServiceValidator) - this.account = unlockedAccount - } - - async createDApp(id, amount, metadata) { - await unlockAccount(this.account) - await this.validator.validateDAppCreation(id, amount) - try { - await DiscoverContract.methods - .createDApp(id, amount, metadata) - .send({ from: this.account }) - } catch (error) { - console.log(error) - throw new Error('Transfer failed') - } - } - - async upVote(id, amount) { - await unlockAccount(this.account) - await this.validator.validateUpVoting(id, amount) - - try { - await DiscoverContract.methods - .upVote(id, amount) - .send({ from: this.account }) - } catch (error) { - throw new Error('Transfer failed') - } - } - - async downVote(id, amount) { - await unlockAccount(this.account) - await this.validator.validateDownVoting(id, amount) - - try { - await DiscoverContract.methods - .downVote(id, amount) - .send({ from: this.account }) - } catch (error) { - throw new Error('Transfer failed') - } - } - - async withdraw(id, amount) { - await unlockAccount(this.account) - await this.validator.validateWithdrawing(id, amount) - - try { - await DiscoverContract.methods - .withdraw(id, amount) - .send({ from: this.account }) - } catch (error) { - throw new Error('Transfer failed') - } - } - - async setMetadata(id, metadata) { - await unlockAccount(this.account) - await this.validator.validateMetadataSet(id) - - await DiscoverContract.methods - .setMetadata(id, metadata) - .send({ from: this.account }) - } - - // async receiveApproval(from, amount, token, data) { - // await unlockAccount(this.account); - // await this.validator.validateReceiveApproval(); - - // await DiscoverContract.methods.receiveApproval(from, amount, token, data, { from: this.account }); - // } -} - -export default DiscoverWriteService diff --git a/src/common/blockchain/discover-services/write-service/validator.js b/src/common/blockchain/discover-services/write-service/validator.js deleted file mode 100644 index cc20799..0000000 --- a/src/common/blockchain/discover-services/write-service/validator.js +++ /dev/null @@ -1,71 +0,0 @@ -import DiscoverValidatorUtils from '../discover-validator-utils' -import DiscoverRServiceValidator from '../read-service/validator' - -class DiscoverWriteServiceValidator extends DiscoverRServiceValidator { - // TODO: Add SNT allowance checks - 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') - } - } - - // TODO: Add SNT allowance checks - async validateUpVoting(id, amount) { - await super.validateUpVoteEffect(id, amount) - - if (amount <= 0) { - throw new Error('You must send some SNT in order to upvote') - } - } - - // TODO: Add SNT allowance checks - async validateDownVoting(id, amount) { - await super.validateDownVoteCost(id) - - const downVoteCost = await this.service.downVoteCost(id) - if (downVoteCost != amount) { - throw new Error('Incorrect amount: valid iff effect on ranking is 1%') - } - } - - async validateWithdrawing(id, amount) { - const dapp = await this.service.getDAppById(id) - await DiscoverValidatorUtils.checkDappCorrectness(dapp, id) - - if (dapp.developer != this.service.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 != this.service.account) { - throw new Error('Only the developer can update the metadata') - } - } - - // async validateReceiveApproval() { - - // } -} - -export default DiscoverWriteServiceValidator diff --git a/src/common/blockchain/index.js b/src/common/blockchain/index.js index de5d59a..800d559 100644 --- a/src/common/blockchain/index.js +++ b/src/common/blockchain/index.js @@ -1,27 +1,22 @@ -// import DiscoverContract from '../../../embarkArtifacts/contracts/Discover'; -import EmbarkJS from '../../embarkArtifacts/embarkjs' +import SNTService from './snt-services/snt-service' +import DiscoverService from './discover-services/discover-service' -import DiscoverReadService from './discover-services/read-service/discover-r-service' -import DiscoverWriteService from './discover-services/write-service/discover-w-service' - -const ReadOnlyServices = { - DiscoverService: new DiscoverReadService(), -} - -// TODO: ask Andy what kind of wallets is going to be used const init = async function() { try { - const account = (await EmbarkJS.enableEthereum())[0] + const sharedContext = { + account: '', + } - const DiscoverService = new DiscoverWriteService(account) + sharedContext.SNTService = new SNTService(sharedContext) + sharedContext.DiscoverService = new DiscoverService(sharedContext) return { - DiscoverService, + SNTService: sharedContext.SNTService, + DiscoverService: sharedContext.DiscoverService, } } catch (error) { - // TODO: Should handle it in an elegant way throw new Error(error.message) } } -export default { init, ...ReadOnlyServices } +export default { init } diff --git a/src/common/blockchain/snt-services/snt-service.js b/src/common/blockchain/snt-services/snt-service.js new file mode 100644 index 0000000..f1d5012 --- /dev/null +++ b/src/common/blockchain/snt-services/snt-service.js @@ -0,0 +1,46 @@ +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.address, 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(this.service) + await this.validator.validateApproveAndCall(spender, amount) + + await SNTToken.methods + .approveAndCall(spender, amount, callData) + .send({ from: this.sharedContext.account }) + } + + // This is for testing purpose only + async generateTokens() { + await super.__unlockServiceAccount(this.service) + + await SNTToken.methods + .generateTokens(this.sharedContext.account, 10000) + .send() + } +} + +export default SNTService diff --git a/src/common/blockchain/snt-services/snt-validator.js b/src/common/blockchain/snt-services/snt-validator.js new file mode 100644 index 0000000..6ba8d1b --- /dev/null +++ b/src/common/blockchain/snt-services/snt-validator.js @@ -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 diff --git a/src/modules/BlockchainExample/BlockchainExample.jsx b/src/modules/BlockchainExample/BlockchainExample.jsx index b0df235..b4fdbc9 100644 --- a/src/modules/BlockchainExample/BlockchainExample.jsx +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -4,13 +4,11 @@ import BlockchainSDK from '../../common/blockchain' class Example extends React.Component { async logDiscoverMethod() { const services = await BlockchainSDK.init() - console.log( - await services.DiscoverService.createDApp( - '0x123', - '100000000000000000', - '0x123', - ), - ) + + console.log(await services.SNTService.controller()) + // await services.SNTService.generateTokens() + // await services.DiscoverService.createDApp('0x2', 10000, '0x2') + // console.log(await services.DiscoverService.getDAppById('0x2')) } render() { From c3920055bb6afbedb7517d41a9261e767f8dac85 Mon Sep 17 00:00:00 2001 From: Lyubomir Kiprov Date: Fri, 3 May 2019 18:50:54 +0300 Subject: [PATCH 4/6] Add IPFS data uploading --- .gitignore | 1 + config/blockchain.js | 5 -- config/contracts.js | 9 ++- config/development/mnemonic.js | 2 + contracts/Discover.sol | 9 +++ src/common/blockchain/blockchain-service.js | 24 ------- src/common/blockchain/index.js | 14 ++-- src/common/blockchain/ipfs/helpers.js | 11 +++ src/common/blockchain/ipfs/index.js | 50 ++++++++++++++ .../blockchain/sdk/blockchain-service.js | 31 +++++++++ src/common/blockchain/sdk/config.js | 8 +++ .../discover-services/discover-service.js | 67 +++++++++++++------ .../discover-services/discover-validator.js | 0 src/common/blockchain/sdk/helpers.js | 9 +++ .../{ => sdk}/snt-services/snt-service.js | 19 +++--- .../{ => sdk}/snt-services/snt-validator.js | 0 src/common/blockchain/utils.js | 18 +++++ .../BlockchainExample/BlockchainExample.jsx | 12 ++-- 18 files changed, 217 insertions(+), 72 deletions(-) create mode 100644 config/development/mnemonic.js delete mode 100644 src/common/blockchain/blockchain-service.js create mode 100644 src/common/blockchain/ipfs/helpers.js create mode 100644 src/common/blockchain/ipfs/index.js create mode 100644 src/common/blockchain/sdk/blockchain-service.js create mode 100644 src/common/blockchain/sdk/config.js rename src/common/blockchain/{ => sdk}/discover-services/discover-service.js (53%) rename src/common/blockchain/{ => sdk}/discover-services/discover-validator.js (100%) create mode 100644 src/common/blockchain/sdk/helpers.js rename src/common/blockchain/{ => sdk}/snt-services/snt-service.js (69%) rename src/common/blockchain/{ => sdk}/snt-services/snt-validator.js (100%) create mode 100644 src/common/blockchain/utils.js diff --git a/.gitignore b/.gitignore index 98f13c7..0e97b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .embark chains.json +config/development/mnemonic config/livenet/password config/production/password coverage diff --git a/config/blockchain.js b/config/blockchain.js index d1bce0d..a6944ec 100644 --- a/config/blockchain.js +++ b/config/blockchain.js @@ -29,11 +29,6 @@ module.exports = { // 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: - // '0xEFA9DB87A755C9D2B96F77BBCB9EF06CBDDFC01DB1A5129CE2649F73E9C2739C', - // balance: '100 ether', - // }, - // { // privateKeyFile: 'path/to/file', // Either a keystore or a list of keys, separated by , or ; // password: 'passwordForTheKeystore', // Needed to decrypt the keystore file // }, diff --git a/config/contracts.js b/config/contracts.js index c2d1c59..8a06264 100644 --- a/config/contracts.js +++ b/config/contracts.js @@ -1,3 +1,5 @@ +const wallet = require('./development/mnemonic') + module.exports = { // default applies to all environments default: { @@ -31,9 +33,8 @@ module.exports = { accounts: [ { - privateKey: - '0xEFA9DB87A755C9D2B96F77BBCB9EF06CBDDFC01DB1A5129CE2649F73E9C2739C', - balance: '100 ether', + mnemonic: wallet.mnemonic, + balance: '1534983463450 ether', }, ], }, @@ -72,7 +73,6 @@ module.exports = { TestBancorFormula: { deploy: false }, MiniMeTokenFactory: {}, SNT: { - from: '0x68C864373C6631984B646453138557A81224ACf6', instanceOf: 'MiniMeToken', args: [ '$MiniMeTokenFactory', @@ -85,7 +85,6 @@ module.exports = { ], }, Discover: { - from: '0x68C864373C6631984B646453138557A81224ACf6', args: ['$SNT'], }, }, diff --git a/config/development/mnemonic.js b/config/development/mnemonic.js new file mode 100644 index 0000000..45c9c58 --- /dev/null +++ b/config/development/mnemonic.js @@ -0,0 +1,2 @@ +module.exports.mnemonic = + 'artefact rebuild liquid honey sport clean candy motor cereal job gap series' diff --git a/contracts/Discover.sol b/contracts/Discover.sol index 1f57aeb..9207402 100644 --- a/contracts/Discover.sol +++ b/contracts/Discover.sol @@ -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. diff --git a/src/common/blockchain/blockchain-service.js b/src/common/blockchain/blockchain-service.js deleted file mode 100644 index 8e05ac7..0000000 --- a/src/common/blockchain/blockchain-service.js +++ /dev/null @@ -1,24 +0,0 @@ -import EmbarkJS from '../../embarkArtifacts/embarkjs' - -class BlockchainService { - constructor(sharedContext, contractAddress, Validator) { - this.contract = contractAddress - this.sharedContext = sharedContext - this.validator = new Validator(this) - } - - async __unlockServiceAccount() { - try { - const accounts = await EmbarkJS.Blockchain.Providers.web3.getAccounts() - if (accounts.length > 0) { - this.sharedContext.account = accounts[0] - } - - this.sharedContext.account = (await EmbarkJS.enableEthereum())[0] - } catch (error) { - throw new Error('Could not unlock an account or web3 is missing') - } - } -} - -export default BlockchainService diff --git a/src/common/blockchain/index.js b/src/common/blockchain/index.js index 800d559..ebaa71e 100644 --- a/src/common/blockchain/index.js +++ b/src/common/blockchain/index.js @@ -1,8 +1,13 @@ -import SNTService from './snt-services/snt-service' -import DiscoverService from './discover-services/discover-service' +import utils from './utils' +import SNTService from './sdk/snt-services/snt-service' +import DiscoverService from './sdk/discover-services/discover-service' -const init = async function() { +import BlockchainConfig from './sdk/config' + +const init = function() { try { + BlockchainConfig() + const sharedContext = { account: '', } @@ -13,10 +18,11 @@ const init = async function() { return { SNTService: sharedContext.SNTService, DiscoverService: sharedContext.DiscoverService, + utils, } } catch (error) { throw new Error(error.message) } } -export default { init } +export default { init, utils } diff --git a/src/common/blockchain/ipfs/helpers.js b/src/common/blockchain/ipfs/helpers.js new file mode 100644 index 0000000..6550afe --- /dev/null +++ b/src/common/blockchain/ipfs/helpers.js @@ -0,0 +1,11 @@ +export const base64ToBlob = base64Text => { + const byteString = atob(base64Text.split(',')[1]) + + 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]) +} diff --git a/src/common/blockchain/ipfs/index.js b/src/common/blockchain/ipfs/index.js new file mode 100644 index 0000000..350380d --- /dev/null +++ b/src/common/blockchain/ipfs/index.js @@ -0,0 +1,50 @@ +import { base64ToBlob } from './helpers' + +// Todo: EmbarkJS -> setup it in init +// Todo: Should check for isAvailable +import EmbarkJS from '../../../embarkArtifacts/embarkjs' + +EmbarkJS.Storage.setProvider('ipfs') + +export const uploadMetadata = async metadata => { + try { + const hash = await EmbarkJS.Storage.saveText(metadata) + return hash + } catch (error) { + throw new Error( + `Uploading DApp metadata to IPFS failed. Details: ${error.message}`, + ) + } +} + +// Todo: should convert base64 image into binary data in order to upload it on IPFS +export const uploadImage = async base64Image => { + try { + const imageFile = [ + { + files: [base64ToBlob(base64Image)], + }, + ] + const hash = await EmbarkJS.Storage.uploadFile(imageFile) + return hash + } catch (error) { + throw new Error( + `Uploading DApp image to IPFS failed. Details: ${error.message}`, + ) + } +} + +export const retrieveMetadata = async metadataHash => { + try { + const metadata = await EmbarkJS.Storage.get(metadataHash) + return metadata + } catch (error) { + throw new Error( + `Fetching metadata from IPFS failed. Details: ${error.message}`, + ) + } +} + +export const retrieveImageUrl = async imageHash => { + return EmbarkJS.Storage.getUrl(imageHash) +} diff --git a/src/common/blockchain/sdk/blockchain-service.js b/src/common/blockchain/sdk/blockchain-service.js new file mode 100644 index 0000000..5f4687f --- /dev/null +++ b/src/common/blockchain/sdk/blockchain-service.js @@ -0,0 +1,31 @@ +import EmbarkJS from '../../../embarkArtifacts/embarkjs' + +class BlockchainService { + constructor(sharedContext, contract, Validator) { + this.contract = contract.address + contract.setProvider(global.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 EmbarkJS.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 diff --git a/src/common/blockchain/sdk/config.js b/src/common/blockchain/sdk/config.js new file mode 100644 index 0000000..c244753 --- /dev/null +++ b/src/common/blockchain/sdk/config.js @@ -0,0 +1,8 @@ +import Web3 from '../../../embarkArtifacts/modules/web3' + +// Should be moved to .env +const RPC_URL = 'http://localhost:8545' + +export default function() { + global.web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL)) +} diff --git a/src/common/blockchain/discover-services/discover-service.js b/src/common/blockchain/sdk/discover-services/discover-service.js similarity index 53% rename from src/common/blockchain/discover-services/discover-service.js rename to src/common/blockchain/sdk/discover-services/discover-service.js index 3953e25..25c314f 100644 --- a/src/common/blockchain/discover-services/discover-service.js +++ b/src/common/blockchain/sdk/discover-services/discover-service.js @@ -1,19 +1,17 @@ +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' - -// TODO: Validators ? - YUP -// TODO: check for unlocked account: If it is not -> request unlocking - YUP -// TODO: Make transfer failed an Error object ? +import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover' class DiscoverService extends BlockchainService { constructor(sharedContext) { - super(sharedContext, DiscoverContract.address, DiscoverValidator) + super(sharedContext, DiscoverContract, DiscoverValidator) } // TODO: Amount -> string/bigInt/number ? - // TODO: Maybe we can get id from a DApp name ? // TODO: formatBigNumberToNumber // View methods @@ -31,6 +29,16 @@ class DiscoverService extends BlockchainService { return DiscoverContract.methods.upvoteEffect(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) { try { const dappId = await DiscoverContract.methods.id2index(id).call() @@ -51,14 +59,22 @@ class DiscoverService extends BlockchainService { } // Transaction methods - async createDApp(id, amount, metadata) { - await this.validator.validateDAppCreation(id, amount) + async createDApp(amount, metadata) { + const dappMetadata = JSON.parse(JSON.stringify(metadata)) + const dappId = global.web3.keccak256(JSON.stringify(dappMetadata)) + + await this.validator.validateDAppCreation(dappId, amount) + + dappMetadata.image = await ipfsSDK.uploadImage(dappMetadata.image) + const metadataHash = await ipfsSDK.uploadMetadata( + JSON.stringify(dappMetadata), + ) const callData = DiscoverContract.methods - .createDApp(id, amount, metadata) + .createDApp(dappId, amount, metadataHash) .encodeABI() - await this.sharedContext.SNTService.approveAndCall( + return this.sharedContext.SNTService.approveAndCall( this.contract, amount, callData, @@ -69,7 +85,7 @@ class DiscoverService extends BlockchainService { await this.validator.validateUpVoting(id, amount) const callData = DiscoverContract.methods.upvote(id, amount).encodeABI() - await this.sharedContext.SNTService.approveAndCall( + return this.sharedContext.SNTService.approveAndCall( this.contract, amount, callData, @@ -80,7 +96,7 @@ class DiscoverService extends BlockchainService { await this.validator.validateDownVoting(id, amount) const callData = DiscoverContract.methods.downvote(id, amount).encodeABI() - await this.sharedContext.SNTService.approveAndCall( + return this.sharedContext.SNTService.approveAndCall( this.contract, amount, callData, @@ -88,25 +104,32 @@ class DiscoverService extends BlockchainService { } async withdraw(id, amount) { - await super.__unlockServiceAccount(this.service) + await super.__unlockServiceAccount() await this.validator.validateWithdrawing(id, amount) try { - await DiscoverContract.methods - .withdraw(id, amount) - .send({ from: this.sharedContext.account }) + return broadcastContractFn( + DiscoverContract.methods.withdraw(id, amount).send, + this.sharedContext.account, + ) } catch (error) { - throw new Error('Transfer on withdraw failed') + throw new Error(`Transfer on withdraw failed. Details: ${error.message}`) } } + // Todo: Should we upload the metadata to IPFS async setMetadata(id, metadata) { - await super.__unlockServiceAccount(this.service) + await super.__unlockServiceAccount() await this.validator.validateMetadataSet(id) - await DiscoverContract.methods - .setMetadata(id, metadata) - .send({ from: this.sharedContext.account }) + try { + return broadcastContractFn( + DiscoverContract.methods.setMetadata(id, metadata).send, + this.sharedContext.account, + ) + } catch (error) { + throw new Error(`Uploading metadata failed. Details: ${error.message}`) + } } } diff --git a/src/common/blockchain/discover-services/discover-validator.js b/src/common/blockchain/sdk/discover-services/discover-validator.js similarity index 100% rename from src/common/blockchain/discover-services/discover-validator.js rename to src/common/blockchain/sdk/discover-services/discover-validator.js diff --git a/src/common/blockchain/sdk/helpers.js b/src/common/blockchain/sdk/helpers.js new file mode 100644 index 0000000..e6ed32b --- /dev/null +++ b/src/common/blockchain/sdk/helpers.js @@ -0,0 +1,9 @@ +export default { + broadcastContractFn: (contractMethod, account) => { + return new Promise(resolve => { + contractMethod({ from: account }).on('transactionHash', hash => { + resolve(hash) + }) + }) + }, +} diff --git a/src/common/blockchain/snt-services/snt-service.js b/src/common/blockchain/sdk/snt-services/snt-service.js similarity index 69% rename from src/common/blockchain/snt-services/snt-service.js rename to src/common/blockchain/sdk/snt-services/snt-service.js index f1d5012..8c626b6 100644 --- a/src/common/blockchain/snt-services/snt-service.js +++ b/src/common/blockchain/sdk/snt-services/snt-service.js @@ -1,11 +1,13 @@ +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) { - super(sharedContext, SNTToken.address, SNTValidator) + super(sharedContext, SNTToken, SNTValidator) } async allowance(from, to) { @@ -25,21 +27,22 @@ class SNTService extends BlockchainService { } async approveAndCall(spender, amount, callData) { - await super.__unlockServiceAccount(this.service) + await super.__unlockServiceAccount() await this.validator.validateApproveAndCall(spender, amount) - await SNTToken.methods - .approveAndCall(spender, amount, callData) - .send({ from: this.sharedContext.account }) + return broadcastContractFn( + SNTToken.methods.approveAndCall(spender, amount, callData).send, + this.sharedContext.account, + ) } // This is for testing purpose only async generateTokens() { - await super.__unlockServiceAccount(this.service) + await super.__unlockServiceAccount() await SNTToken.methods .generateTokens(this.sharedContext.account, 10000) - .send() + .send({ from: this.sharedContext.account }) } } diff --git a/src/common/blockchain/snt-services/snt-validator.js b/src/common/blockchain/sdk/snt-services/snt-validator.js similarity index 100% rename from src/common/blockchain/snt-services/snt-validator.js rename to src/common/blockchain/sdk/snt-services/snt-validator.js diff --git a/src/common/blockchain/utils.js b/src/common/blockchain/utils.js new file mode 100644 index 0000000..c9851ca --- /dev/null +++ b/src/common/blockchain/utils.js @@ -0,0 +1,18 @@ +const TRANSACTION_STATUSES = { + Failed: 0, + Successful: 1, + Pending: 2, +} + +export default { + getTxStatus: async txHash => { + const txReceipt = await global.web3.eth.getTransactionReceipt(txHash) + if (txReceipt) { + return txReceipt.status + ? TRANSACTION_STATUSES.Successful + : TRANSACTION_STATUSES.Failed + } + + return TRANSACTION_STATUSES.Pending + }, +} diff --git a/src/modules/BlockchainExample/BlockchainExample.jsx b/src/modules/BlockchainExample/BlockchainExample.jsx index b4fdbc9..848aa07 100644 --- a/src/modules/BlockchainExample/BlockchainExample.jsx +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -3,17 +3,21 @@ import BlockchainSDK from '../../common/blockchain' class Example extends React.Component { async logDiscoverMethod() { - const services = await BlockchainSDK.init() - - console.log(await services.SNTService.controller()) + // 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')) } render() { - return

+ return ( +
+

+

+ ) } } export default Example +// QmZGzoAEEZoFP9jYXoVfhkDqXHxVrFCSMxSU8eGQpcDNHw From 2b55cc79559883f9185b04f557a8cb1b93cd0677 Mon Sep 17 00:00:00 2001 From: Lyubomir Kiprov Date: Tue, 7 May 2019 18:25:11 +0300 Subject: [PATCH 5/6] 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 --- src/common/blockchain/index.js | 6 +- src/common/blockchain/ipfs/helpers.js | 19 +- src/common/blockchain/ipfs/index.js | 40 ++- src/common/blockchain/sdk/helpers.js | 9 - .../blockchain/{sdk => services}/config.js | 5 +- .../contracts-services}/blockchain-service.js | 20 +- .../discover-service}/discover-service.js | 49 ++- .../discover-service}/discover-validator.js | 31 +- .../services/contracts-services/helpers.js | 7 + .../snt-service}/snt-service.js | 4 +- .../snt-service}/snt-validator.js | 0 .../services/embark-service/embark-service.js | 14 + src/common/blockchain/utils.js | 4 +- .../BlockchainExample/BlockchainExample.jsx | 82 ++++- src/modules/BlockchainExample/dapp.image.json | 3 + utils/testUtils.js | 300 ++++++++++-------- 16 files changed, 366 insertions(+), 227 deletions(-) delete mode 100644 src/common/blockchain/sdk/helpers.js rename src/common/blockchain/{sdk => services}/config.js (53%) rename src/common/blockchain/{sdk => services/contracts-services}/blockchain-service.js (56%) rename src/common/blockchain/{sdk/discover-services => services/contracts-services/discover-service}/discover-service.js (73%) rename src/common/blockchain/{sdk/discover-services => services/contracts-services/discover-service}/discover-validator.js (68%) create mode 100644 src/common/blockchain/services/contracts-services/helpers.js rename src/common/blockchain/{sdk/snt-services => services/contracts-services/snt-service}/snt-service.js (90%) rename src/common/blockchain/{sdk/snt-services => services/contracts-services/snt-service}/snt-validator.js (100%) create mode 100644 src/common/blockchain/services/embark-service/embark-service.js create mode 100644 src/modules/BlockchainExample/dapp.image.json diff --git a/src/common/blockchain/index.js b/src/common/blockchain/index.js index ebaa71e..c441624 100644 --- a/src/common/blockchain/index.js +++ b/src/common/blockchain/index.js @@ -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 { diff --git a/src/common/blockchain/ipfs/helpers.js b/src/common/blockchain/ipfs/helpers.js index 6550afe..0ed5ab3 100644 --- a/src/common/blockchain/ipfs/helpers.js +++ b/src/common/blockchain/ipfs/helpers.js @@ -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 +} diff --git a/src/common/blockchain/ipfs/index.js b/src/common/blockchain/ipfs/index.js index 350380d..1dccc5a 100644 --- a/src/common/blockchain/ipfs/index.js +++ b/src/common/blockchain/ipfs/index.js @@ -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) } diff --git a/src/common/blockchain/sdk/helpers.js b/src/common/blockchain/sdk/helpers.js deleted file mode 100644 index e6ed32b..0000000 --- a/src/common/blockchain/sdk/helpers.js +++ /dev/null @@ -1,9 +0,0 @@ -export default { - broadcastContractFn: (contractMethod, account) => { - return new Promise(resolve => { - contractMethod({ from: account }).on('transactionHash', hash => { - resolve(hash) - }) - }) - }, -} diff --git a/src/common/blockchain/sdk/config.js b/src/common/blockchain/services/config.js similarity index 53% rename from src/common/blockchain/sdk/config.js rename to src/common/blockchain/services/config.js index c244753..a37d448 100644 --- a/src/common/blockchain/sdk/config.js +++ b/src/common/blockchain/services/config.js @@ -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)) } diff --git a/src/common/blockchain/sdk/blockchain-service.js b/src/common/blockchain/services/contracts-services/blockchain-service.js similarity index 56% rename from src/common/blockchain/sdk/blockchain-service.js rename to src/common/blockchain/services/contracts-services/blockchain-service.js index 5f4687f..1a20850 100644 --- a/src/common/blockchain/sdk/blockchain-service.js +++ b/src/common/blockchain/services/contracts-services/blockchain-service.js @@ -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) // } diff --git a/src/common/blockchain/sdk/discover-services/discover-service.js b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js similarity index 73% rename from src/common/blockchain/sdk/discover-services/discover-service.js rename to src/common/blockchain/services/contracts-services/discover-service/discover-service.js index 25c314f..a9f830b 100644 --- a/src/common/blockchain/sdk/discover-services/discover-service.js +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js @@ -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) { diff --git a/src/common/blockchain/sdk/discover-services/discover-validator.js b/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js similarity index 68% rename from src/common/blockchain/sdk/discover-services/discover-validator.js rename to src/common/blockchain/services/contracts-services/discover-service/discover-validator.js index a67da95..a85ff86 100644 --- a/src/common/blockchain/sdk/discover-services/discover-validator.js +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js @@ -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') } diff --git a/src/common/blockchain/services/contracts-services/helpers.js b/src/common/blockchain/services/contracts-services/helpers.js new file mode 100644 index 0000000..5fe53ff --- /dev/null +++ b/src/common/blockchain/services/contracts-services/helpers.js @@ -0,0 +1,7 @@ +export const broadcastContractFn = (contractMethod, account) => { + return new Promise(resolve => { + contractMethod({ from: account }).on('transactionHash', hash => { + resolve(hash) + }) + }) +} diff --git a/src/common/blockchain/sdk/snt-services/snt-service.js b/src/common/blockchain/services/contracts-services/snt-service/snt-service.js similarity index 90% rename from src/common/blockchain/sdk/snt-services/snt-service.js rename to src/common/blockchain/services/contracts-services/snt-service/snt-service.js index 8c626b6..d486ee8 100644 --- a/src/common/blockchain/sdk/snt-services/snt-service.js +++ b/src/common/blockchain/services/contracts-services/snt-service/snt-service.js @@ -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) { diff --git a/src/common/blockchain/sdk/snt-services/snt-validator.js b/src/common/blockchain/services/contracts-services/snt-service/snt-validator.js similarity index 100% rename from src/common/blockchain/sdk/snt-services/snt-validator.js rename to src/common/blockchain/services/contracts-services/snt-service/snt-validator.js diff --git a/src/common/blockchain/services/embark-service/embark-service.js b/src/common/blockchain/services/embark-service/embark-service.js new file mode 100644 index 0000000..48ac056 --- /dev/null +++ b/src/common/blockchain/services/embark-service/embark-service.js @@ -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() diff --git a/src/common/blockchain/utils.js b/src/common/blockchain/utils.js index c9851ca..53a5ddf 100644 --- a/src/common/blockchain/utils.js +++ b/src/common/blockchain/utils.js @@ -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 diff --git a/src/modules/BlockchainExample/BlockchainExample.jsx b/src/modules/BlockchainExample/BlockchainExample.jsx index 848aa07..477708e 100644 --- a/src/modules/BlockchainExample/BlockchainExample.jsx +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -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 (
-

+

+

) } } export default Example -// QmZGzoAEEZoFP9jYXoVfhkDqXHxVrFCSMxSU8eGQpcDNHw diff --git a/src/modules/BlockchainExample/dapp.image.json b/src/modules/BlockchainExample/dapp.image.json new file mode 100644 index 0000000..3f5aaf4 --- /dev/null +++ b/src/modules/BlockchainExample/dapp.image.json @@ -0,0 +1,3 @@ +{ + "image": "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" +} diff --git a/utils/testUtils.js b/utils/testUtils.js index 803b342..afe32cc 100644 --- a/utils/testUtils.js +++ b/utils/testUtils.js @@ -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 -} \ No newline at end of file +} From d8359163c2ecc67394217bcc251d6b50ccafa875 Mon Sep 17 00:00:00 2001 From: Lyubomir Kiprov Date: Wed, 8 May 2019 15:45:17 +0300 Subject: [PATCH 6/6] Upload metadata on setMetadata --- src/common/blockchain/ipfs/index.js | 59 ++++++++++--------- .../discover-service/discover-service.js | 15 ++--- .../discover-service/discover-validator.js | 2 +- .../BlockchainExample/BlockchainExample.jsx | 22 ++++++- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/common/blockchain/ipfs/index.js b/src/common/blockchain/ipfs/index.js index 1dccc5a..f66de20 100644 --- a/src/common/blockchain/ipfs/index.js +++ b/src/common/blockchain/ipfs/index.js @@ -8,13 +8,29 @@ const checkIPFSAvailability = async () => { } } -export const uploadMetadata = async metadata => { +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() - const hash = await EmbarkJSService.Storage.saveText(metadata) - const metadataInBytes = helpers.getBytes32FromIpfsHash(hash) - return metadataInBytes + 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}`, @@ -22,31 +38,21 @@ export const uploadMetadata = async metadata => { } } -export const uploadImage = async base64Image => { - try { - await checkIPFSAvailability() - - const imageFile = [ - { - files: [helpers.base64ToBlob(base64Image)], - }, - ] - - const hash = await EmbarkJSService.Storage.uploadFile(imageFile) - return hash - } catch (error) { - throw new Error( - `Uploading DApp image to IPFS failed. Details: ${error.message}`, - ) - } +const retrieveMetadata = async metadataBytes32 => { + const metadataHash = helpers.getIpfsHashFromBytes32(metadataBytes32) + return EmbarkJSService.Storage.get(metadataHash) } -export const retrieveMetadata = async metadataBytes32 => { +const retrieveImageUrl = async imageHash => { + return EmbarkJSService.Storage.getUrl(imageHash) +} + +export const retrieveDAppMetadataByHash = async metadataBytes32 => { try { await checkIPFSAvailability() - const metadataHash = helpers.getIpfsHashFromBytes32(metadataBytes32) - const metadata = await EmbarkJSService.Storage.get(metadataHash) + const metadata = JSON.parse(await retrieveMetadata(metadataBytes32)) + metadata.image = await retrieveImageUrl(metadata.image) return metadata } catch (error) { @@ -55,8 +61,3 @@ export const retrieveMetadata = async metadataBytes32 => { ) } } - -export const retrieveImageUrl = async imageHash => { - await checkIPFSAvailability() - return EmbarkJSService.Storage.getUrl(imageHash) -} diff --git a/src/common/blockchain/services/contracts-services/discover-service/discover-service.js b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js index a9f830b..9e27088 100644 --- a/src/common/blockchain/services/contracts-services/discover-service/discover-service.js +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js @@ -54,9 +54,7 @@ class DiscoverService extends BlockchainService { 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) - + dapp.metadata = await ipfsSDK.retrieveDAppMetadataByHash(dapp.metadata) return dapp } catch (error) { throw new Error('Error fetching correct data from IPFS') @@ -78,10 +76,7 @@ class DiscoverService extends BlockchainService { await this.validator.validateDAppCreation(dappId, amount) - dappMetadata.image = await ipfsSDK.uploadImage(dappMetadata.image) - const uploadedMetadata = await ipfsSDK.uploadMetadata( - JSON.stringify(dappMetadata), - ) + const uploadedMetadata = await ipfsSDK.uploadDAppMetadata(dappMetadata) const callData = DiscoverContract.methods .createDApp(dappId, amount, uploadedMetadata) @@ -132,14 +127,16 @@ class DiscoverService extends BlockchainService { } } - // Todo: Should we upload the metadata to IPFS 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, metadata).send, + DiscoverContract.methods.setMetadata(id, uploadedMetadata).send, this.sharedContext.account, ) } catch (error) { diff --git a/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js b/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js index a85ff86..17a4c3c 100644 --- a/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js @@ -67,7 +67,7 @@ class DiscoverValidator { async validateMetadataSet(id) { const dapp = await this.service.getDAppById(id) - if (dapp.developer != this.service.sharedContext.account) { + if (dapp.developer.toLowerCase() != this.service.sharedContext.account) { throw new Error('Only the developer can update the metadata') } } diff --git a/src/modules/BlockchainExample/BlockchainExample.jsx b/src/modules/BlockchainExample/BlockchainExample.jsx index 477708e..4882095 100644 --- a/src/modules/BlockchainExample/BlockchainExample.jsx +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -58,8 +58,19 @@ class Example extends React.Component { 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)}`, @@ -72,8 +83,15 @@ class Example extends React.Component { console.log( `DownVoteCost Result : ${await this.downVoteCost(createdDApp.id)}`, ) - const dappData = await this.getFullDApp(createdDApp.id) - document.getElementById('testImage').src = dappData.metadata.image + + console.log( + `Set metadata TX Hash : ${await this.setMetadata(createdDApp.id)}`, + ) + console.log( + `Updated DApp : ${JSON.stringify( + await this.getFullDApp(createdDApp.id), + )}`, + ) } render() {